Study Notes
本博主會(huì)持續(xù)更新各種前端的技術(shù),如果各位道友喜歡,可以關(guān)注、收藏、點(diǎn)贊下本博主的文章。
模擬 VueRouter
前置的知識:
- 插件
- slot 插槽
- 混入
- render 函數(shù)
- 運(yùn)行時(shí)和完整版的 Vue
Vue Router 的核心代碼
// 注冊插件
// Vue.use() 內(nèi)部調(diào)用傳入對象的 install 方法
Vue.use(VueRouter);
// 創(chuàng)建路由對象
const router = new VueRouter({
routes: [{ name: 'home', path: '/', component: homeComponent }],
});
// 創(chuàng)建 Vue 實(shí)例,注冊 router 對象
new Vue({
router,
render: (h) => h(App),
}).$mount('#app');
Hash 模式
- URL 中#后面的內(nèi)容作為路徑地址
- 監(jiān)聽 hashchange 事件
- 根據(jù)當(dāng)前路由地址找到對應(yīng)的組件重新渲染
history 模式
- 通過 history.pushState()方法改變地址欄
- 監(jiān)聽 popstate 事件
- 根據(jù)當(dāng)前路由地址找到對應(yīng)的組件重新渲染
實(shí)現(xiàn)思路
這里模擬的是一個(gè)簡單的ruoter的 history 模式,不能嵌套使用
- 創(chuàng)建 VueRouter 插件,靜態(tài)方法 install
- 判斷插件是否已經(jīng)被加載
- 當(dāng) Vue 加載的時(shí)候把傳入的 router 對象掛載到 Vue 實(shí)例上(注意:只執(zhí)行一次)
- 創(chuàng)建 VueRouter 類
- 初始化,options、routeMap、app(簡化操作,創(chuàng)建 Vue 實(shí)例作為響應(yīng)式數(shù)據(jù)記錄當(dāng)前路徑)
- initRouteMap() 遍歷所有路由信息,把組件和路由的映射記錄到 routeMap 對象中
- 注冊 popstate 事件,當(dāng)路由地址發(fā)生變化,重新記錄當(dāng)前的路徑
- 創(chuàng)建 router-link 和 router-view 組件
- 當(dāng)路徑改變的時(shí)候通過當(dāng)前路徑在 routerMap 對象中找到對應(yīng)的組件,渲染 router-view
install
let _Vue = null;
export default class VueRouter {
static install(Vue) {
// 判斷是否已經(jīng)加載過install
if (!VueRouter.install.installed) {
// 將狀態(tài)改為已加載
VueRouter.install.installed = true;
// 將Vue的構(gòu)造函數(shù)記錄到全局
_Vue = Vue;
// 將創(chuàng)建Vue的實(shí)例時(shí)傳入的router對象注入到Vue實(shí)例
// _Vue.prototype.$router = this.$options.router;
// 如果我們直接使用Vue的原型鏈將router注入,會(huì)有以下的問題
// 因?yàn)閕nstall是靜態(tài)方法,會(huì)被VueRouter.install調(diào)用,這時(shí)this將指向VueRouter對象
// 所以我們這邊使用混入
_Vue.mixin({
beforeCreate() {
// 因?yàn)樵谑褂眠^程中beforeCreate方法會(huì)被不停的調(diào)用,然而我們這邊只需要執(zhí)行一次掛載
// 判斷this.$options.router是否存在
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
}
},
});
}
}
}
VueRouter 構(gòu)造函數(shù)
Vue.observable(object)
讓一個(gè)對象可響應(yīng)。Vue 內(nèi)部會(huì)用它來處理 data 函數(shù)返回的對象。
返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi),并且會(huì)在發(fā)生變更時(shí)觸發(fā)相應(yīng)的更新。也可以作為最小化的跨組件狀態(tài)存儲(chǔ)器,用于簡單的場景
這里我們使用 Vue.observable 創(chuàng)建一個(gè)響應(yīng)式對象
let _Vue = null;
export default class VueRouter {
constructor(options) {
this.options = options;
// 設(shè)置路由模式,默認(rèn)hash
this.mode = options.mode || 'hash';
this.routerMap = {};
let pathname = window.location.pathname;
let search = window.location.search;
!window.location.hash &&
history.pushState({}, '', `${pathname + search}#/`); // 如果hash不存在,則改變地址欄地址
let hash = window.location.hash;
// Vue.observable(object)
// 讓一個(gè)對象可響應(yīng)。Vue 內(nèi)部會(huì)用它來處理 data 函數(shù)返回的對象。
// 返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi),并且會(huì)在發(fā)生變更時(shí)觸發(fā)相應(yīng)的更新。
// 也可以作為最小化的跨組件狀態(tài)存儲(chǔ)器,用于簡單的場景
// 這里我們使用Vue.observable創(chuàng)建一個(gè)響應(yīng)式對象
this.data = _Vue.observable({
current: this.mode === 'hash' ? hash.replace('#', '') : pathname, // 存儲(chǔ)當(dāng)前路由地址
});
this.init();
}
}
createRouterMap
遍歷所有的路由規(guī)則,把路由規(guī)則解析成鍵值對的形式存儲(chǔ)到routerMap中
export default class VueRouter {
createRouterMap() {
this.options.routes.forEach((route) => {
this.routerMap[route.path] = route.component;
});
}
}
initComponent
Vue 運(yùn)行版本不支持 template
我們有兩種方案解決
- 方案一:配置 cli 為完整版 Vue
在項(xiàng)目根目錄下創(chuàng)建vue.config.js,配置runtimeCompiler
是否使用包含運(yùn)行時(shí)編譯器的 Vue 構(gòu)建版本。設(shè)置為 true 后你就可以在 Vue 組件中使用 template 選項(xiàng)了,但是這會(huì)讓你的應(yīng)用額外增加 10kb 左右。
{ "runtimeCompiler": true }
- 方案二:使用 render 函數(shù)
export default class VueRouter {
initComponent(Vue) {
const mode = this.mode;
// 方案二:使用 render 函數(shù)
Vue.component('router-link', {
props: {
to: String,
},
// template: `<a :href="to"><slot></slot></a>`,
render(createElement) {
return createElement(
'a',
{
attrs: { href: this.to },
on: {
click: this.clickHandler, // 添加點(diǎn)擊事件
},
},
[this.$slots.default],
);
},
methods: {
clickHandler(e) {
// 阻止默認(rèn)事件
e.preventDefault();
// 如果當(dāng)前地址和需要跳轉(zhuǎn)的地址一樣,直接返回
if (this.to === this.$router.data.current) {
return;
}
// 改變地址欄
if (mode === 'history') {
history.pushState({}, '', this.to);
} else {
history.pushState(
{},
'',
`${window.location.pathname + window.location.search}#${this.to}`,
);
}
// 將當(dāng)前路由地址改為點(diǎn)擊事件里的href,這里的data是響應(yīng)式對象,它改變時(shí),會(huì)重新渲染頁面
this.$router.data.current = this.to;
},
},
});
const self = this;
Vue.component('router-view', {
render(createElement) {
// 獲取當(dāng)前路由地址對應(yīng)的路由組件
const component = self.routerMap[self.data.current];
return createElement(component);
},
});
}
}
createElement 參數(shù)
接下來你需要熟悉的是如何在 createElement 函數(shù)中使用模板中的那些功能。這里是 createElement 接受的參數(shù):
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一個(gè) HTML 標(biāo)簽名、組件選項(xiàng)對象,或者
// resolve 了上述任何一種的一個(gè) async 函數(shù)。必填項(xiàng)。
'div',
// {Object}
// 一個(gè)與模板中 attribute 對應(yīng)的數(shù)據(jù)對象??蛇x。
{
// (詳情見下一節(jié))
},
// {String | Array}
// 子級虛擬節(jié)點(diǎn) (VNodes),由 `createElement()` 構(gòu)建而成,
// 也可以使用字符串來生成“文本虛擬節(jié)點(diǎn)”??蛇x。
[
'先寫一些文字',
createElement('h1', '一則頭條'),
createElement(MyComponent, {
props: {
someProp: 'foobar',
},
}),
],
);
initEvent
export default class VueRouter {
initEvent() {
// 監(jiān)聽點(diǎn)擊后退鍵
window.addEventListener('popstate', () => {
// 將當(dāng)前路由地址改為地址欄中的pathname,這里的data是響應(yīng)式對象,它改變時(shí),會(huì)重新渲染頁面
this.data.current =
this.mode === 'hash'
? window.location.hash.replace('#', '')
: window.location.pathname;
});
}
}
完整示例
let _Vue = null;
export default class VueRouter {
static install(Vue) {
// 判斷是否已經(jīng)加載過install
if (!VueRouter.install.installed) {
// 將狀態(tài)改為已加載
VueRouter.install.installed = true;
// 將Vue的構(gòu)造函數(shù)記錄到全局
_Vue = Vue;
// 將創(chuàng)建Vue的實(shí)例時(shí)傳入的router對象注入到Vue實(shí)例
// _Vue.prototype.$router = this.$options.router;
// 如果我們直接使用Vue的原型鏈將router注入,會(huì)有以下的問題
// 因?yàn)閕nstall是靜態(tài)方法,會(huì)被VueRouter.install調(diào)用,這時(shí)this將指向VueRouter對象
// 所以我們這邊使用混入
_Vue.mixin({
beforeCreate() {
// 因?yàn)樵谑褂眠^程中beforeCreate方法會(huì)被不停的調(diào)用,然而我們這邊只需要執(zhí)行一次掛載
// 判斷this.$options.router是否存在
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
}
},
});
}
}
constructor(options) {
this.options = options;
// 設(shè)置路由模式,默認(rèn)hash
this.mode = options.mode || 'hash';
this.routerMap = {};
let pathname = window.location.pathname;
let search = window.location.search;
!window.location.hash &&
history.pushState({}, '', `${pathname + search}#/`); // 如果hash不存在,則改變地址欄地址
let hash = window.location.hash;
// Vue.observable(object)
// 讓一個(gè)對象可響應(yīng)。Vue 內(nèi)部會(huì)用它來處理 data 函數(shù)返回的對象。
// 返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi),并且會(huì)在發(fā)生變更時(shí)觸發(fā)相應(yīng)的更新。
// 也可以作為最小化的跨組件狀態(tài)存儲(chǔ)器,用于簡單的場景
// 這里我們使用Vue.observable創(chuàng)建一個(gè)響應(yīng)式對象
this.data = _Vue.observable({
current: this.mode === 'hash' ? hash.replace('#', '') : pathname, // 存儲(chǔ)當(dāng)前路由地址
});
this.init();
}
init() {
this.createRouterMap();
this.initComponent(_Vue);
this.initEvent();
}
createRouterMap() {
// 遍歷所有的路由規(guī)則,把路由規(guī)則解析成鍵值對的形式存儲(chǔ)到routerMap中
this.options.routes.forEach((route) => {
this.routerMap[route.path] = route.component;
});
}
initComponent(Vue) {
// Vue運(yùn)行版本不支持template
// 我們有兩種方案解決
// 方案一:配置cli為完整版Vue
// 在項(xiàng)目根目錄下創(chuàng)建vue.config.js
// 配置runtimeCompiler
// 是否使用包含運(yùn)行時(shí)編譯器的 Vue 構(gòu)建版本。
// 設(shè)置為 true 后你就可以在 Vue 組件中使用 template 選項(xiàng)了.
// 但是這會(huì)讓你的應(yīng)用額外增加 10kb 左右。
// {runtimeCompiler: true}
const mode = this.mode;
// 方案二:使用 render 函數(shù)
Vue.component('router-link', {
props: {
to: String,
},
// template: `<a :href="to"><slot></slot></a>`,
render(createElement) {
return createElement(
'a',
{
attrs: { href: this.to },
on: {
click: this.clickHandler, // 添加點(diǎn)擊事件
},
},
[this.$slots.default],
);
},
methods: {
clickHandler(e) {
// 阻止默認(rèn)事件
e.preventDefault();
// 如果當(dāng)前地址和需要跳轉(zhuǎn)的地址一樣,直接返回
if (this.to === this.$router.data.current) {
return;
}
// 改變地址欄
if (mode === 'history') {
history.pushState({}, '', this.to);
} else {
history.pushState(
{},
'',
`${window.location.pathname + window.location.search}#${this.to}`,
);
}
// 將當(dāng)前路由地址改為點(diǎn)擊事件里的href,這里的data是響應(yīng)式對象,它改變時(shí),會(huì)重新渲染頁面
this.$router.data.current = this.to;
},
},
});
const self = this;
Vue.component('router-view', {
render(createElement) {
// 獲取當(dāng)前路由地址對應(yīng)的路由組件
const component = self.routerMap[self.data.current];
return createElement(component);
},
});
}
initEvent() {
// 監(jiān)聽點(diǎn)擊后退鍵
window.addEventListener('popstate', () => {
// 將當(dāng)前路由地址改為地址欄中的pathname,這里的data是響應(yīng)式對象,它改變時(shí),會(huì)重新渲染頁面
this.data.current =
this.mode === 'hash'
? window.location.hash.replace('#', '')
: window.location.pathname;
});
}
}