本文会从通过对vue-router的原理解析,实现一个简易版本的vue-router。
我们在页面中使用vue-router一般是这样的:
<!--这是App.vue组件-->
<div>
<div>
<!--这里放导航-->
<router-link to="/home">home</router-link>
<router-link to="/about">about</router-link>
</div>
<!--这里是导航对应的组件要渲染的地方-->
<router-view></router-view>
</div>
- 当我们点击
home导航,<router-view></router-view>里就会展示home组件对应的内容 - 当我们点击
about导航,<router-view></router-view>里就会展示about组件对应的内容
基本原理
<router-link to="/home">home</router-link>
<router-link to="/about">about</router-link>
会被解析为
<a href="#/home">home</a>
<a href="#/about">about</a>
当我们点击<a href="#/home">home</a>时,页面链接的hash就变成了#/home,这会触发hashchange事件,我们通过监听hashchange事件,可以拿到当前页面的hash变为了#/home,然后可以通过/home拿到其对应的组件,再将组件放到<router-view></router-view>组件里渲染出来就可以了。
如何通过/home拿到对应的组件呢,因为我们创建router实例的时候会先创建一个类似下面routes的路由配置对象,通过该对象可以拿到当前路由对应的组件。
const routes = [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
以上就是从路由变化到路由跳转的一个基本过程。
源码实现
我们通过代码来实现上面的这个简易版vue-router
当我们使用vue-router的时候,一般是下面这样使用的:
import Vue from 'vue';
import vueRouter from 'vue-router';
import Home from './components/App';
import Home from './components/Home';
import About from './components/About';
// vue-router是插件,需要使用Vue.use
Vue.use(vueRouter);
// 创建路由配置对象
const routes = [
{ path: '/home', component: Home },
{ path: '/about', component: About }
]
// 创建 router 实例,然后把routes传进去
const router = new vueRouter({
routes
})
// 创建Vue实例,把router实例也传进去
new Vue({
router,
render: h => h(App)
}).$mount('#app')
需要做的事情:
- 1、首先vue-router作为一个插件,需用有一个install方法,因为
Vue.use(vueRouter)的时候,会去找vueRouter里的install方法,并执行。 - 2、需要定义两个全局组件
router-link 和 router-view。 - 3、我们在组件里会使用this.$router来拿到router实例,所以需要在合适的时机将router实例挂载到Vue.prototype上。
- 4、需要监听hash的变化,并在变化的时候根据hash更新
router-view里的组件。
实现代码如下:
(具体解析请看代码里的注释)
let vue;
class Router {
constructor({ routes }) {
// 在router实例上定义一个current属性,用来保存页面的hash
// 并且使用Vue提供的defineReactive方法,让这个属性变成是可响应的,
// 所有依赖了这个属性的组件会被依赖收集起来,当这个属性变化的时候,依赖它的组件就会更新
// router-view组件里面引用了router实例的current属性,
// 所以当current变化的时候,router-view组件会重新渲染,
// 重新渲染的时候,会根据当前的hash拿到其对应的组件并渲染
vue.util.defineReactive(this, 'current', '/');
// 监控hash变化,hash变化的时候更新current,从而触发router-view组件的重新渲染
window.addEventListener('hashchange', this.onHashChange.bind(this));
window.addEventListener('load', this.onHashChange.bind(this));
// 创建一个路由映射表,用于让router-view重新渲染的时候,
// 更方便的可以根据hash拿到对应的要渲染的组件
this.routesMap = {};
for (let {path, component} of routes) {
this.routesMap[path] = component;
}
}
// hash变化的时候,将hash设置到current属性上
onHashChange() {
this.current = window.location.hash.slice(1);
}
}
Router.install = function(_vue) {
vue = _vue;
// 混入一个beforeCreate钩子函数,将router实例挂载到vue.prototype上,
// 这样所有vue实例都能通过this.$router拿到路由实例,
// 为什么要使用混入的方式呢?
// 因为Vue.use的时候,router实例还没创建,
// 所以使用混入在beforeCreate的时候,再将router实例挂载到Vue的原型对象上
vue.mixin({
beforeCreate() {
this.$options.router
&& (vue.prototype.$router = this.$options.router);
}
});
// 创建 router-link 全局组件
vue.component('router-link', {
props: ['to'],
render(h) {
return h
(
// 创建a标签
'a',
// 设置href属性为组件上的to属性
{ attrs: {href: '#' + this.to} },
// a标签内容设置为组件默认插槽的内容
this.$slots.default
)
}
});
// 创建 router-view 全局组件
vue.component('router-view', {
// 在组件内我们可以通过 this.$router 获取路由实例
// 通过this.$router.current获取当前页面的hash
// 通过this.$router.routesMap获取路由映射表
// 所以通过 this.$router.routesMap[this.$router.current] 就能获取该hash对应的组件
// 因为this.$router.current这个属性是响应式的
// 所以每当hash变化的时候,current就会变化,就会触发router-view重新执行渲染函数拿到新的hash对应的组件,并渲染
render(h) {
let com = this.$router.routesMap[this.$router.current];
return h(com);
}
});
}
export default Router;
以上就是vue-router的基本原理,下面补充下其它功能
路由嵌套
我们在写路由的时候有时候会写嵌套路由,配置对象类似下面这样,我们在/about路由下添加了children属性,保存其子路由:
import Home from './components/Home';
import About from './components/About';
import RouterLearn from './components/RouterLearn';
const routes = [
{ path: '/home', component: Home },
{
path: '/about',
component: About,
// 子路由
children: [
{
path: 'routerLearn',
component: RouterLearn
}
]
}
]
单文件组件里面会这么引用:
App.vue组件
<template>
<div>
<p>这是App组件</p>
<div>
<router-link to="/home">home</router-link>
<router-link to="/about/routerLearn">about/routerLearn</router-link>
</div>
<router-view></router-view>
</div>
</template>
About组件
<template>
<div>
这是about组件
<router-view></router-view>
</div>
</template>
当我们点击<router-link to="/about/routerLearn">routerLearn</router-link>导航时,页面的hash会变成#/about/routerLearn,所以这个时候要实现的是App.vue组件里的<router-view></router-view>渲染的是About组件,About组件里的<router-view></router-view>里面渲染的是routerLearn组件。
如何让<router-view></router-view>知道自己该渲染的是哪个组件呢,实现方法是:
- 1、给每个router-view组件计算出自己的深度,最外层的
router-view其深度为0,如果router-view渲染的组件里又有router-view,那么这个嵌套的router-view深度就是1,计算方法如下:
vue.component('router-view', {
render(h) {
// 如果是router-view组件,就在实例上设置一个标识
this.routerView = true;
// 获取当前router-view的深度,初始值为0
let depth = 0;
let parent = this.$parent; // 父组件实例
// 向上递归查找父组件,如果找到depth就+1,最终计算出的depth即为该router-view的深度
while (parent) {
if (parent.routerView) depth++;
parent = parent.$parent;
}
// 根据当前router-view的深度,从匹配的路由中拿到对应的组件
let com = this.$router.matched[depth].component;
return h(com);
}
});
- 2、根据路由配置对象以及当前页面的hash,计算出匹配当前页面hash的路由数组,计算方法如下:
// 路由配置对象是这样的
const routes = [
{ path: '/home', component: Home },
{
path: '/about',
component: About,
// 子路由
children: [
{
path: 'routerLearn',
component: RouterLearn
}
]
}
]
// 当前的页面hash为 #/about/routerLearn
let current = '#/about/routerLearn';
// 得到当前hash匹配的路由数组
// 这里matched会得到 [About路由配置对象, RouterLearn路由配置对象]
let matched = match(routes);
// 递归遍历routes,获取当前hash匹配的路由
function match(routes) {
for (const route of routes) {
if (this.current.indexOf(route.path) !== -1) {
this.matched.push(route);
if (route.children) {
this.match(route.children);
}
}
}
}
- 3、计算出
router-view的深度,以及拿到matched数组后,就能知道每个router-view该渲染哪个组件了,需要渲染的组件为matched[depth].component
实现了嵌套路由的简易版vue-router代码如下:
let vue;
class Router {
constructor({ routes }) {
// 实例上保存一下路由配置对象
this.routes = routes;
vue.util.defineReactive(this, 'current', '/');
// 保存匹配的路由数组,并设置为响应式,这样当该数组变化的时候,页面会重新渲染
vue.util.defineReactive(this, 'matched', []);
this.onHashChange();
// 监控url变化,url变化的时候更新current,从而触发router-view组件的重新渲染
window.addEventListener('hashchange', this.onHashChange.bind(this));
}
// hash变化时,重新获取匹配的路由
onHashChange() {
this.current = window.location.hash.slice(1);
this.matched = []; // 保存匹配的路由数组
this.match();
}
// 递归遍历routes,获取当前hash匹配的路由
match(routes = this.routes) {
for (const route of routes) {
if (this.current.indexOf(route.path) !== -1) {
this.matched.push(route);
if (route.children) {
this.match(route.children);
}
}
}
}
}
Router.install = function(_vue) {
vue = _vue;
vue.mixin({
beforeCreate() {
this.$options.router
&& (vue.prototype.$router = this.$options.router);
}
});
vue.component('router-link', {
props: ['to'],
render(h) {
return h
(
'a',
{ attrs: {href: '#' + this.to} },
this.$slots.default
)
}
});
vue.component('router-view', {
render(h) {
// 如果是router-view组件,就在实例上设置一个标识
this.routerView = true;
// 获取当前router-view的深度
let depth = 0;
let parent = this.$parent; // 父组件实例
while (parent) {
if (parent.routerView) depth++;
parent = parent.$parent;
}
// 根据当前router-view的深度,从匹配的路由中拿到对应的组件,然后渲染
let com = this.$router.matched[depth].component;
// let com = this.$router.routesMap[this.$router.current];
return h(com);
}
});
}
export default Router;