个人觉得学习一门框架,想要快速掌握其底层原理最好的办法就是去实现一个简版的、功能一样的东西,那我们今天就来学习一下vue-router的底层原理,并尝试着手写一个简版的vue-router。
平时我们是这么使用路由的
此时我们会思考
vue-router这个模块内部做了什么?Vue.use(VueRouter)是做什么的?
先把答案放在这里:
第一问:
1. vue-router 实现了一个install方法,因为vue-router也是一个插件,既然是插件,想要通过Vue.use()的方式进行注册,就得有一个install方法。
2. vue-router 实现并声明了两个全局组件(router-view和router-link),还有一些其他的实例方法,如:$router.push()等
第二问:Vue.use()会调用传入插件中的install方法,去注册vue-router。
接下来我们就根据答案,去实现vue-router中的相关功能。
为了让大家更好的理解,本文更多的是代码与注释的结合,毕竟这个东西用文本的方式讲述不太容易讲清楚,还是代码来的最实在。
一、创建构造函数
首先,创建VueRouter构造函数
// router/vue-router.js
let Vue; // 用于保存Vue实例
class VueRouter {
constructor(options) {
// 此处的this.$options是指向VueRouter实例的,值为new时传入的参数
this.$options = options;
}
}
此处的this.$options是指向VueRouter实例的,值为new时传入的参数,如下图指向,我们后面会用到其中的routes集合
二、创建install方法
创建install方法的目的是为了让Vue.use()调用此方法,去注册vue-router,并且实现相关功能
// router/vue-router.js
// Vue.use会调用该方法,注册VueRouter,其中参数_Vue是Vue.use调用时传入的,即Vue实例
VueRouter.install = function(_Vue) {
Vue = _Vue; // 将Vue实例保存到全局,方便其他地方使用
// 挂载$router属性到Vue原型上
mountRouter();
// 实现router-link组件
mountRouterLink();
// 实现router-view组件
mountRouterView();
};
此处,我们能够获取到Vue.use(vueRouter)传进来的Vue实例,我们将其保存到全局,这样其他地方就能很方便的使用到Vue实例了,切记,此处只是在初始化阶段,所以还无法获取到router实例。
接下来我们要在install中实现以下三个方法:
- 挂载$router属性到Vue原型上(mountRouter)
- 实现router-link组件(mountRouterLink)
- 实现router-view组件(mountRouterView)
1. 挂载$router属性到Vue原型上
为什么要挂载$router属性?是因为我们在通过router-view组件渲染路由时,要用到路由器对象里的信息,要根据不同的路由地址渲染不同的路由页面内容。
挂载$router属性到Vue原型上的实现方法如下:
// router/vue-router.js
function mountRouter() {
// 全局混入目的:延迟下面逻辑到router创建完毕并且附加到选项上时才执行
Vue.mixin({
// 该钩子在每个组件创建实例时都会被调用,所以会被执行多次
beforeCreate() {
// 1.只有当执行到根实例时,才能获取到router属性,因为只有根实例才有该选项
// 2.所以此处我们要进行相关判断,如果发现$option上有router属性,就证明vue-router已经挂载完成
if (this.$options.router) {
// 此处的this.$options是指向的main.js中的new Vue实例的
// 我们将其挂载到之前全局声明的Vue变量上,这样其他地方就能使用router实例上的信息了
Vue.prototype.$router = this.$options.router;
}
}
});
}
由于当Vue.use()注册vue-router时,我们还无法在此处的Vue上获取到完整的router实例,所以我们这里使用全局混入的方式,去延迟获取main.js中的new Vue那个实例上的router实例,因为只要执行到main.js中的new Vue()那一步时,我们的vue-router已经挂载完成,就能获取到router实例。此时我们可以打印下this.$option加以验证
2. 实现router-link组件
我们平时会通过router-link进行路由间跳转,router-link的底层实现,实际上就是a标签,只不过它是通过锚点(#)的形式进行跳转的,实际上页面并没有改变,只是通过渲染不同的路由页面达到跳转的效果,实现方法如下:
// router/vue-router.js
function mountRouterLink() {
// 注册全局组件 router-link
Vue.component("router-link", {
props: {
// to就是<router-link to="xxx"></router-link>上的属性,接收传进来的路由地址
to: {
type: String,
required: true
}
},
render(h) {
// 这里是通过vue中的render函数来进行渲染的,为什么不使用template模板的方式来进行渲染呢
// 因为此处没有编译器,我们知道要想在vue中使用模板的方式,必须得有编译器才行
// 不过此处除了通过render函数来渲染,也可以使用jsx的方式来实现
// 即:return <a href={'#'+this.to}>{this.$slots.default}</a>
return h(
"a", {
attrs: {
href: "#" + this.to
}
},
this.$slots.default
);
}
});
}
我们试着在页面中使用一下自己写的router-link组件,看看有没有达到预期的效果
我们可以看到,我们通过点击不同路由,路由地址确实发生了相应的变化。那么问题来了,如何实现点击不同路由,渲染对应的路由页面内容呢?接下来就来实现最重要的router-view组件。
3. 实现router-view组件
我们都知道router-view的作用是根据路由地址,渲染不同的路由页面内容,那么它底层是怎么实现的?实现方式如下
// router/vue-router.js
function mountRouterView() {
// 注册全局组件 router-view
Vue.component("router-view", {
// 一样是通过render函数进行相关渲染
render(h) {
// 当前定位的路由名称
let component = null;
// 将当前定位的路由名称,在VueRouter的实例上进行查找与对比
const route = this.$router.$options.routes.find(
// this.$router.current指当前定位的路由名称
route => route.path === this.$router.current
);
// 如果当前定位的路由在之前声明的routes列表里有
if (route) {
component = route.component;
}
// 那么就将对应的组件页面返回,并且通过render函数渲染到页面上
return h(component);
}
});
}
这里问题就来了,这里的当前定位的路由名称该怎么获取呢?我们是不是可以通过监听url地址栏的变化,然后进行截取相关路由字段,是不是就能获取到当前定位的路由了呢?
没错,接下来就在VueRouter构造函数中来实现监听吧
// router/vue-router.js
let Vue; // 用于保存Vue实例
class VueRouter {
constructor(options) {
// 此处的this.$options是指向VueRouter实例的,值为new时传入的参数
this.$options = options;
// 监听hash变化
window.addEventListener("hashchange", () => {
// current就是变化后的路由名称
let current = window.location.hash.slice(1);
});
}
}
但是问题又来了,我虽然获取到了变化后的路由名称,但是怎么能够在监听到路由变化的同时,去动态渲染对应的路由页面内容呢?有的同学就会想,直接调用之前的mountRouterView方法,这样就能实现动态渲染了,但是有没有想到一个问题,如果每次调用mountRouterView方法,是不是就每次都创建了一个router-view组件呢,我们知道全局只能有一个router-view组件,很显然这种方法是不可行的。
我们可不可以这样,利用vue的双向绑定这个特性去实现,我们将这个路由名称变量加上双向绑定特性,当该值发生改变时,我们的mountRouterView方法中的render函数是不是就可以动态的进行渲染了(这里render函数有个内置的特性,就是只要监听到router实例上有发生任何改变,都会重新进行渲染)?
我们继续在VueRouter构造函数中去实现
// router/vue-router.js
let Vue; // 用于保存Vue实例
class VueRouter {
constructor(options) {
// 此处的this.$options是指向VueRouter实例的,值为new时传入的参数
this.$options = options;
// 把current作为响应式数据,指当前所在路由名称
// 将来发生变化,router-view的render函数能够再次执行
const initial = window.location.hash.slice(1) || "/";
// 将一个变量添加双向绑定特性
Vue.util.defineReactive(this, "current", initial);
// 监听hash变化
window.addEventListener("hashchange", () => {
// 这里的current值一发生改变,$router实例上的current也会发生改变
// 进而使router-view的render函数能够再次执行
this.current = window.location.hash.slice(1);
});
}
}
只要有了双向绑定特性的current属性发生了变化,router-view就会重新进行渲染新的路由页面内容,以达到页面跳转的效果。我们来试试效果吧
这样,我们就基本实现了vue-router的功能,是不是觉得很神奇,原来vue-router的底层原理如此简单。当然更多的路由实例方法我这里也没有去实现,有兴趣的同学,可以自己去尝试实现一下。
好了,以上就是如何实现一个简版的vue-router,当然,跟原版的vue-router比起来肯定还是有差异的,有些地方可能讲的不对,还请大家做出指正,谢谢大家!