簡(jiǎn)化版的vue-router實(shí)現(xiàn)思路詳解
本文旨在介紹 vue-router 的實(shí)現(xiàn)思路,并動(dòng)手實(shí)現(xiàn)一個(gè)簡(jiǎn)化版的 vue-router 。我們先來看一下一般項(xiàng)目中對(duì) vue-router 最基本的一個(gè)使用,可以看到,這里定義了四個(gè)路由組件,我們只要在根 vue 實(shí)例中注入該 router 對(duì)象就可以使用了.
import VueRouter from 'vue-router';
import Home from '@/components/Home';
import A from '@/components/A';
import B from '@/components/B'
import C from '@/components/C'
Vue.use(VueRouter)
export default new VueRouter.Router({
// mode: 'history',
routes: [
{
path: '/',
component: Home
},
{
path: '/a',
component: A
},
{
path: '/b',
component: B
},
{
path: '/c',
component: C
}
]
})
vue-router 提供兩個(gè)全局組件, router-view 和 router-link ,前者是用于路由組件的占位,后者用于點(diǎn)擊時(shí)跳轉(zhuǎn)到指定路由。此外組件內(nèi)部可以通過 this.$router.push , this.$rouer.replace 等api實(shí)現(xiàn)路由跳轉(zhuǎn)。本文將實(shí)現(xiàn)上述兩個(gè)全局組件以及 push 和 replace 兩個(gè)api,調(diào)用的時(shí)候支持 params 傳參,并且支持 hash 和 history 兩種模式,忽略其余api、嵌套路由、異步路由、 abstract 路由以及導(dǎo)航守衛(wèi)等高級(jí)功能的實(shí)現(xiàn),這樣有助于理解 vue-router 的核心原理。本文的最終代碼不建議在生產(chǎn)環(huán)境使用,只做一個(gè)學(xué)習(xí)用途,下面我們就來一步步實(shí)現(xiàn)它。
install實(shí)現(xiàn)
任何一個(gè) vue 插件都要實(shí)現(xiàn)一個(gè) install 方法,通過 Vue.use 調(diào)用插件的時(shí)候就是在調(diào)用插件的 install 方法,那么路由的 install 要做哪些事情呢?首先我們知道 我們會(huì)用 new 關(guān)鍵字生成一個(gè) router 實(shí)例,就像前面的代碼實(shí)例一樣,然后將其掛載到根 vue 實(shí)例上,那么作為一個(gè)全局路由,我們當(dāng)然需要在各個(gè)組件中都可以拿到這個(gè) router 實(shí)例。另外我們使用了全局組件 router-view 和 router-link ,由于 install 會(huì)接收到 Vue 構(gòu)造函數(shù)作為實(shí)參,方便我們調(diào)用 Vue.component 來注冊(cè)全局組件。因此,在 install 中主要就做兩件事,給各個(gè)組件都掛載 router 實(shí)例,以及實(shí)現(xiàn) router-view 和 router-link 兩個(gè)全局組件。下面是代碼:
const install = (Vue) => {
if (this._Vue) {
return;
};
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) {
this._routerRoot = this;
this._router = this.$options.router;
Vue.util.defineReactive(this, '_routeHistory', this._router.history)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
Object.defineProperty(this, '$router', {
get() {
return this._routerRoot._router;
}
})
Object.defineProperty(this, '$route', {
get() {
return {
current: this._routerRoot._routeHistory.current,
...this._routerRoot._router.route
};
}
})
}
});
Vue.component('router-view', {
render(h) { ... }
})
Vue.component('router-link', {
props: {
to: String,
tag: String,
},
render(h) { ... }
})
this._Vue = Vue;
}
這里的 this 代表的就是 vue-router 對(duì)象,它有兩個(gè)屬性暴露出來供外界調(diào)用,一個(gè)是 install ,一個(gè)是 Router 構(gòu)造函數(shù),這樣可以保證插件的正確安裝以及路由實(shí)例化。我們先忽略 Router 構(gòu)造函數(shù),來看 install ,上面代碼中的 this._Vue 是個(gè)開始沒有定義的屬性,他的目的是防止多次安裝。我們使用 Vue.mixin 對(duì)每個(gè)組件的 beforeCreate 鉤子做全局混入,目的是讓每個(gè)組件實(shí)例共享 router 實(shí)例,即通過 this.$router 拿到路由實(shí)例,通過 this.$route 拿到路由狀態(tài)。需要重點(diǎn)關(guān)注的是這行代碼:
Vue.util.defineReactive(this, '_routeHistory', this._router.history)
這行代碼利用 vue 的響應(yīng)式原理,對(duì)根 vue 實(shí)例注冊(cè)了一個(gè) _routeHistory 屬性,指向路由實(shí)例的 history 對(duì)象,這樣 history 也變成了響應(yīng)式的。因此一旦路由的 history 發(fā)生變化,用到這個(gè)值的組件就會(huì)觸發(fā) render 函數(shù)重新渲染,這里的組件就是 router-view 。從這里可以窺察到 vue-router 實(shí)現(xiàn)的一個(gè)基本思路。上述的代碼中對(duì)于兩個(gè)全局組件的 render 函數(shù)的實(shí)現(xiàn),因?yàn)闀?huì)依賴于 router 對(duì)象,我們先放一放,稍后再來實(shí)現(xiàn)它們,下面我們分析一下 Router 構(gòu)造函數(shù)。
Router構(gòu)造函數(shù)
經(jīng)過剛才的分析,我們知道 router 實(shí)例需要有一個(gè) history 對(duì)象,需要一個(gè)保存當(dāng)前路由狀態(tài)的對(duì)象 route ,另外很顯然還需要接受路由配置表 routes ,根據(jù) routes 需要一個(gè)路由映射表 routerMap 來實(shí)現(xiàn)組件搜索,還需要一個(gè)變量 mode 判斷是什么模式下的路由,需要實(shí)現(xiàn) push 和 replace 兩個(gè)api,代碼如下:
const Router = function (options) {
this.routes = options.routes; // 存放路由配置
this.mode = options.mode || 'hash';
this.route = Object.create(null), // 生成路由狀態(tài)
this.routerMap = createMap(this.routes) // 生成路由表
this.history = new RouterHistory(); // 實(shí)例化路由歷史對(duì)象
this.init(); // 初始化
}
Router.prototype.push = (options) => { ... }
Router.prototype.replace = (options) => { ... }
Router.prototype.init = () => { ... }
我們看一下路由表 routerMap 的實(shí)現(xiàn),由于不考慮嵌套等其他情況,實(shí)現(xiàn)很簡(jiǎn)單,如下:
const createMap = (routes) => {
let resMap = Object.create(null);
routes.forEach(route => {
resMap[route['path']] = route['component'];
})
return resMap;
}
RouterHistory 的實(shí)現(xiàn)也很簡(jiǎn)單,根據(jù)前面分析,我們只需要一個(gè) current 屬性就可以,如下:
const RouterHistory = function (mode) {
this.current = null;
}
有了路由表和 history , router-view 的實(shí)現(xiàn)就很容易了,如下:
Vue.component('router-view', {
render(h) {
let routerMap = this._self.$router.routerMap;
return h(routerMap[this._self.$route.current])
}
})
這里的 this 是一個(gè) renderProxy 實(shí)例,他有一個(gè)屬性 _self 可以拿到當(dāng)前的組件實(shí)例,進(jìn)而訪問到 routerMap ,可以看到路由實(shí)例 history 的 current 本質(zhì)上就是我們配置的路由表中的 path 。
接下來我們看一下 Router 要做哪些初始化工作。對(duì)于 hash 路由而言,url上 hash 值的改變不會(huì)引起頁面刷新,但是可以觸發(fā)一個(gè) hashchange 事件。由于路由 history.current 初始為 null ,因此匹配不到任何一個(gè)路由,所以會(huì)導(dǎo)致頁面刷新加載不出任何路由組件?;谶@兩點(diǎn),在 init 方法中,我們需要實(shí)現(xiàn)對(duì)頁面加載完成的監(jiān)聽,以及 hash 變化的監(jiān)聽。對(duì)于 history 路由,為了實(shí)現(xiàn)瀏覽器前進(jìn)后退時(shí)準(zhǔn)確渲染對(duì)應(yīng)組件,還要監(jiān)聽一個(gè) popstate 事件。代碼如下:
Router.prototype.init = function () {
if (this.mode === 'hash') {
fixHash()
window.addEventListener('hashchange', () => {
this.history.current = getHash();
})
window.addEventListener('load', () => {
this.history.current = getHash();
})
}
if (this.mode === 'history') {
removeHash(this);
window.addEventListener('load', () => {
this.history.current = location.pathname;
})
window.addEventListener('popstate', (e) => {
if (e.state) {
this.history.current = e.state.path;
}
})
}
}
當(dāng)啟用 hash 模式的時(shí)候,我們要檢測(cè)url上是否存在 hash 值,沒有的話強(qiáng)制賦值一個(gè)默認(rèn) path , hash 路由時(shí)會(huì)根據(jù) hash 值作為 key 來查找路由表。 fixHash 和 getHash 實(shí)現(xiàn)如下:
const fixHash = () => {
if (!location.hash) {
location.hash = '/';
}
}
const getHash = () => {
return location.hash.slice(1) || '/';
}
這樣在刷新頁面和 hash 改變的時(shí)候, current 可以得到賦值和更新,頁面能根據(jù) hash 值準(zhǔn)確渲染路由。 history 模式也是一樣的道理,只是它通過 location.pathname 作為 key 搜索路由組件,另外 history 模式需要去除url上可能存在的 hash , removeHash 實(shí)現(xiàn)如下:
const removeHash = (route) => {
let url = location.href.split('#')[1]
if (url) {
route.current = url;
history.replaceState({}, null, url)
}
}
我們可以看到當(dāng)瀏覽器后退的時(shí)候, history 模式會(huì)觸發(fā) popstate 事件,這個(gè)時(shí)候是通過 state 狀態(tài)去獲取 path 的,那么 state 狀態(tài)從哪里來呢,答案是從 window.history 對(duì)象的 pushState 和 replaceState 而來,這兩個(gè)方法正好可以用來實(shí)現(xiàn) router 的 push 方法和 replace 方法,我們看一下這里它們的實(shí)現(xiàn):
Router.prototype.push = (options) => {
this.history.current = options.path;
if (this.mode === 'history') {
history.pushState({
path: options.path
}, null, options.path);
} else if (this.mode === 'hash') {
location.hash = options.path;
}
this.route.params = {
...options.params
}
}
Router.prototype.replace = (options) => {
this.history.current = options.path;
if (this.mode === 'history') {
history.replaceState({
path: options.path
}, null, options.path);
} else if (this.mode === 'hash') {
location.replace(`#${options.path}`)
}
this.route.params = {
...options.params
}
}
pushState 和 replaceState 能夠?qū)崿F(xiàn)改變url的值但不引起頁面刷新,從而不會(huì)導(dǎo)致新請(qǐng)求發(fā)生, pushState 會(huì)生成一條歷史記錄而 replaceState 不會(huì),后者只是替換當(dāng)前url。在這兩個(gè)方法執(zhí)行的時(shí)候?qū)?path 存入 state ,這就使得 popstate 觸發(fā)的時(shí)候可以拿到路徑從而觸發(fā)組件渲染了。我們?cè)诮M件內(nèi)按照如下方式調(diào)用,會(huì)將 params 寫入 router 實(shí)例的 route 屬性中,從而在跳轉(zhuǎn)后的組件 B 內(nèi)通過 this.$route.params 可以訪問到傳參。
this.$router.push({
path: '/b',
params: {
id: 55
}
});
router-link實(shí)現(xiàn)
router-view 的實(shí)現(xiàn)很簡(jiǎn)單,前面已經(jīng)說過。最后,我們來看一下 router-link 的實(shí)現(xiàn),先放上代碼:
Vue.component('router-link', {
props: {
to: String,
tag: String,
},
render(h) {
let mode = this._self.$router.mode;
let tag = this.tag || 'a';
let routerHistory = this._self.$router.history;
return h(tag, {
attrs: tag === 'a' ? {
href: mode === 'hash' ? '#' + this.to : this.to,
} : {},
on: {
click: (e) => {
if (this.to === routerHistory.current) {
e.preventDefault();
return;
}
routerHistory.current = this.to;
switch (mode) {
case 'hash':
if (tag === 'a') return;
location.hash = this.to;
break;
case 'history':
history.pushState({
path: this.to
}, null, this.to);
break;
default:
}
e.preventDefault();
}
},
style: {
cursor: 'pointer'
}
}, this.$slots.default)
}
})
router-link 可以接受兩個(gè)屬性, to 表示要跳轉(zhuǎn)的路由路徑, tag 表示 router-link 要渲染的標(biāo)簽名,默認(rèn)為標(biāo)簽。如果是 a 標(biāo)簽,我們?yōu)槠涮砑右粋€(gè) href 屬性。我們給標(biāo)簽綁定 click 事件,如果檢測(cè)到本次跳轉(zhuǎn)為當(dāng)前路由的話什么都不做直接返回,并且阻止默認(rèn)行為,否者根據(jù) to 更換路由。 hash 模式下并且是 a 標(biāo)簽時(shí)候可以直接利用瀏覽器的默認(rèn)行為完成url上 hash 的替換,否者重新為 location.hash 賦值。 history 模式下則利用 pushState 去更新url。
以上實(shí)現(xiàn)就是一個(gè)簡(jiǎn)單的vue-router,完整代碼參見vue-router-simple 。
總結(jié)
以上所述是小編給大家介紹的簡(jiǎn)化版的vue-router實(shí)現(xiàn)思路詳解,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
解決antd 下拉框 input [defaultValue] 的值的問題
這篇文章主要介紹了解決antd 下拉框 input [defaultValue] 的值的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10
詳解利用eventemitter2實(shí)現(xiàn)Vue組件通信
這篇文章主要介紹了詳解利用eventemitter2實(shí)現(xiàn)Vue組件通信,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
基于Vue2實(shí)現(xiàn)數(shù)字縱向滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了如何基于Vue2實(shí)現(xiàn)數(shù)字縱向滾動(dòng)效果,從而達(dá)到顯示計(jì)時(shí)器滾動(dòng)效果,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03
vue3-treeselect數(shù)據(jù)綁定失敗的解決方案
這篇文章主要介紹了vue3-treeselect數(shù)據(jù)綁定失敗的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
在vue中v-bind使用三目運(yùn)算符綁定class的實(shí)例
今天小編就為大家分享一篇在vue中v-bind使用三目運(yùn)算符綁定class的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09
Vue 引入AMap高德地圖的實(shí)現(xiàn)代碼
這篇文章主要介紹了Vue 引入AMap高德地圖的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
vue 實(shí)現(xiàn)移動(dòng)端鍵盤搜索事件監(jiān)聽
今天小編就為大家分享一篇vue 實(shí)現(xiàn)移動(dòng)端鍵盤搜索事件監(jiān)聽,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-11-11

