本文旨在介紹vue-router
的實現思路,並動手實現一個簡化版的vue-router
。咱們先來看一下通常項目中對vue-router
最基本的一個使用,能夠看到,這裏定義了四個路由組件,咱們只要在根vue
實例中注入該router
對象就可使用了.javascript
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
提供兩個全局組件,router-view
和router-link
,前者是用於路由組件的佔位,後者用於點擊時跳轉到指定路由。此外組件內部能夠經過this.$router.push
,this.$rouer.replace
等api實現路由跳轉。本文將實現上述兩個全局組件以及push
和replace
兩個api,調用的時候支持params
傳參,而且支持hash
和history
兩種模式,忽略其他api、嵌套路由、異步路由、abstract
路由以及導航守衛等高級功能的實現,這樣有助於理解vue-router
的核心原理。本文的最終代碼不建議在生產環境使用,只作一個學習用途,下面咱們就來一步步實現它。vue
任何一個vue
插件都要實現一個install
方法,經過Vue.use
調用插件的時候就是在調用插件的install
方法,那麼路由的install
要作哪些事情呢?首先咱們知道 咱們會用new
關鍵字生成一個router
實例,就像前面的代碼實例同樣,而後將其掛載到根vue
實例上,那麼做爲一個全局路由,咱們固然須要在各個組件中均可以拿到這個router
實例。另外咱們使用了全局組件router-view
和router-link
,因爲install
會接收到Vue
構造函數做爲實參,方便咱們調用Vue.component
來註冊全局組件。所以,在install
中主要就作兩件事,給各個組件都掛載router
實例,以及實現router-view
和router-link
兩個全局組件。下面是代碼:java
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
對象,它有兩個屬性暴露出來供外界調用,一個是install
,一個是Router
構造函數,這樣能夠保證插件的正確安裝以及路由實例化。咱們先忽略Router
構造函數,來看install
,上面代碼中的this._Vue
是個開始沒有定義的屬性,他的目的是防止屢次安裝。咱們使用Vue.mixin
對每一個組件的beforeCreate
鉤子作全局混入,目的是讓每一個組件實例共享router
實例,即經過this.$router
拿到路由實例,經過this.$route
拿到路由狀態。須要重點關注的是這行代碼:git
Vue.util.defineReactive(this, '_routeHistory', this._router.history)
複製代碼
這行代碼利用vue
的響應式原理,對根vue
實例註冊了一個_routeHistory
屬性,指向路由實例的history
對象,這樣history
也變成了響應式的。所以一旦路由的history
發生變化,用到這個值的組件就會觸發render
函數從新渲染,這裏的組件就是router-view
。從這裏能夠窺察到vue-router
實現的一個基本思路。上述的代碼中對於兩個全局組件的render
函數的實現,由於會依賴於router
對象,咱們先放一放,稍後再來實現它們,下面咱們分析一下Router
構造函數。github
通過剛纔的分析,咱們知道router
實例須要有一個history
對象,須要一個保存當前路由狀態的對象route
,另外很顯然還須要接受路由配置表routes
,根據routes
須要一個路由映射表routerMap
來實現組件搜索,還須要一個變量mode
判斷是什麼模式下的路由,須要實現push
和replace
兩個api,代碼以下:vue-router
const Router = function (options) {
this.routes = options.routes; // 存放路由配置
this.mode = options.mode || 'hash';
this.route = Object.create(null), // 生成路由狀態
this.routerMap = createMap(this.routes) // 生成路由表
this.history = new RouterHistory(); // 實例化路由歷史對象
this.init(); // 初始化
}
Router.prototype.push = (options) => { ... }
Router.prototype.replace = (options) => { ... }
Router.prototype.init = () => { ... }
複製代碼
咱們看一下路由表routerMap
的實現,因爲不考慮嵌套等其餘狀況,實現很簡單,以下:api
const createMap = (routes) => {
let resMap = Object.create(null);
routes.forEach(route => {
resMap[route['path']] = route['component'];
})
return resMap;
}
複製代碼
RouterHistory
的實現也很簡單,根據前面分析,咱們只須要一個current
屬性就能夠,以下:瀏覽器
const RouterHistory = function (mode) {
this.current = null;
}
複製代碼
有了路由表和history
,router-view
的實現就很容易了,以下:異步
Vue.component('router-view', {
render(h) {
let routerMap = this._self.$router.routerMap;
return h(routerMap[this._self.$route.current])
}
})
複製代碼
這裏的this
是一個renderProxy
實例,他有一個屬性_self
能夠拿到當前的組件實例,進而訪問到routerMap
,能夠看到路由實例history
的current
本質上就是咱們配置的路由表中的path
。函數
接下來咱們看一下Router
要作哪些初始化工做。對於hash
路由而言,url上hash
值的改變不會引發頁面刷新,可是能夠觸發一個hashchange
事件。因爲路由history.current
初始爲null
,所以匹配不到任何一個路由,因此會致使頁面刷新加載不出任何路由組件。基於這兩點,在init
方法中,咱們須要實現對頁面加載完成的監聽,以及hash
變化的監聽。對於history
路由,爲了實現瀏覽器前進後退時準確渲染對應組件,還要監聽一個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;
}
})
}
}
複製代碼
當啓用hash
模式的時候,咱們要檢測url上是否存在hash
值,沒有的話強制賦值一個默認path
,hash
路由時會根據hash
值做爲key
來查找路由表。fixHash
和getHash
實現以下:
const fixHash = () => {
if (!location.hash) {
location.hash = '/';
}
}
const getHash = () => {
return location.hash.slice(1) || '/';
}
複製代碼
這樣在刷新頁面和hash
改變的時候,current
能夠獲得賦值和更新,頁面能根據hash
值準確渲染路由。history
模式也是同樣的道理,只是它經過location.pathname
做爲key
搜索路由組件,另外history
模式須要去除url上可能存在的hash
,removeHash
實現以下:
const removeHash = (route) => {
let url = location.href.split('#')[1]
if (url) {
route.current = url;
history.replaceState({}, null, url)
}
}
複製代碼
咱們能夠看到當瀏覽器後退的時候,history
模式會觸發popstate
事件,這個時候是經過state
狀態去獲取path
的,那麼state
狀態從哪裏來呢,答案是從window.history
對象的pushState
和replaceState
而來,這兩個方法正好能夠用來實現router
的push
方法和replace
方法,咱們看一下這裏它們的實現:
Router.prototype.push = function (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 = function (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
可以實現改變url的值但不引發頁面刷新,從而不會致使新請求發生,pushState
會生成一條歷史記錄而replaceState
不會,後者只是替換當前url。在這兩個方法執行的時候將path
存入state
,這就使得popstate
觸發的時候能夠拿到路徑從而觸發組件渲染了。咱們在組件內按照以下方式調用,會將params
寫入router
實例的route
屬性中,從而在跳轉後的組件B
內經過this.$route.params
能夠訪問到傳參。
this.$router.push({
path: '/b',
params: {
id: 55
}
});
複製代碼
router-view
的實現很簡單,前面已經說過。最後,咱們來看一下router-link
的實現,先放上代碼:
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
能夠接受兩個屬性,to
表示要跳轉的路由路徑,tag
表示router-link
要渲染的標籤名,默認a
爲標籤。若是是a
標籤,咱們爲其添加一個href
屬性。咱們給標籤綁定click
事件,若是檢測到本次跳轉爲當前路由的話什麼都不作直接返回,而且阻止默認行爲,不然根據to
更換路由。hash
模式下而且是a
標籤時候能夠直接利用瀏覽器的默認行爲完成url上hash
的替換,否者從新爲location.hash
賦值。history
模式下則利用pushState
去更新url。
以上實現就是一個簡單的vue-router,完整代碼參見vue-router-simple。