Study Notes
本博主會持續(xù)更新各種前端的技術(shù),如果各位道友喜歡族淮,可以關(guān)注讲弄、收藏爽航、點(diǎn)贊下本博主的文章焕盟。
模擬 VueRouter
前置的知識:
- 插件
- slot 插槽
- 混入
- render 函數(shù)
- 運(yùn)行時和完整版的 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)思路
這里模擬的是一個簡單的ruoter
的 history 模式椭符,不能嵌套使用
- 創(chuàng)建 VueRouter 插件剪勿,靜態(tài)方法 install
- 判斷插件是否已經(jīng)被加載
- 當(dāng) Vue 加載的時候把傳入的 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)路徑改變的時候通過當(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í)例時傳入的router對象注入到Vue實(shí)例
// _Vue.prototype.$router = this.$options.router;
// 如果我們直接使用Vue的原型鏈將router注入髓窜,會有以下的問題
// 因?yàn)閕nstall是靜態(tài)方法扇苞,會被VueRouter.install調(diào)用,這時this將指向VueRouter對象
// 所以我們這邊使用混入
_Vue.mixin({
beforeCreate() {
// 因?yàn)樵谑褂眠^程中beforeCreate方法會被不停的調(diào)用寄纵,然而我們這邊只需要執(zhí)行一次掛載
// 判斷this.$options.router是否存在
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
}
},
});
}
}
}
VueRouter 構(gòu)造函數(shù)
Vue.observable(object)
讓一個對象可響應(yīng)鳖敷。Vue 內(nèi)部會用它來處理 data 函數(shù)返回的對象。
返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi)程拭,并且會在發(fā)生變更時觸發(fā)相應(yīng)的更新定踱。也可以作為最小化的跨組件狀態(tài)存儲器,用于簡單的場景
這里我們使用 Vue.observable 創(chuàng)建一個響應(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)
// 讓一個對象可響應(yīng)亦歉。Vue 內(nèi)部會用它來處理 data 函數(shù)返回的對象。
// 返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi)畅哑,并且會在發(fā)生變更時觸發(fā)相應(yīng)的更新肴楷。
// 也可以作為最小化的跨組件狀態(tài)存儲器,用于簡單的場景
// 這里我們使用Vue.observable創(chuàng)建一個響應(yīng)式對象
this.data = _Vue.observable({
current: this.mode === 'hash' ? hash.replace('#', '') : pathname, // 存儲當(dāng)前路由地址
});
this.init();
}
}
createRouterMap
遍歷所有的路由規(guī)則荠呐,把路由規(guī)則解析成鍵值對的形式存儲到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)行時編譯器的 Vue 構(gòu)建版本。設(shè)置為 true 后你就可以在 Vue 組件中使用 template 選項(xiàng)了泥张,但是這會讓你的應(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)式對象瑰剃,它改變時,會重新渲染頁面
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}
// 一個 HTML 標(biāo)簽名、組件選項(xiàng)對象粤剧,或者
// resolve 了上述任何一種的一個 async 函數(shù)歇竟。必填項(xiàng)。
'div',
// {Object}
// 一個與模板中 attribute 對應(yīng)的數(shù)據(jù)對象抵恋』酪椋可選。
{
// (詳情見下一節(jié))
},
// {String | Array}
// 子級虛擬節(jié)點(diǎn) (VNodes)弧关,由 `createElement()` 構(gòu)建而成盅安,
// 也可以使用字符串來生成“文本虛擬節(jié)點(diǎn)”∈滥遥可選别瞭。
[
'先寫一些文字',
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)式對象株憾,它改變時蝙寨,會重新渲染頁面
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í)例時傳入的router對象注入到Vue實(shí)例
// _Vue.prototype.$router = this.$options.router;
// 如果我們直接使用Vue的原型鏈將router注入,會有以下的問題
// 因?yàn)閕nstall是靜態(tài)方法嗤瞎,會被VueRouter.install調(diào)用墙歪,這時this將指向VueRouter對象
// 所以我們這邊使用混入
_Vue.mixin({
beforeCreate() {
// 因?yàn)樵谑褂眠^程中beforeCreate方法會被不停的調(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)
// 讓一個對象可響應(yīng)。Vue 內(nèi)部會用它來處理 data 函數(shù)返回的對象掉瞳。
// 返回的對象可以直接用于渲染函數(shù)和計(jì)算屬性內(nèi)毕源,并且會在發(fā)生變更時觸發(fā)相應(yīng)的更新髓帽。
// 也可以作為最小化的跨組件狀態(tài)存儲器,用于簡單的場景
// 這里我們使用Vue.observable創(chuàng)建一個響應(yīng)式對象
this.data = _Vue.observable({
current: this.mode === 'hash' ? hash.replace('#', '') : pathname, // 存儲當(dāng)前路由地址
});
this.init();
}
init() {
this.createRouterMap();
this.initComponent(_Vue);
this.initEvent();
}
createRouterMap() {
// 遍歷所有的路由規(guī)則脑豹,把路由規(guī)則解析成鍵值對的形式存儲到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)行時編譯器的 Vue 構(gòu)建版本郑藏。
// 設(shè)置為 true 后你就可以在 Vue 組件中使用 template 選項(xiàng)了.
// 但是這會讓你的應(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)式對象,它改變時俱饿,會重新渲染頁面
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)式對象,它改變時拍埠,會重新渲染頁面
this.data.current =
this.mode === 'hash'
? window.location.hash.replace('#', '')
: window.location.pathname;
});
}
}