vue-router是開發vue項目中必不可少的依賴,爲了能更好的理解其實現原理,而源碼閱讀起來又過於複雜和枯燥,筆者這裏實現一個簡易版本的vue-rouer,幫助本身來更好的理解源碼。javascript
其功能以下:vue
基礎demo單獨新建了一個分支 ,方便學習和查看java
在實現本身的router以前,咱們先使用官方的包來書寫一個基礎demo,以後咱們會以這個demo爲需求,一步步實現咱們本身的vue-router。vue-router
demo的代碼邏輯以下:數組
下面開始使用咱們本身寫的vue-router來實現上邊展現的功能。app
vue-router使用方式以下:ide
import Vue from 'vue';
import VueRouter from '../my-router';
import Home from '../views/Home.vue';
import About from '@/views/About';
Vue.use(VueRouter);
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
const router = new VueRouter({
routes
});
export default router;
以後會在main.js中將router做爲配置項傳入:函數
import Vue from 'vue';
import App from './App.vue';
import router from './router';
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App)
}).$mount('#app');
由用法咱們能夠知道vue-router是一個類,而且它有一個靜態方法install:學習
import install from '@/my-router/install';
class VueRouter {
constructor (options) {
}
init(app) {
}
}
VueRouter.install = install;
export default VueRouter;
install方法中會爲全部組件添加$router以及$route屬性,而且會全局註冊router-view以及router-link組件。咱們在install.js中來單獨書寫install的邏輯:this
import RouterView from '@/my-router/components/view';
import RouterLink from '@/my-router/components/link';
const install = (Vue) => {
Vue.mixin({
beforeCreate () {
const { router } = this.$options;
// mount $router property for all components
if (router) {
// 用_rootRouter來存儲根實例
this._rootRouter = this;
// 爲根實例添加$router屬性
this.$router = router;
// 在實例上定義響應式屬性,可是這個API可能會發生變化,因此Vue並無在文檔中提供
Vue.util.defineReactive(this, '$route', this.$router.history.current);
// 初始化路由
router.init(this);
} else {
this._rootRouter = this.$parent && this.$parent._rootRouter;
if (this._rootRouter) {
this.$router = this._rootRouter.$router;
// 在爲$route賦值時,會從新指向新的地址,致使子組件的$route再也不更新
// this.$route = this._rootRouter.$route;
Object.defineProperty(this, '$route', {
get () {
return this._rootRouter.$route;
}
});
}
}
}
});
Vue.component('RouterView', RouterView);
Vue.component('RouterLink', RouterLink);
};
在install方法中作了以下幾件事:
vue-router在hash模式下能夠利用hash值的切換來渲染對應的組件,原理實際上是利用頁面地址hash值發生改變不會刷新頁面,而且會觸發hashchange事件。
在history目錄下,新建hash.js來存放hash值變化,組件進行切換的邏輯:
import { getHash } from '@/my-router/util';
import { createRoute } from '@/my-router/create-matcher';
const ensureSlash = () => {
if (!location.hash) {
location.hash = '/';
}
};
class HashHistory {
constructor (router) {
this.router = router;
// 綁定this指向
this.onHashchange = this.onHashchange.bind(this);
// 默認hash值爲'/'
ensureSlash();
}
listenEvent () {
window.addEventListener('hashchange', this.onHashchange);
}
onHashchange () {
}
}
export default HashHistory;
在VueRouter實例執行init方法時,監聽hashchange事件:
class VueRouter {
constructor (options) {
this.history = new HashHistory(this);
}
init (app) {
// 第一次渲染時也須要手動執行一次onHashchange方法
this.history.onHashchange();
this.history.listenEvent();
}
}
在onHashchange方法中,須要根據當前頁面地址的hash值來找到其對應的路由信息:
class HashHistory {
// some code ...
onHashchange () {
const path = getHash();
const route = this.router.match(path);
}
}
爲了找到當前的路由信息,HashHistory中調用了VueRouter的match方法。match方法放到了create-matcher.js中來實現:
// create-matcher.js
export const createRoute = (route, path) => {
const matched = [];
// 遞歸route的全部父路由,生成matched數組,並和path一塊兒返回,做爲當前的路由信息
while (route) {
matched.unshift(route);
route = route.parent;
}
return {
path,
matched
};
};
function createMatcher (routes) {
const pathMap = createRouteMap(routes);
// need to get all matched route, then find current routes by matched and router-view
const match = (path) => {
const route = pathMap[path];
return createRoute(route, path);
};
return {
match
};
}
// create-route-map.js
function addRouteRecord (routes, pathMap, parent) {
routes.forEach(route => {
const { path, children, ...rest } = route;
// 拼接子路由path
const normalizedPath = parent ? parent.path + '/' + path : path;
// 將parent也放入到屬性中,方便以後生成matched數組
pathMap[normalizedPath] = { ...rest, path: normalizedPath, parent };
if (children) {
// 繼續遍歷子路由
addRouteRecord(children, pathMap, route);
}
});
}
const createRouteMap = (routes, pathMap = {}) => {
addRouteRecord(routes, pathMap);
return pathMap;
};
createMatcher會經過createRouteMap生成hash值和路由的映射關係:
const pathMap = {
'/about': {
path: '/about',
name: 'About',
children: [
// ...
],
parent: undefined
}
// ...
}
這樣咱們能夠很方便的經過hash值來獲取路由信息。
最終咱們調用match方法獲得的路由信息結構以下:
{
"path": "/about/a",
"matched": [
{
"path": "/about",
"name": "About",
"component": About,
"children": [
{
"path": "a",
"name": "AboutA",
"component": A
},
{
"path": "b",
"name": "AboutB",
"component": B
}
]
},
// ...
]
}
須要注意的是對象中的matched屬性,它裏面存放的是當前hash匹配的全部路由信息組成的數組。在實現嵌套路由時會用到matched數組,由於嵌套路由本質上是router-view組件的嵌套,因此能夠根據router-view在組件中的深度在matched中找到對應的匹配項,而後進行展現。
如今咱們回到hashHistory的onHashchange方法,它會調用VueRouter實例的match方法,代碼以下:
class VueRouter {
constructor (options) {
this.matcher = createMatcher(options.routes);
this.history = new HashHistory(this);
}
init (app) {
this.history.onHashchange();
this.history.listenEvent();
}
match (path) {
return this.matcher.match(path);
}
}
在hashHistory中將其賦值給實例中的current屬性:
class HashHistory {
constructor (router) {
// pass instance of VueRoute class, can call methods and properties of instance directly
this.router = router;
// 當前的路由信息,在current更新後,因爲其不具備響應性,因此儘管值更新了,可是不會觸發頁面渲染
// 須要將其定義爲響應式的數據
this.current = createRoute(null, '/');
this.onHashchange = this.onHashchange.bind(this);
ensureSlash();
}
listenEvent () {
window.addEventListener('hashchange', this.onHashchange);
}
onHashchange () {
const path = getHash();
const route = this.router.match(path);
this.current = route
}
}
爲了方便用戶訪問當前路由信息,而且讓其具備響應性,會經過Vue.util.defineReactive來爲vue的根實例提供響應性的$route屬性,並在每次頁面初始化以及路徑更新時更新$route:
class HashHistory {
constructor (router) {
// pass instance of VueRoute class, can call methods and properties of instance directly
this.router = router;
// 當前的路由信息,在current更新後,因爲其不具備響應性,因此儘管值更新了,可是不會觸發頁面渲染
// 須要將其定義爲響應式的數據
this.current = createRoute(null, '/');
this.onHashchange = this.onHashchange.bind(this);
}
// some code ...
onHashchange () {
const path = getHash();
const route = this.router.match(path);
// 將當前路由賦值給根實例,app會在router.init方法中進行初始化
this.router.app.$route = this.current = route
}
}
在install方法中爲根實例定義$route屬性,並將全部子組件實例的$route屬性賦值爲根實例的$route屬性:
const install = (Vue) => {
Vue.mixin({
beforeCreate () {
const { router } = this.$options;
// mount $router property for all components
if (router) {
this._rootRouter = this;
this.$router = router;
// 定義響應性$route屬性
Vue.util.defineReactive(this, '$route', this.$router.history.current);
router.init(this);
} else {
this._rootRouter = this.$parent && this.$parent._rootRouter;
if (this._rootRouter) {
this.$router = this._rootRouter.$router;
// 這樣直接賦值會致使引用刷新而沒法改變$route
// this.$route = this._rootRouter.$route;
// 獲取根組件實例的$route屬性,其具備響應性
Object.defineProperty(this, '$route', {
get () {
return this._rootRouter.$route;
}
});
}
}
}
});
};
到這裏,咱們已經能夠在地址切換時獲取到對應的路由信息,接下來咱們實現router-view來展現對應的組件。
router-view組件須要展現當前匹配的hash所對應的component,這裏採用函數式組件來實現:
export default {
name: 'RouterView',
render (h) {
// 記錄組件的深度,默認爲0
let depth = 0;
const route = this.$parent.$route;
let parent = this.$parent;
// 遞歸查找父組件,若是父組件是RouterView組件,深度++
// 最終的深度即爲路由的嵌套層數
while (parent) {
if (parent.$options.name === 'RouterView') {
depth++;
}
parent = parent.$parent;
}
// 根據深度從matched中找到對應的記錄
const record = route.matched[depth];
if (record) { // /about會匹配About頁面,會渲染About中的router-view,此時record爲undefined
return h(record.component);
} else {
return h();
}
}
};
這裏必需要爲組件指定name屬性,方便進行遞歸查找,進行深度標識。
到這裏,咱們爲路由信息中添加的matched數組,終於派上了用場,其與router-view組件的深度depth進行巧妙結合,最終展現出了全部匹配到的路由組件。
router-link主要支持如下幾個功能:
在頁面中vue-router也支持經過router-link來進行路由跳轉,其實現比較簡單:
export default {
props: {
to: {
type: String,
},
tag: {
type: String,
default: () => 'a'
}
},
computed: {
active () {
return this.$route.matched.map(item => item.path).includes(this.to);
}
},
methods: {
onClick () {
this.$router.push(this.to);
}
},
render () {
return (
<this.tag
onClick={this.onClick}
href="javascript:;"
class={{ 'router-link-active': this.active }}
>
{this.$slots.default}
</this.tag>
);
}
};
router-link能夠接受tag來渲染不一樣的標籤,默認會渲染a標籤。當點擊router-link的時候,其內部會調用VueRouter實例的push方法:
class VueRouter {
// some code ...
push (path) {
location.hash = path;
}
// some code ...
}
// some code ...
push方法會切換頁面的hash,當hash發生變化後,就會觸發hashchange事件,執行事件處理函數onHashchange,從新經過path匹配對應的路由信息。
在代碼中咱們經過計算屬性active來計算當前的router-link是否激活,須要注意的是當子路由激活時父路由也會激活。若是matched的path屬性組成的數組中包含this.to,說明該router-link被激活。用戶能夠經過router-link-active類來設置激活樣式。
在平常開發中,常常會用到beforeEach全局前置守衛,讓咱們在進入頁面以前執行一些邏輯:
// some code ....
const router = new VueRouter({
routes
});
// 在每次進入頁面以前,都會先執行全部的beforeEach中的回調函數
router.beforeEach((to, from, next) => {
console.log(1);
setTimeout(() => {
// 調用下一個回調函數
next();
}, 1000);
});
router.beforeEach((to, from, next) => {
console.log(2);
next();
});
在每次進入頁面以前,vue-router會先執行beforeEach中的回調函數,而且只有當用戶調用回調函數中傳入的next函數後,纔會執行以後的beforeEach中的回調。
當全部beforeEach中的回調執行完畢後,調用next函數會更新路由信息,而後經過router-view來顯示對應的組件。其實現以下:
// my-router/index.js
class VueRouter {
constructor (options) {
// some code ...
this.beforeEachs = [];
}
// cache in global, execute before get matched route record
beforeEach (fn) {
this.beforeEachs.push(fn);
}
}
// my-router/history/hash.js
class HashHistory {
// some code ...
onHashchange () {
const path = getHash();
const route = this.router.match(path);
// 當用戶手動調用next時,會執行下一個beforeEach鉤子,在全部的鉤子執行完畢後,會更新當前路由信息
const next = (index) => {
const { beforeEachs } = this.router;
if (index === beforeEachs.length) {
//update route after executed all beforeEach hook
this.router.app.$route = this.current = route;
return;
}
const hook = beforeEachs[index];
hook(route, this.current, () => next(index + 1));
};
next(0);
}
}
上述代碼的執行流程以下:
慶祝一下????,這裏咱們已經完成了文章開頭定下的全部目標!
但願在讀完文章以後,能讓讀者對vue-router的底層實現有更深刻的瞭解,明白平常使用的API是怎麼來的,從而更加熟練的使用vue-router。