简单实现VUE-Router
OUDUIDUI

github

vue-router

Vue-routerVue.js官方的路由管理器。

它和Vue.js的核心深度集成,让构建单页面应用变得易如反掌。

安装

script
1
vue add router

核心步骤

  • 步骤一:使用vue-router插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //router.js
    import Router from 'vue-router';

    /*
    * VueRouter是一个插件
    * 1)实现并声明两个组件router-view router-link
    * 2)install: this.$router.push()
    * */
    Vue.use(Router); // 引入插件
  • 步骤二:创建Router实例

    1
    2
    // router.js
    export default new Router({...}) // 导出Router实例
  • 步骤三:在根组件添加该实例

    1
    2
    3
    4
    5
    // main.js
    import router from './router';
    new Vue({
    router // 添加到配置项
    }).$mount("#app")
  • 步骤四:添加路由视图

    1
    2
    <!--  App.vue  -->
    <router-view></router-view>
  • 步骤五:导航

    1
    2
    <router-link to="/">Home</router-link>
    <router-link to="/about">About</router-link>
    1
    2
    this.$router.push('/');
    this.$router.push('/about')

vue-router简单实现

需求分析

  • 单页面应用程序中,url发生变化时候,不能刷新,显示对应视图
    • hash:#/about
    • History api:/about
  • 根据url显示对应的内容
    • router-view
    • 数据响应式:current变量持有url地址,一旦变化,动态执行render

任务

  • 实现一个插件
    • 实现VueRouter
      • 处理路由选项
      • 监控url变化
      • 响应变化
    • 实现install方法
      • $router注册
      • 两个全局组件

实现

创建新的插件

Vue2.x项目中的src路径下,复制一份router文件,重命名为ou-router

然后在ou-router路径下新建一个ou-vue-router.js文件,并将index.js文件中的VueRouter引入改为ou-vue-router.js

1
import VueRouter from './ou-vue-router'

同时将main.js中的router引入也修改一下。

1
import router from './ou-router'

创建Vue插件

关于Vue插件的创建:

  • 可以使用function实现,也可以使用objectclass实现;
  • 要求必须有一个install方法,将来会被Vue.use()使用
1
2
3
4
5
6
7
8
9
10
11
12
13
let Vue;   // 保存Vue的构造函数,插件中需要用到

class VueRouter {}

/*
* 插件:实现install方法,注册$router
* 参数1是Vue.use()一定会传入
* */
VueRouter.install = function (_Vue) {
Vue = _Vue; // 引用构造函数,VueRouter中要使用
}

export default VueRouter;

挂载$router

当我们发现vue-router引入vue的时候,第一次是在router/index.js中使用了Vue.use(Router),在这个时候也就会调用了vue-routerinstall方法;而第二次则是在main.js中,创建根组件实例的时候引入router,即new Vue({router}).$mount("#app")

也就是说,当调用vue-routerinstall方法的时候,项目还没有创建Vue的根组件实例。因此我们需要在vue-routerinstall方法使用全局混入,延迟到router创建完毕才执行挂载$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
28
let Vue;   // 保存Vue的构造函数,插件中需要用到

class VueRouter {}

/*
* 插件:实现install方法,注册$router
* 参数1是Vue.use()一定会传入
* */
VueRouter.install = function (_Vue) {
Vue = _Vue; // 引用构造函数,VueRouter中要使用

/* 挂载$router */
/*
* 全局混入
* 全局混入的目的是为了延迟下面逻辑到router创建完毕并且附加到选项上时才执行
* */
Vue.mixin({
beforeCreate() { // 此钩子在每个组件创建实例时都会调用
/* this.$options即创建Vue实例的第一个参数 */
if(this.$options.router){ // 只在根组件拥有router选项
Vue.prototype.$router = this.$options.router; // vm.$router
}

}
})
}

export default VueRouter;

注册全局组件router-linkrouter-view

首先我们可以新建两个文件:ou-router-link.jsou-router-view.js

1
2
3
4
5
6
// ou-router-link.js 和 ou-router-view.js
export default {
render(createElement){
return createElement('div','router-view'); // 返回虚拟Dom
}
}

然后引入进来,在install方法中注册两个全局组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import ouRouterLink from "./ou-router-link";
import ouRouterView from "./ou-router-view";

let Vue;

class VueRouter {}

VueRouter.install = function (_Vue) {
Vue = _Vue;

Vue.mixin({
...
})

/* 注册全局组件router-link和router-view */
Vue.component('router-link', ouRouterLink);
Vue.component('router-view', ouRouterView);
}

export default VueRouter;
  • router-view是一个a标签
  • router-viewto属性设置到a标签的herf属性(先默认使用hash方法)
  • 获取router-view的插槽内容,插入a标签中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
props: {
to: {
type: String,
required: true
}
},

render(createElement) { // 返回虚拟Dom
return createElement('a',
{
attrs: {href: '#' + this.to} // 设置a标签的href属性
},
this.$slots.default // 获取标签插槽内容
);
}
}

