vue-router源码实现

从零开始,实现vue-router

本篇目标

  • 实现VueRouter类和install方法,使之作为一个插件存在
  • 实现两个全局组件:router-view用于显示匹配组件内容,router-link用于跳转
  • 监控url变化:监听hashchangepopstate事件
  • 响应最新url:创建一个响应式的属性current,当它改变时获取对应的组件并显示

实现一个插件:创建VueRouter类和install方法

src目录下创jrouter文件夹用于存放我们手写的路由的相关代码,在jrouter下新建index.jsjvue-router.js

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
//jvue-router.js

let Vue;//引用构造函数,在JVueRouter里面使用
//1、实现一个插件:挂载$router
class JVueRouter{
constructor(options){
//获取用户配置并保存
this.$options=options
}
}
//插件:实现install方法,注册$router
JVueRouter.install = function(_Vue){
//保存构造函数,在JVueRouter里面使用
Vue=_Vue;
// 挂载$router
Vue.mixin({//混入
beforeCreate(){
//确保根实例的时候才执行,只有根组件拥有router选项
if(this.$options.router){
Vue.prototype.$router = this.$options.router
}
}
})
}
export default JVueRouter;

为什么要采用混入的方式:主要原因是use代码在前,Router实例创建在后,而install逻辑又需要用到该实例

修改jrouterindex.js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Vue from 'vue'
import VueRouter from './jvue-router'//引入我们写好的插件
import Home from '../views/Home.vue'

// 1.应用插件
Vue.use(VueRouter)

const routes = [
......
]

// 2.创建实例
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})

export default router

最后我们只需要修改main.js中对路由的引用就可以了

1
2
3
4
5
6
7
8
...
import router from './jrouter'
...
new Vue({
router,//挂载router实例
store,
render: h => h(App)
}).$mount('#app')

实现router-link和router-view

修改jrouterindex.js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
KVueRouter.install = function (_Vue) {
...
Vue.component('router-link', {
props: {
to: {
type: String,
required: true
},
},
render(h) {
// <a href="#/about">abc</a>
// <router-link to="/about">xxx</router-link>
// h(tag, data, children)
return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
// return <a href={'#' + this.to}>{this.$slots.default}</a>
}
})
Vue.component('router-view', {
render(h){
return h('div','router-view')
}
})
}

监控url的变化

定义响应式的current,监听hashchange事件

1
2
3
4
5
6
7
8
9
10
11
12
13
class JVueRouter{
constructor(options){
this.$options=options
//需要创建响应式的current的属性,可以利用vue监听current的变化
Vue.util.defineReactive(this,'current','/')
//监控url变化
window.addEventListener('hashchange', this.onHashChange.bind(this))//避免onHashChange中this变成window
window.addEventListener('load', this.onHashChange.bind(this))//页面加载 避免onHashChange中this变成window
}
onHashChange(){
this.current = window.location.hash.slice(1)
}
}

动态获取对应的组件

1
2
3
4
5
6
7
8
9
10
11
12
Vue.component('router-view', {
render(h){
//获取path对应的component
let component=null
this.$router.$options.routes.forEach(route=>{
if(route.path===this.$router.current){
component=route.component
}
})
return h(component)
}
})

提前处理路由表

提前处理路由表可以避免每次都循环

1
2
3
4
5
6
7
8
9
10
11
class JVueRouter{
constructor(options){
......
//创建一个路由映射表
this.routeMap={}
options.routes.forEach(route=>{
this.routeMap[route.path]=route
})
......
}
}

修改router-view根据path获取component的方法

1
2
3
4
5
6
7
Vue.component('router-view', {
render(h){
const {routeMap,current}=this.$router
const component=routeMap[current].component || null
return h(component)
}
})

代码结构优化

我们在jrouter文件夹下新建jrouter-link.jsjrouter-view.js,调整jvue-router.js中代码

1
2
3
4
5
6
7
8
9
10
11
12
13
//首先进行引入
import Link from './jrouter-link'
import View from './jrouter-view'

......

//
JVueRouter.install = function (_Vue) {
......
Vue.component('router-link', Link)
Vue.component('router-view', View)
......
}

然后我们将原先写在jvue-router.js中组件部分的代码分别写到对应的js文件中

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
//jrouter-link.js
export default {
props: {
to: {
type: String,
required: true
},
},
render(h) {
// <a href="#/about">abc</a>
// <router-link to="/about">xxx</router-link>
// h(tag, data, children)
return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
// return <a href={'#' + this.to}>{this.$slots.default}</a>
}
}
// jrouter-view.js
export default {
render(h) {
//获取path对应的component
const {routeMap, current} = this.$router;
const component = routeMap[current].component || null;
return h(component)
}
}

如果我们不用Vue.util.defineReactive(this,'current','/')来实现监听,其实也可以用这种方式

1
2
3
4
5
6
7
8
9
this.app = new Vue({
data(){
return {
current:'/'
}
}
})
//获取current的方式就变为
this.app.current

嵌套路由

当用户的路由为类似如下的嵌套路由时,我们应该如何兼容呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const routes = [
......
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue'),
children:[
{
path:'/about/info',
component:() => import('../views/AboutInfo.vue'),
}
......
]
}
......
]

打开jrouter-view.js文件,并修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
render(h) {
//标记当前router-view深度
this.$vnode.data.routerView = true;
let depth = 0
let parent = this.$parent
while(parent){
const vnodeData = parent.$vnode&&parent.$vnode.data
if(vnodeData&&vnodeData.routerView){
//说明当前的parent是一个router-view
depth++
}
parent = parent.$parent
}
//获取path对应的component
let component = null;
const route = this.$router.matched[depth];
if(route){
component = route.component
}

return h(component)
}
}

修改jvue-router.js

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
30
31
32
33
34
35
36
37
class JVueRouter{
constructor(options){
this.$options=options
//需要创建响应式的current的属性,可以利用vue监听current的变化
// Vue.util.defineReactive(this,'current','/')通过matched数组获取component,不需要current响应式了
this.current = window.location.hash.slice(1)||'/'
Vue.util.defineReactive(this,'matched',[])
//match方法可以递归遍历路由表,获得匹配关系的数组
this.match()
//监控url变化
window.addEventListener('hashchange', this.onHashChange.bind(this))//避免onHashChange中this变成window
window.addEventListener('load', this.onHashChange.bind(this))//页面加载 避免onHashChange中this变成window
}
onHashChange(){
this.current = window.location.hash.slice(1)
this.matched = []
this.match()

}
match(routes){
routes = routes||this.$options.routes
//递归遍历路由表
for(const route of routes){
if(route.path === '/'&&this.current==='/'){
this.matched.push(route)
return
}
if(route.path!=='/'&&this.current.indexOf(route.path)!=-1){
this.matched.push(route)
if(route.children){//如果有嵌套
this.match(route.children)
}
return
}
}
}
}
文章作者: Joker
文章链接: https://qytayh.github.io/2020/07/vue-router%E6%BA%90%E7%A0%81%E5%AE%9E%E7%8E%B0/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Joker's Blog