摘要:最近项目中遇到了点Vue Router的问题,发现有些只是没理清楚,再次复习了下Vue Router的源码,记录下...
Vue-Router的能力十分强大,它支持hash、history、abstract 3种路由方式,提供了<router-link>和<router-view>2种组件,还提供了简单的路由配置和一系列好用的 API。
先来看一个最基本使用例子,学习源码可结合这个例子逐步调试,理解整个路由工作过程:
<div id="app"><h1>Hello App!</h1><p><!-- 使用 router-link 组件来导航. --><!-- 通过传入 `to` 属性指定链接. --><!--** <router-link> 默认会被渲染成一个 `<a>` 标签** --><router-link to="/foo">Go to Foo</router-link><router-link to="/bar">Go to Bar</router-link></p><!-- 路由出口 --><!-- 路由匹配到的组件将渲染在这里 --><router-view></router-view>
</div>
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App'**Vue.use(VueRouter) // 注册**// 1. **定义(路由)组件**。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }// 2. **定义路由配置**
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过Vue.extend()创建的组件构造器,或者只是一个组件配置对象。晚点再讨论嵌套路由。
const routes = [{ path: '/foo', component: Foo },{ path: '/bar', component: Bar }
]// 3. **创建 router 实例**,**然后传 `routes` 配置**
// 还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({routes // (缩写)相当于 routes: routes
})// 4. **创建和挂载根实例**。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({el: '#app',render(h) {return h(App)},router
})
关于VueRouter,先从 Vue.use(VueRouter) 说起。
1. 路由注册
Vue 从设计上就是一个渐进式JavaScript框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。Vue-Router就是官方维护的路由插件,在介绍它的注册实现之前,我们先来分析一下 Vue 通用的插件注册原理。
1.1 Vue.use
Vue提供了Vue.use的全局API来注册这些插件,Vue.js插件初始化函数的实现定义在 vue/src/core/global-api/use.js 中:
export function initUse (Vue: GlobalAPI) {Vue.use = function (plugin: Function | Object) { // **Vue对象的use方法,用于注册插件**// 存储所有注册过的plugin,未定义过则初始化为空数组**const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))if (installedPlugins.indexOf(plugin) > -1) { // **保证插件只注册一次**return this}const args = toArray(arguments, 1) // toArray函数将arguments对象转换为真正的数组,索引从1开始,跳过第一个参数(即plugin)args.unshift(this) // install方法的第一个参数,存储Vueif (typeof plugin.install === 'function') { // 判断plugin有没有定义install方法plugin.install.apply(plugin, args) // 调用插件的install方法,将plugin对象作为上下文,并将args数组作为参数传递给install方法} else if (typeof plugin === 'function') { // 插件本身就是一个函数,则直接调用plugin.apply(null, args)}installedPlugins.push(plugin) // 已注册的插件添加到installedPlugins数组中,以便跟踪已安装的插件return this}
}
上述方法中,Vue.use 接受一个plugin参数,并且维护了一个_installedPlugins数组,它存储所有注册过的plugin;接着又会判断 plugin 有没有定义install方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue;最后把plugin存储到 installedPlugins 中。 可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install 方法,当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个install方法的第一个参数我们可以拿到Vue对象,这样的好处就是作为插件的编写方不需要再额外去import Vue(Vue的插件对Vue对象是有依赖的,但又不能去单独去import Vue,因为那样会增加包体积,所以就通过这种方式拿到Vue对象)。
1.2 路由安装
Vue-Router的入口文件是src/index.js,其中定义了VueRouter类,也实现了install的静态方法:VueRouter.install = install,它的定义在 src/install.js 中。
export let _Vue // _Vue变量,用于存储传入的Vue构造函数;export后可在源码的任何地方访问Vue
export function install (Vue) { // VueRouter的install的静态方法,用于安装Vue Router**if (install.installed && _Vue === Vue) return // 避免重复安装。如果已安装且传入的Vue构造函数与之前保存的相同,则直接返回install.installed = true // 已安装的标志位_Vue = Vue // 保留传入的Vue**const isDef = v => v !== undefined // 检查变量是否已定义const registerInstance = (vm, callVal) => { // registerInstance,用于在组件中注册路由实例let i = vm.$options._parentVnodeif (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {i(vm, callVal)}}// 最重要的一步:利用Vue.mixin去把beforeCreate和destroyed钩子函数注入到每一个组件中**Vue.mixin({beforeCreate () { // beforeCreate生命周期钩子中执行一些逻辑,包括**初始化路由**、定义响应式对象等if (isDef(this.$options.router)) { // 判断当前组件是否存在$options.router,存在则是根组件this._routerRoot = this // 将当前组件设置为根组件(根Vue实例)this._router = this.$options.router // 将当前组件的$options.router赋值给 _router,即保存了路由实例this._router.init(this)// 将_route变量变成响应式对象,实现当路由发生变化时自动更新视图Vue.util.defineReactive(this, '_route', this._router.history.current) // 把this._route变成响应式对象} else { // 非根组件, 将其与根组件关联起来// 当前组件有父组件,并且父组件存在_routerRoot,则将其设置为当前组件的_routerRoot,否则将当前组件设置为自身的_routerRootthis._routerRoot = (this.$parent && this.$parent._routerRoot) || this}registerInstance(this, this)},destroyed () { // 注销路由实例registerInstance(this)}})// 原型上定义$router属性,使得在组件中可以通过**this.$router访问路由实例**Object.defineProperty(Vue.prototype, '$router', {get () { return this._routerRoot._router }})// 原型上定义 $route 属性,使得在组件中可以通过**this.$route访问当前路由信息**Object.defineProperty(Vue.prototype, '$route', {get () { return this._routerRoot._route }})Vue.component('RouterView', View) **// 全局注册名为RouterView的组件,使用的组件是View**Vue.component('RouterLink', Link) **// 全局注册名为RouterLink的组件,使用的组件是Link**const strats = Vue.config.optionMergeStrategies // 获取Vue的配置选项合并策略strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created // 将路由相关的生命周期钩子函数合并策略设置为created
}
当用户执行Vue.use(VueRouter)的时候,实际上就是在执行install函数(完成将 Vue Router 注入到 Vue 实例中),为了确保 install 逻辑只执行一次,用了install.installed变量做已安装的标志位。
另外用一个全局的_Vue来接收参数Vue,因为作为Vue的插件对Vue对象是有依赖的,但又不能去单独去import Vue,因为那样会增加包体积,所以就通过这种方式拿到Vue对象。 Vue-Router安装最重要的一步就是利用Vue.mixin去把beforeCreate和destroyed钩子函数注入到每一个组件中。Vue.mixin的定义,在vue/src/core/global-api/mixin.js 中:
export function initMixin (Vue: GlobalAPI) { // 接受参数Vue,用于初始化Vue实例Vue.mixin = function (mixin: Object) { // 将mixin函数添加到Vue上。参数mixin,表示要混入的选项this.options =** mergeOptions(this.options, mixin) // 将当前Vue实例的选项与传入的mixin对象进行合并return this}
}
它的实现实际上非常简单,就是把要混入的对象通过mergeOptions合并到Vue的options 中,由于每个组件的构造函数都会在extend阶段合并Vue.options到自身的options中,所以也就相当于每个组件都定义了mixin定义的选项。 回到 Vue-Router 的install方法,先看混入的beforeCreate钩子函数,对于根Vue实例而言,执行该钩子函数时定义了this._routerRoot表示它自身; this._router表示VueRouter的实例router,它是在new Vue的时候传入的;另外执行了this._router.init()方法初始化 router,这个逻辑之后介绍,然后用 defineReactive方法把this._route变成响应式对象,这个作用我们之后会介绍。而对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候this._routerRoot始终指向的离它最近的传入了router对象作为配置而实例化的父实例。 对于beforeCreate和destroyed钩子函数,它们都会执行registerInstance方法,这个方法的作用之后会介绍。 接着给Vue原型上定义了router和route 2个属性的get方法,这就是为什么我们可以在组件实例上可以访问this.router以及 this.route,它们的作用之后介绍。 接着又通过Vue.component 方法定义了全局的<router-link>和<router-view> 2个组件,这也是为什么我们在写模板的时候可以使用这两个标签,它们的作用也是之后介绍。 最后定义了路由中的钩子函数的合并策略,和普通的钩子函数一样。
总结:
- Vue编写插件的时候,通常要提供静态的install方法;
- Vue-Router的install方法会给每一组件注入beforeCreated和destoryed钩子函数。在beforeCreated做一些私有属性定义和路由初始化工作;
2. VueRouter对象
VueRouter 的实现是一个类,定义在 src/index.js 中,先对它做一个简单地分析:
export default class VueRouter {static install: () => void;static version: string;app: any;apps: Array<any>;ready: boolean;readyCbs: Array<Function>;options: RouterOptions;mode: string;history: HashHistory | HTML5History | AbstractHistory;matcher: Matcher;fallback: boolean;beforeHooks: Array<?NavigationGuard>; resolveHooks: Array<?NavigationGuard>;afterHooks: Array<?AfterNavigationHook>;// 构造函数**constructor (options: RouterOptions = {}) { this.app = null // 初始化了一些属性this.apps = []this.options = optionsthis.beforeHooks = [] // 导航守卫this.resolveHooks = []this.afterHooks = []this.matcher = createMatcher(options.routes || [], this)let mode = options.mode || 'hash' // **根据传入的options.mode来确定路由模式****// history模式支持判断,**supportsPushState会对浏览器UA进行检测this.fallback = mode === 'history' && !supportsPushState && options.fallback !== falseif (this.fallback) {mode = 'hash'}if (!inBrowser) { // abstract模式不是浏览器环境下使用mode = 'abstract'}this.mode = modeswitch (mode) { // 根据路由模式创建对应的路由历史对象this.history(继承于history Class)case 'history':this.history = new HTML5History(this, options.base)breakcase 'hash':this.history = new HashHistory(this, options.base, this.fallback)breakcase 'abstract':this.history = new AbstractHistory(this, options.base)breakdefault:if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)}}}// 路由匹配方法: 传入原始位置raw、当前路由current和重定向来源redirectedFrom,返回匹配的路由对象match (raw: RawLocation,current?: Route,redirectedFrom?: Location): Route {return this.matcher.match(raw, current, redirectedFrom)}get currentRoute (): ?Route { // 当前的路由对象,通过访问路由历史对象的current属性获取return this.history && this.history.current}// 初始化路由和应用程序实例,并监听路由变化,更新应用程序实例的_route属性init (app: any) {// 非生产环境,并且 install.installed 不为真,则抛出错误提示,提醒在创建根实例之前调用Vue.use(VueRouter)process.env.NODE_ENV !== 'production' && assert(install.installed,`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +`before creating root instance.`)this.apps.push(app) // 应用程序实例app添加到apps数组中,用于跟踪多个应用程序实例if (this.app) { // 如果当前应用程序实例this.app已存在,则直接返回,否则将传入的应用程序实例app 设置为当前应用程序实例return}this.app = appconst history = this.history// 根据路由模式初始化路由历史对象,并监听路由变化if (history instanceof HTML5History) { // History模式,则调用transitionTo方法进行路由过渡到当前位置history.transitionTo(history.getCurrentLocation())} else if (history instanceof HashHistory) { // Hash模式则设置监听器并调用transitionTo 方法进行路由过渡到当前位置const setupHashListener = () => {history.setupListeners()}history.transitionTo(history.getCurrentLocation(),setupHashListener,setupHashListener)}history.listen(route => { // 每当路由变化时,更新每个应用程序实例 _route属性为新的路由信息this.apps.forEach((app) => {app._route = route})})}// 导航触发之前调用的钩子函数,可以用来进行导航守卫beforeEach (fn: Function): Function { // 函数作为参数,并将其注册到beforeHooks钩子数组中return registerHook(this.beforeHooks, fn)}// 导航确认之前调用的钩子函数,和 beforeEach 类似,但是在**所有异步路由组件解析之后调用**beforeResolve (fn: Function): Function { // 将函数注册到resolveHooks钩子数组中return registerHook(this.resolveHooks, fn)}// 导航成功完成之后调用afterEach (fn: Function): Function { // 将函数注册到afterHooks钩子数组中return registerHook(this.afterHooks, fn)}// 路由初始化完成时调用的回调函数onReady (cb: Function, errorCb?: Function) { // 将onReady方法传递给路由历史管理对象(如this.history) 的onReady方法this.history.onReady(cb, errorCb)}// 当路由初始化失败时调用的回调函数onError (errorCb: Function) { // 将onError方法传递给路由历史管理对象的onError方法this.history.onError(errorCb)}// 路由历史堆栈中添加一个新的路由记录push (location: RawLocation, onComplete?: Function, onAbort?: Function) {this.history.push(location, onComplete, onAbort) // 调用路由历史管理对象的push方法,用于向历史堆栈中添加新的路由记录。}// 替换当前的路由记录,导航到指定的位置replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {this.history.replace(location, onComplete, onAbort) // 类似于push,但是用于替换当前路由记录而不是添加新的记录}go (n: number) {this.history.go(n) // 整数参数n,表示前进或后退的步数。调用路由历史管理对象的go方法,以在浏览器历史记录中导航}back () {this.go(-1) // back方法调用go(-1),表示后退一页}forward () {this.go(1) // forward方法调用go(1),表示前进一页}// 获取与目标位置匹配的组件数组,用于动态加载路由组件getMatchedComponents (to?: RawLocation | Route): Array<any> { const route: any = to // 可选的参数to,表示要匹配的目标路由。返回目标路由的匹配组件数组? to.matched? to: this.resolve(to).route: this.currentRouteif (!route) {return []}return [].concat.apply([], route.matched.map(m => {return Object.keys(m.components).map(key => {return m.components[key]})}))}resolve (to: RawLocation, // 解析目标路由current?: Route,append?: boolean): {location: Location,route: Route,href: string,normalizedTo: Location,resolved: Route} {const location = normalizeLocation( // 将目标位置标准化to,current || this.history.current,append,this)const route = this.match(location, current) // 使用路由匹配器(matcher)对目标位置进行匹配,得到路由信息const fullPath = route.redirectedFrom || route.fullPathconst base = this.history.baseconst href = createHref(base, fullPath, this.mode) // 根据路由信息生成href,并返回解析后的路由信息对象return {location,route,href,normalizedTo: location,resolved: route}}// 将新的路由配置添加到路由匹配器中,并触发对应的路由更新**addRoutes (routes: Array<RouteConfig>) { this.matcher.addRoutes(routes)if (this.history.current !== START) {this.history.transitionTo(this.history.getCurrentLocation())}}
}
VueRouter 定义了一些属性和方法,下面按照书序逐个分析其作用:
首先,从它的构造函数看,当我们执行 new VueRouter 的时候做了哪些事情。
constructor (options: RouterOptions = {}) {this.app = null // 初始化属性this.apps = []this.options = optionsthis.beforeHooks = []this.resolveHooks = []this.afterHooks = []this.matcher = createMatcher(options.routes || [], this) // 创建路由匹配器,未传入路由配置,则使用空数组作为默认值let mode = options.mode || 'hash'this.fallback = mode === 'history' && !supportsPushState && options.fallback !== falseif (this.fallback) {mode = 'hash'}if (!inBrowser) {mode = 'abstract'}this.mode = mode // 路由模式设置到路由器实例的 mode 属性中switch (mode) {case 'history':this.history = new HTML5History(this, options.base)breakcase 'hash':this.history = new HashHistory(this, options.base, this.fallback)breakcase 'abstract':this.history = new AbstractHistory(this, options.base)breakdefault:if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)}}
}
构造函数定义了一些属性,其中this.app表示根Vue实例,this.apps保存持有$options.router属性的 Vue实例,this.options 保存传入的路由配置。
this.beforeHooks、 this.resolveHooks、this.afterHooks 表示一些钩子函数,我们之后会介绍;this.matcher表示路由匹配器(重点),我们之后会介绍;
this.fallback表示在浏览器不支持history.pushState的情况下,根据传入的fallback配置参数,决定是否回退到hash模式;
this.mode表示路由创建的模式;
this.history表示路由历史的具体的实现实例,它是根据this.mode的不同实现不同,它有History基类,然后不同的history实现都是继承History。 实例化VueRouter后会返回它的实例router,我们在new Vue的时候会把router作为配置的属性传入,回顾一下上一节我们讲beforeCreate混入的时候有这么一段代码:
beforeCreate() {if (isDef(this.$options.router)) { // 检查是否定义了路由器实例// ...this._router = this.$options.router // 将路由器实例赋值给组件实例的 _router 属性this._router.init(this) // 传入了router实例,都会执行router.init方法// ...}
}
所以组件在执行 beforeCreate 钩子函数的时候,如果传入了router实例,都会执行router.init方法:
init (app: any) {// 非生产环境下,使用assert函数检查是否已经安装了Vue Router插件process.env.NODE_ENV !== 'production' && assert(install.installed,`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +`before creating root instance.`)this.apps.push(app) // Vue实例,然后存储到this.apps中if (this.app) { // 已经初始化过应用程序,则直接返回,避免重复初始化return}this.app = app // 将传入的应用程序实例赋值给路由器实例的app属性const history = this.history // 获取this.history,后面判断使用if (history instanceof HTML5History) { // HTML5Historyhistory.transitionTo(history.getCurrentLocation()) // 将当前路由状态切换到当前地址对应的路由} else if (history instanceof HashHistory) { // HashHistory执行不同的逻辑const setupHashListener = () => {history.setupListeners()}history.transitionTo( // 将当前路由状态切换到当前地址对应的路由,并设置哈希变化时的监听器。history.getCurrentLocation(),setupHashListener,setupHashListener)}history.listen(route => { // 监听路由变化,并将路由信息_route更新到所有应用程序实例中this.apps.forEach((app) => {app._route = route})})
}
init()方法的逻辑:
传入的参数是Vue实例,然后存储到this.apps中;只有根Vue实例会保存到this.app中,并且会拿到当前的this.history,根据它的不同类型来执行不同逻辑。
由于我们平时使用 hash路由多一些,所以我们先看这部分逻辑,先定义了 setupHashListener 函数,接着执行了 history.transitionTo 方法,它是定义在History基类中,代码在 src/history/base.js:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {const route = this.router.match(location, this.current) // 做匹配// ...
}
先不着急去看 transitionTo 的具体实现,先看第一行代码,它调用了this.router.match 函数:
// match方法,用于根据给定的原始位置(raw)、当前路由对象(current)和重定向来源(redirectedFrom)
// 来匹配路由并返回匹配的路由对象
match (raw: RawLocation,current?: Route,redirectedFrom?: Location
): Route {** return this.matcher.match(raw, current, redirectedFrom)**
}
实际上是调用了this.matcher.match方法去做匹配,所以接下来我们先来了解一下 matcher的相关实现。
总结:
路由初始化的时机是在组件的初始化阶段执行到beforeCreated钩子函数的时候会执行router.init方法。然后会执行history.transitionTo方法做路由过度。
持续更新中...