前言
我始终觉得如何能写出优雅高质量的代码并去了解一个知识点的相关知识,一个很有效的方法就是去阅读那些有许多人使用的框架插件的源码,这不仅能让我们学习到这个插件框架所用的知识点,也能学习别人是如何来编写一个框架插件的,就像本文就是对vue-router源码的解析,通过对vue-router源码的解析我们不仅可以了解到前端路由相关的几乎所有的知识点,也能学习别人是如何编写这种插件的,还有一点就是你能很好的使用vue-router这个插件甚至在这个插件的基础上去扩展,写出自己的前端路由框架。
本文主要先讲解一下vue-router的大致结构和基本思路一些具体的模块和实现的功能放到后面的文章去讲解,还有本文所分析的是vue-router 3.1.3 版本的源码,对应这个版本的vue框架应该是2.+的,vue-router源码中会有一些对vue源码的使用,所以要完全了解vue-router的源码你还需要了解一些vue源码相关的基础。不过这里我会把相关的vue源码中的知识点拿出来说明,这里的vue源码是2.6.1相关版本的。
推荐大家在阅读vue-router源码时最好先对vue-router相关的api和概念有一定程度的了解,这样在看到作者为什么这么设计和编写时就不会那么的懵逼了。
源码的目录结构
我们这里主要对src下的目录文件进行大致的分析,因为文件的数量还是比较多的,解析的内容主要是以一些核心的点为主而且最好是结合着源码进行阅读。下面就是src主要的一些文件。本文也不对一些基本的概念进行介绍分析了,像是前端路由的模式什么的,vue-router是如果使用的这些请同学们自行去了解。
从文件引入开始
我们知道使用vue-router基本的三个步骤,第一步引入router,Vue.use(router),这也是其他的vue插件都要进行的操作,也是vue的插件机制所要求的,第二步根据用户定义的路由配置new一个router对象,第三步就是在new Vue实例的时候传入这个new出来的router对象。经过这三个步骤vue-router就已经安装到我们的vue中了,并且完成基本的初始工作,接下来我们就对这三个步骤干了什么进行具体的分析。
在我们引入router的时候,就相当于引入了src下的index.js文件,在文件中有下面这一段代码
Vue.use(router)
这里的VueRouter就是router对象的类定义,他在这个类中添加了install方法,这个方法就是用于Vue.use方法中进行插件安装的,我们先看一下Vue.use的实现
通过这些代码我们可以知道Vue使用插件的本质上就是调用了插件的install方法并将当前的Vue构造函数传入install方法当做参数。接下来我们看下instal方法干了点什么。
Vue.mixin({
//在每个vue实例添加beforeCreate钩子,
beforeCreate () {
//vue实例beforeCreate时触发
if (isDef(this.$options.router)) {
//如果当前vue实例的options中存在router对象 一般就是new vue时候传入的根vue实例的router
this._routerRoot = this
this._router = this.$options.router
this._router.init(this) //调用router的init
//使_route属性具有响应性 对应的就是history对象中的current 当current改变会触发 view-router组件的更新
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
//这个分支是vue的子实例调用的逻辑,从父实例去获取_routerRoot的引用
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
上面这段代码就是install方法中的最主要的部分了,当我们在使用Vue.use时就会向Vue全局添加一个beforeCreate的钩子函数,这个钩子函数中我们根据当前的vue实例是否传入了router配置,这里一般只有根vue实例会传入,就是我们new Vue时传入的。经过这些操作,我们先对router进行了init初始化,这也是很重要的一个步骤,我们后面再讲,紧接着就是向vue实例上添加了响应绑定,这时我们的vue实例就和router实例产生了关联了。可能有同学会问,这些操作是在每一个vue实例初始化时都会调用吗,看代码我们就知道这些逻辑只会在根实例出入router对象的地方调用,那些vue子实例都是复用父实例对router相关的引用。这时我们Vue.use的相关逻辑就完成了。接下来就是new router实例了。
new Router
new router时 我们会传入一个配置对象参数,有关于这个配置对象的参数类型如下面所示,其中比较重要的就是routes用户自定义的路由配置,mode要采取的路由模式。
declare type RouterOptions = {
routes?: Array<RouteConfig>; // 用户定义的路由配置
mode?: string; // 路由模式 hash history等模式
fallback?: boolean; //是否回退到hash模式
base?: string; // 基础路由
linkActiveClass?: string;
linkExactActiveClass?: string;
parseQuery?: (query: string) => Object;
stringifyQuery?: (query: Object) => string;
scrollBehavior?: ( //滚动行为
to: Route,
from: Route,
savedPosition: ?Position
) => PositionResult | Promise<PositionResult>;
}
接下来看看new Router做了一些什么事情
constructor (options: RouterOptions = {}) {
this.app = null //根vue实例
this.apps = [] //相关vue实例
this.options = options //用户定义的路由配置
this.beforeHooks = [] //钩子函数
this.resolveHooks = [] //钩子函数
this.afterHooks = [] //钩子函数
//匹配器 解析路由配置 生成相关记录 返回路由方法
this.matcher = createMatcher(options.routes || [], this)
//获取模式配置 默认hash模式
let mode = options.mode || 'hash'
//是否回退hash
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
//不支持pushState就使用hash模式
mode = 'hash'
}
if (!inBrowser) {
//不在浏览器就用abstract
mode = 'abstract'
}
this.mode = mode
//创建路由history
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
我们可以看到new router时,先调用了createMatcher这个函数去创建一个路由匹配器,这个路由匹配器就是提供解析当前路由并在路由记录中匹配响应路由的功能,这是vue-router匹配规则实现的核心。接着根据用户传入的mode去创建不同的History对象,history对象主要提供了路由变化事件监听和路由跳转相关操作的实现,这也是vue-router比较核心的部分了。目前我们还不需要去了解这些逻辑的具体实现后面会进行细讲。当我们new router了以后只需要在new Vue时传入该router对象。这样基本的初始化流程就结束了,主要是创建了根据路由配置生成的路由记录的映射这个路由的映射在后面页面中跳转等都需要通过这个映射来进行页面和组件的匹配。