本文旨在介绍 vue-router 的实现思路,并动手实现一个简化版的 vue-router 。我们先来看一下一般项目中对 vue-router 最基本的一个使用,可以看到,这里定义了四个路由组件,我们只要在根 vue 实例中注入该 router 对象就可以使用了.
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: 'https://www.jb51.net/', 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 的核心原理。本文的最终代码不建议在生产环境使用,只做一个学习用途,下面我们就来一步步实现它。
install实现
任何一个 vue 插件都要实现一个 install 方法,通过 Vue.use 调用插件的时候就是在调用插件的 install 方法,那么路由的 install 要做哪些事情呢?首先我们知道 我们会用 new 关键字生成一个 router 实例,就像前面的代码实例一样,然后将其挂载到根 vue 实例上,那么作为一个全局路由,我们当然需要在各个组件中都可以拿到这个 router 实例。另外我们使用了全局组件 router-view 和 router-link ,由于 install 会接收到 Vue 构造函数作为实参,方便我们调用 Vue.component 来注册全局组件。因此,在 install 中主要就做两件事,给各个组件都挂载 router 实例,以及实现 router-view 和 router-link 两个全局组件。下面是代码:
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 拿到路由状态。需要重点关注的是这行代码:
Vue.util.defineReactive(this, '_routeHistory', this._router.history)
这行代码利用 vue 的响应式原理,对根 vue 实例注册了一个 _routeHistory 属性,指向路由实例的 history 对象,这样 history 也变成了响应式的。因此一旦路由的 history 发生变化,用到这个值的组件就会触发 render 函数重新渲染,这里的组件就是 router-view 。从这里可以窥察到 vue-router 实现的一个基本思路。上述的代码中对于两个全局组件的 render 函数的实现,因为会依赖于 router 对象,我们先放一放,稍后再来实现它们,下面我们分析一下 Router 构造函数。
Router构造函数
经过刚才的分析,我们知道 router 实例需要有一个 history 对象,需要一个保存当前路由状态的对象 route ,另外很显然还需要接受路由配置表 routes ,根据 routes 需要一个路由映射表 routerMap 来实现组件搜索,还需要一个变量 mode 判断是什么模式下的路由,需要实现 push 和 replace 两个api,代码如下:
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 的实现,由于不考虑嵌套等其他情况,实现很简单,如下:
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]) } })