实现router-view

router-view实质上根据url的变化,实时响应渲染对应的组件,而createElement函数是可以传入一个组件参数的。

因此,我们不进行渲染任何内容,后面实现监听url变化后,从映射表获取到组件后,再来实现router-view

1
2
3
4
5
6
export default {
render(createElement) {
let component = null;
return createElement(component); // 返回虚拟Dom
}
}

监听url变化

我们在VueRouter类的constructor函数中监听url的变化,这里我们默认使用hash方式。

而且,我们需要将存入url的变量设置为响应式数据,这样子当其发生变化的时候,router-viewrender函数才能够再次执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class VueRouter {
/*
* options:
* mode: 'hash'
* base: process.env.BASE_URL
* routes
* */
constructor(options) {
this.$options = options;

// 将current设置为响应式数据,即current变化时router-view的render函数能够再次执行
const initial = window.location.hash.slice(1) || '/';
Vue.util.defineReactive(this, 'current',initial);

// 监听hash变化
window.addEventListener('hashchange', () => {
this.current = window.location.hash.slice(1);
})
}
}

同时,我们可以再创建一个路由映射表,便于组件获取到路由信息。

1
2
3
4
5
6
7
8
9
10
11
12
class VueRouter {
constructor(options) {

...

// 创建一个路由映射表
this.routeMap = {}
options.routes.forEach(route => {
this.routeMap[route.path] = route
})
}
}

因此,我们可以来实现router-view组件。

render函数中,this.$router指向的是VueRouter创建的实例,因此我们可以通过this.$router.$option.routes获取路由映射表,this.$router.current获取当前路由,然后通过遍历匹配获取组件。

1
2
3
4
5
6
7
8
9
export default {
render(createElement) {
//获取path对应的component
const {routeMap, current} = this.$router;

const component = routeMap[current].component || null;
return createElement(component)
}
}

实现history模式

前面的实现都默认为hash模式,接下来简单实现一下history模式。

首先将监听url的代码优化一下,并判别mode的值来设置current的初始值,而history模式下初始值为window.location.pathname

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
38
39
40
41
42
class VueRouter {
/*
* options:
* mode: 'hash'
* base: process.env.BASE_URL
* routes
* */
constructor(options) {
this.$options = options;

switch (options.mode) {
case 'hash':
this.hashModeHandle();
break;
case 'history':
this.historyModeHandle();
}

this.routeMap = {}
options.routes.forEach(route => {
this.routeMap[route.path] = route
})
}

// Hash模式处理
hashModeHandle() {
// 将current设置为响应式数据,即current变化时router-view的render函数能够再次执行
const initial = window.location.hash.slice(1) || '/';
Vue.util.defineReactive(this, 'current', initial);

// 监听hash变化
window.addEventListener('hashchange', () => {
this.current = window.location.hash.slice(1);
})
}

// History模式处理
historyModeHandle() {
const initial = window.location.pathname || '/';
Vue.util.defineReactive(this, 'current', initial);
}
}

然后我们来实现history模式下的router-link组件。

history模式下,当我们点击router-link时,即点下a标签时,页面会重新刷新。所以我们需要设置一下其点击事件,取消默认事件,然后通过history.pushState去修改url,然后重设current的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default {
render(createElement) { // 返回虚拟Dom
const self = this;
const route = this.$router.$options.routes
.find(route => route.path === this.to);
return createElement('a',
{
attrs: {href: this.to}, // 设置a标签的href属性
on: {
click(e) {
e.preventDefault(); // 取消a标签的默认事件,即刷新页面
history.pushState({}, route.name, self.to); // 通过history.pushState来改变url
self.$router.current = self.to;
}
}
},
this.$slots.default // 获取标签插槽内容
);
}
}

最后我们将两种模式的router-link组件进行一个合并。

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
export default {
props: {
to: {
type: String,
required: true
},
},
render(createElement) { // 返回虚拟Dom
if(this.$router.$options.mode === 'hash'){
return createElement('a',
{
attrs: {href: '#' + this.to} // 设置a标签的href属性
},
this.$slots.default // 获取标签插槽内容
);
}else{
const self = this;
const route = this.$router.$options.routes
.find(route => route.path === this.to);
return createElement('a',
{
attrs: {href: this.to}, // 设置a标签的href属性
on: {
click(e) {
e.preventDefault(); // 取消a标签的默认事件,即刷新页面
history.pushState({}, route.name, self.to); // 通过history.pushState来改变url
self.$router.current = self.to;
}
}
},
this.$slots.default // 获取标签插槽内容
);
}
}
}

实现嵌套路由

vue-router实际上是可以实现路由嵌套的。

我们在路由配置中,在About页面添加一个子路由,即/about/info

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
38
39
import Vue from 'vue'
import VueRouter from './ou-vue-router'
import Home from '../views/Home.vue'


Vue.use(VueRouter)

const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
// 添加一个子路由
children: [
{
path: '/about/info',
component: {
render(h) {
return h('div', 'info page')
}
}
}
]
}
]

