Vue源码解析:vue-router路由(入门版)

按:

第一次尝试写本文,还是去年年底(2019-12-11 21:37:07),后来入职新公司忙的要死,也就鸽了。今天重新梳理,争取弄得清楚一些:带你读 Vue-Router源码。

官方git仓库 Vue-router

VueRouter 出现的意义

不多说,spa切换不刷新,Vue官方提供了Router。目前基本使用两种路由模式: hash+ history

使用流程

Vue-Router 的核心使用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1 导入 main.js
import VueRouter from 'vue-router'
Vue.use(VueRouter) // 说明是Vue插件,自然要想到install方法

// 2 创建router实例 规则。router.js
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
] //export

// 3 添加到根实例 main.js
import routes from 'router.js'
const router = new VueRouter({
routes // (缩写)相当于 routes: routes
})

new Vue({
router
}).$mount('#app')

// 4 两个全局组件 App.vue
// <router-view /> // 路由视图
// <router-link to="/">去首页</router-link> // 路由导航

源码解析

如果要实现这种使用方式,需要完成下面的需求:

  • 是一个 Vue插件 – Vue插件编写, install
  • 书写规则,解析规则 router.js
  • 随 根实例一起创建
  • 注册全局组件
  • 监听路由变化,展示内容

实现 vue-router 插件

首先我们需要实现一个 类,挂一个 Install 方法,做成vue插件。

1
2
3
// step 1
class VueRouter{}
VueRouter.install=function(){}

困难点:router install时候,还没有 new Vue,也就是拿不到实例。该如何处理?

针对这个方法,官方妥善使用了 mixin混入,在 Vue的 beforeCreate声明周期中执行相关逻辑,也需要进一步判断是否是根实例。

官方是这么做的,点开查看官方router/install.js。当install方法执行的时候。忽略一些代码,能看到 mixin.

1
2
3
4
5
6
7
8
9
10
// VueRouter.instrall 里
Vue.minxin({
// 推迟挂载。把 new Vue 传入的router挂载
beforeCreate() {
// 只有根实例才有router选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})

接下来就可以准备 注册全局组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// step2

let Vue; // 这里定义 Vue 方便后续使用

class vueRouter{} // 这里先不关注

// 插件需要实现install方法
// 接收一个参数,Vue构造函数,主要用于数据响应式
VueRouter.install = function (_Vue) {
// 保存Vue构造函数在VueRouter中使用
Vue = _Vue

// 任务1:使用混入来做router挂载这件事情
Vue.mixin({
beforeCreate() {
// 只有根实例才有router选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})

// 任务2:实现两个全局组件
// 这部分代码放到下面
Vue.component('router-link',{})
Vue.component('router-view',{})
}

注册全局组件

文档中也提到了,插件中是如何注册组件的

  • Import 组件导入
  • Vue.component() 注册全局组件

先只考虑 hash模式。<route-link to="/">aaa</a> 这个组件有 props slot,注意插件不适用template,vue版本不一定存在编译器,直接使用render函数。因为 href 是 attribute,所以这样编写如下组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// router-link: 生成一个a标签,在url后面添加#
// <a href="#/about">aaaa</a>
Vue.component('router-link', {
props: {
to: {
type: String,
required: true
}
},
render(h) {
// h(tag, props, children)
return h('a',
{ attrs: { href: '#' + this.to } },
this.$slots.default
)
// 使用jsx
// return <a href={'#'+this.to}>{this.$slots.default}</a>
}
})

这样就实现了渲染为html:<a href='#/'>aaa</a>

route-view

暂时先不实现。

1
2
3
4
5
Vue.component('router-view', {
render(h) {
return h(null)
}
})

完善逻辑

接下来实现点击a连接,变化 hash,也就是监听hashchange事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// step3
let Vue;

class VueRouter {
constructor(options) {
this.$options = options; // 挂载options

// 缓存path和route映射关系,方便解析 router配置文件
this.routeMap = {}
// 遍历得到每一项配置项,暂时不考虑子类嵌套。
this.$options.routes.forEach(route => {
this.routeMap[route.path] = route
})

// 定义数据响应式
// 定义一个响应式的current,则如果他变了,那么使用它的组件会rerender
Vue.util.defineReactive(this, 'current', '')

// 请确保onHashChange中this指向当前实例
// 保证 load 和 hashchange 都是对的。
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
}

onHashChange() {
// console.log(window.location.hash);
this.current = window.location.hash.slice(1) || '/'
}
}
  • 通过监听hashchagne,调用自定义方法,记得绑定this放走丢。
  • 把 current 变为响应式的
  • 现在 current 始终是最新的路由导航。

有了 current,再去匹配路由规则。比如当前 hash#/about,去匹配 /about对应的页面或组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
// step4 实现 view
Vue.component('router-view', {
render(h) {
let component = null
// 先拿到 hash,然后去取规则里的组件
const { routeMap, current } = this.$router
if (routeMap[current]) {
component = routeMap[current].component
}
// 渲染
return h(component)
}
})

最终,页面导出类

1
export default VueRouter

这样,Vue.use(plugin) 时候执行install,根实例挂载$router

配置路由规则,去new Router 得到实例。


先实现到这里。

请我喝杯咖啡吧~