说明
以下内容均是依托 vue-router 2.0.0 版本展开分析。
此篇文章,是自己对 vue-router 实现分析的记录性文章,如有任何错误,欢迎指正,相互交流。
基础使用
import Vue from ‘vue’;
import VueRouter from ‘vue-router’;
Vue.use(VueRouter);
const Home = { template: ‘
const Foo = { template: ‘
const Bar = { template: ‘
const Child = { template: ‘
const router = new VueRouter({
mode: ‘history’,
// base: __dirname,
base: ‘/’, // 默认 ‘/’
routes: [
{ path: ‘/’, component: Home },
{ path: ‘/foo’, component: Foo },
{
path: ‘/bar’,
component: Bar,
children: [{ path: ‘child’, component: Child }]
}
]
});
const template = `
Basic
- /
- /foo
- /bar
- /bar
new Vue({
router,
template
}).$mount(’#app’);
复制代码根据上述基础使用,我们大概可以梳理一个基本流程出来:
注册插件:
将 $router 和 $route 注入所有启用路由的子组件。
安装 和 。
定义路由组件。
new VueRouter(options) 创建一个路由器, 传入相关配置。
创建并挂载根实例,确保注入路由器。路由组件将在 中呈现。
下面我们就根据上述流程步骤,一步一步解析,vue-router 代码实现。
注册插件(vue-router)
首先 Vue.use(VueRouter); 这段代码间接执行 VueRouter 暴露的 install 方法,下面来看看 install 具体实现:(由于有完整的注释,故略去文字叙述)
install
import View from ‘./components/view’;
import Link from ‘./components/link’;
/**
- 安装 Vue.js 插件 install 方法调用时,会将 Vue 作为参数传入。
- @export
- @param {*} Vue
- @returns
*/
export function install(Vue) {
// 防止插件被多次安装 - 当 install 方法被同一个插件多次调用,插件将只会被安装一次。
if (install.installed) return;
install.installed = true;
// 在 Vue 原型上添加 r o u t e r 属 性 ( V u e R o u t e r ) 并 代 理 到 t h i s . router 属性( VueRouter )并代理到 this. router属性(VueRouter)并代理到this.root._router
Object.defineProperty(Vue.prototype, 'KaTeX parse error: Expected '}', got 'EOF' at end of input: … return this.root._router;
}
});
// 在 Vue 原型上添加 r o u t e 属 性 ( 当 前 路 由 对 象 ) 并 代 理 到 t h i s . route 属性( 当前路由对象 )并代理到 this. route属性(当前路由对象)并代理到this.root._route
Object.defineProperty(Vue.prototype, 'KaTeX parse error: Expected '}', got 'EOF' at end of input: … return this.root._route;
}
});
// 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
Vue.mixin({
/**
* 混入 Vue 创建前钩子
* 1.取传入 Vue 构造函数的路由配置参数并调用 init 方法。
* 2.在 Vue 根实例添加 _router 属性( VueRouter 实例)
* 3.执行路由实例的 init 方法并传入 Vue 实例
* 4.把 (KaTeX parse error: Expected '}', got 'EOF' at end of input: … if (this.options.router) {
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, ‘_route’, this._router.history.current);
}
}
});
// 注册全局组件
Vue.component(‘router-view’, View);
Vue.component(‘router-link’, Link);
}
复制代码上述 就是 vue-router 暴露给 Vue 的注册方法。这里特别说明一下:defineReactive Vue 构建响应式的核心方法。在研究注册的两个全局组件: 和 之前,我们先讨论 VueRouter 构造函数,因为它们其中涉及到 VueRouter 的很多方法。
代码接着执行,接下来就是为 vue-router 装填配置并实例化。之后把实例化的结果传入 Vue 。
const router = new VueRouter({
// 选择路由模式
mode: ‘history’,
// 应用的基路径。默认值: “/” 例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 “/app/”.
base: ‘/’, // 默认 ‘/’
// 路由配置表
routes: [
{ path: ‘/’, component: Home },
{ path: ‘/foo’, component: Foo },
{
path: ‘/bar’,
component: Bar,
children: [{ path: ‘child’, component: Child }]
}
]
});
new Vue({
router
}).$mount(’#app’);
复制代码接下来我们就来看看定义路由的 VueRouter 构造函数。
定义路由:VueRouter 构造函数
/* @flow */
import { install } from ‘./install’;
import { createMatcher } from ‘./create-matcher’;
import { HashHistory } from ‘./history/hash’;
import { HTML5History } from ‘./history/html5’;
import { AbstractHistory } from ‘./history/abstract’;
import { inBrowser, supportsHistory } from ‘./util/dom’;
import { assert } from ‘./util/warn’;
export default class VueRouter {
static install: () => void;
app: any; // Vue 实例
options: RouterOptions; // 路由配置
mode: string; // 路由模式,默认 hash
history: HashHistory | HTML5History | AbstractHistory;
match: Matcher; // 一个数组,包含当前路由的所有嵌套路径片段的路由记录。?
fallback: boolean; // 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。
beforeHooks: Array<?NavigationGuard>; // 前置钩子集合
afterHooks: Array<?(to: Route, from: Route) => any>; // 后置钩子集合
constructor(options: RouterOptions = {}) {
this.app = null;
this.options = options;
this.beforeHooks = [];
this.afterHooks = [];
this.match = createMatcher(options.routes || []); // 匹配器
/******* 确定路由模式 - 默认为 hash *******/
let mode = options.mode || 'hash';
// 如果传入的模式为 ·history· 在浏览器环境下不支持 history 模式,则强制回退到 hash 模式
this.fallback = mode === 'history' && !supportsHistory;
if (this.fallback) {
mode = 'hash';
}
// 在非浏览器环境下,采用 abstract 模式
if (!inBrowser) {
mode = 'abstract';
}
this.mode = mode;
}
/**
- 当前路由
- @readonly
- @type {?Route}
- @memberof VueRouter
*/
get currentRoute(): ?Route {
return this.history && this.history.current;
}
/**
- 初始化
- @param {Any} app Vue component instance
*/
init(app: any) {
// 断言有没有安装插件,如果没有抛出错误提示
assert(
install.installed,
没有安装。在创建根实例之前,请确保调用 Vue.use(VueRouter)。
);
this.app = app;
const { mode, options, fallback } = this;
// 根据不同模式实例化不同基类,无效模式下抛出错误
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base);
break;
case 'hash':
this.history = new HashHistory(this, options.base, fallback);
break;
case 'abstract':
this.history = new AbstractHistory(this);
break;
default:
assert(false, `invalid mode: ${mode}`);
}
// 调用 history 属性下 listen 方法?
this.history.listen(route => {
this.app._route = route;
});
}
/**
- Router 实例方法 beforeEach 全局前置的导航守卫。
- 当一个导航触发时,全局前置守卫按照创建顺序调用。
- 守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
- @param {Function} fn (to, from, next) => {}
- @memberof VueRouter
*/
beforeEach(fn: Function) {
this.beforeHooks.push(fn);
}
/**
- Router 实例方法 afterEach 全局后置钩子
- @param {Function} fn (to, from) => {}
- @memberof VueRouter
*/
afterEach(fn: Function) {
this.afterHooks.push(fn);
}
/**
- 编程式导航 push 导航到对应的 location
- 这个方法会向 history 栈添加一个新的记录,
- 所以,当用户点击浏览器后退按钮时,则回到之前的 location。
- @param {RawLocation} location
- @memberof VueRouter
*/
push(location: RawLocation) {
this.history.push(location);
}
/**
- 编程式导航 replace 导航到对应的 location
- 它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
- @param {RawLocation} location
- @memberof VueRouter
*/
replace(location: RawLocation) {
this.history.replace(location);
}
/**
- 在 history 记录中向前或者后退多少步,类似 window.history.go(n)。
- @param {number} n
- @memberof VueRouter
*/
go(n: number) {
this.history.go(n);
}
/**
- 后退
- @memberof VueRouter
*/
back() {
this.go(-1);
}
/**
- 前进
- @memberof VueRouter
*/
forward() {
this.go(1);
}
/**
- 获取匹配到的组件列表
- @returns {Array}
- @memberof VueRouter
*/
getMatchedComponents(): Array {
if (!this.currentRoute) {
return [];
}
return [].concat.apply(
[],
this.currentRoute.matched.map(m => {
return Object.keys(m.components).map(key => {
return m.components[key];
});
})
);
}
}
// 添加 install 方法
VueRouter.install = install;
// 在浏览器环境下且 Vue 构造函数存在的情况下调用 use 方法注册插件(插件预装)
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter);
}
复制代码由上述代码实现分析,首先声明了一些属性、方法、和常用的API;接着添加 install 方法和执行 Vue.use 方法注册插件。
其中对相关代码做了详细的注释,无需再此重复论述,但其中有一些方法将在后面涉及时,做深入分析。继续分析前,这里需要先对这段代码进行解释:
this.match = createMatcher(options.routes || []); // 创建匹配器
复制代码/**
- 创建匹配器
- @export
- @param {Array} routes
- @returns {Matcher}
*/
export function createMatcher(routes: Array): Matcher {
const { pathMap, nameMap } = createRouteMap(routes);
function match(
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
…
return _createRoute(null, location);
}
function redirect(record: RouteRecord, location: Location): Route {
…
}
function alias(
record: RouteRecord,
location: Location,
matchAs: string
): Route {
…
}
function _createRoute(
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
…
}
return match;
}
复制代码上述代码首先根据传入的路由配置表,创建新的映射表并从中解构出路径映射表、名称映射表,之后返回内建函数 match。这里暂时先不对其内部实现做详细介绍,之后再被调用处详细论述。
初始化
我们知道在 VueRouter -