const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})

export default router

然后在About.vue中插入router-view组件。

1
2
3
4
5
6
7
<template>
<div class="about">
<h1>This is an about page</h1>
<router-view></router-view>
</div>
</template>

这时候运行的话,来到About页面的时候,后台就会报错,而来到Info页面,则不会显示内容。

这是因为在About页面中,存在一个router-view组件,而这个router-view会一直返回About页面的虚拟Dom,而虚拟Dom中又有一个router-view组件,因此形成一个死循环。

因此我们需要在router-view组件中,设置一个变量来保存页面的深度值,即判定是否返回对应页面的虚拟Dom。

其次,我们还需要处理路由里的children属性,获取里面的嵌套路由信息,否则的话/about/info是无法渲染出来的。

因此我们重构一下前面url监听的代码,使用一个matched响应式数组存放当前路径下的所有路由信息,可通过页面深度去获取到对应的路由信息。

因此,我们就需要完成这两个任务:

  • router-view深度标记;

  • 路由匹配时获取代表深度层级的matched数组

首先,我们不再用routeMap存路由映射表了,也不用current作为响应式属性了,使用一个matched数组来作为响应式属性。

而这个matched属性,里面存放的是当前路径下的所有路由信息,比如/about路径,matched数组就存放着about路由的信息,如果是/about/info路径,matched存放的是about路由和info路由的信息,因此我们也可以通过页面深度去获取到对应的路由信息。

matched数组的赋值动作,我们单独写一个match方法来实现。

然后每一次路由的变化,我们都需要清空matched数组,并调用一次match办法。

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
38
39
40
class VueRouter {
constructor(options) {
this.$options = options;

switch (options.mode) {
case 'hash':
this.hashModeHandle();
break;
case 'history':
this.historyModeHandle();
break;
}
}

// Hash模式处理
hashModeHandle() {
// 将current设置为响应式数据,即current变化时router-view的render函数能够再次执行
this.current = window.location.hash.slice(1) || '/';
Vue.util.defineReactive(this, 'matched', []);
// match方法可以递归遍历路由表,获得匹配关系的数组
this.match();

// 监听hash变化
window.addEventListener('hashchange', () => {
this.current = window.location.hash.slice(1);
this.matched = [];
this.match();
})
}

// History模式处理
historyModeHandle() {
this.current = window.location.pathname || '/';
Vue.util.defineReactive(this, 'matched', []);
this.match();
}

match(routes) { }
}
}

router-link组件也需要做一下修改:

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
export default {
props: {
to: {
type: String,
required: true
},
},
render(createElement) {
if(this.$router.$options.mode === 'hash'){
return createElement('a',
{
attrs: {href: '#' + this.to}
},
this.$slots.default
);
}else{
const self = this;
const route = this.$router.$options.routes
.find(route => route.path === this.to);
return createElement('a',
{
attrs: {href: this.to}, 性
on: {
click(e) {
e.preventDefault();
history.pushState({}, route.name, self.to);
self.$router.current = self.to;
self.$router.matched = []; // 清空matched数组
self.$router.match(); // 调用match方法
}
}
},
this.$slots.default
);
}
}
}

接下来我们来完善一下match方法。

首先match方法接收一个routes参数,如果未传入参数的话,默认为this.$options.routes路由表。

然后遍历理由表,如果当前路径是根路径的话,就将根路径的路由信息pushmatched数组中,因为一般不会根路径下创建嵌套路由,因此我们就可以结束遍历,直接return

如果不是根路径的话,就将与当前路径匹配的路由信息,存入matched数组中,并且判断该路由信息是否有chilren属性,有的话自调用match方法,并传入route.chilren作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 && route.children.length){
this.match(route.children);
}
return ;
}
}
}

最后,我们来完善一下router-view

首先我们需要一个对当前虚拟Dom贴个标签,即在它的data中新建一个routerView的变量,设置为true

其次,我们设置一个深度变量depth,初始值为0;然后获取该虚拟Dom的父级组件。如果父级组件存在的话,我们判断该父级组件的data中是否存在routerView的变量并且为true,如果存在的话,depth加一。

接着就继续检测该父组件的父组件,直至找不到为止。

最后我们获取到了当前router-view的路由嵌套深度,就匹配一下matched数组,获取对应的路由信息,并返回出去。如果匹配不到的话,就返回null

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
export default {
render(createElement) {
// 标记当前router-view的深度
this.$vnode.data.routerView = true; // 当前虚拟DOM的data,添加一个routerView属性

let depth = 0;
let parent = this.$parent;

while (parent) {
if (parent.$vnode && parent.$vnode.data && parent.$vnode.data.routerView) {
// 说明当前parent是一个router-view
depth++;
}
parent = parent.$parent;
}

//获取path对应的component
let component = null;
const route = this.$router.matched[depth];
if (route) {
component = route.component;
}

console.log(component);
return createElement(component)
}
}
 Comments