摘要:最近项目中遇到了点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方法的第一个参数,存储Vue
if (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._parentVnode
if (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,否则将当前组件设置为自身的_routerRoot
this._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 = options
this.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 !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) { // abstract模式不是浏览器环境下使用
mode = 'abstract'
}
this.mode = mode
switch (mode) { // 根据路由模式创建对应的路由历史对象this.history(继承于history Class)
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}`)
}
}
}
// 路由匹配方法: 传入原始位置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 = app
const 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.currentRoute
if (!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.fullPath
const base = this.history.base
const 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 = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this) // 创建路由匹配器,未传入路由配置,则使用空数组作为默认值
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode // 路由模式设置到路由器实例的 mode 属性中
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}`)
}
}
}
构造函数定义了一些属性,其中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) { // HTML5History
history.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方法做路由过度。
3. matcher
上一小节实例化Vue Router对象时,会执行器构造函数,然后会执行createMatcher方法,传入参数是用户配置数组options.routes和实例对象VueRouter实例,返回Matcher对象。
matcher相关的实现都在src/create-matcher.js中,我们先来看一下matcher的数据结构:
export type Matcher = { // Matcher对象的属性值为函数
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
};
Matcher返回了2个方法,match和addRoutes,在上一节我们接触到了 match 方法,顾名思义它是做匹配,那么匹配的是什么。
在介绍之前,我们先了解路由中重要的2个概念,Loaction 和 Route,它们的数据结构定义在 flow/declarations.js 中。
- Location
declare type Location = {
_normalized?: boolean;
name?: string;
path?: string;
hash?: string;
query?: Dictionary<string>;
params?: Dictionary<string>;
append?: boolean;
replace?: boolean;
}
Vue-Router中定义的Location数据结构和浏览器提供的window.location部分结构有点类似,它们都是对url的结构化描述。举个例子:/abc?foo=bar&baz=qux#hello,它的path是 /abc,query是 {foo:'bar',baz:'qux'}。Location的其他属性我们之后会介绍。
- Route
declare type Route = {
path: string;
name: ?string;
hash: string;
query: Dictionary<string>;
params: Dictionary<string>;
fullPath: string;
**matched: Array<RouteRecord>; // 表示匹配到的路由记录。路由记录包含了路由规则和组件信息等**
redirectedFrom?: string;
meta?: any;
}
Route表示的是路由中的一条线路,它除了描述了类似Loctaion的path、query、hash这些概念,还有matched表示匹配到的所有的 RouteRecord。Route的其他属性我们之后会介绍。
3.1 createMatcher
在了解了Location和Route后,我们来看一下matcher的创建过程:
export function createMatcher (
routes: Array<RouteConfig>, **// 用户定义的路由配置数组**
router: VueRouter // **new VueRouter返回的实例,用于路由导航等操作**
): Matcher { // Matcher对象,每个属性都是一个函数
const { pathList, pathMap, nameMap } = createRouteMap(routes) // 初始化,创建路由映射表
// pathList存储了所有路由的路径,pathMap将路径映射到路由记录,nameMap将命名路由映射到路由记录
function addRoutes (routes) { // 用于添加新的路由配置,并更新路由映射表
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 对原始路由进行标准化处理,获取标准的路由信息;然后,根据路由的名称或路径进行匹配
function match (**
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router) //对原始路由进行标准化处理
const { name } = location
if (name) {
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
}
} else if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
return _createRoute(null, location)
}
// 根据路由记录创建路由对象。如果路由记录包含重定向信息,则调用redirect函数进行重定向;如果包含别名
// 信息,则调用 alias 函数进行别名处理;否则,调用 createRoute 函数创建普通路由对象
**function _createRoute (**
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes
}
}
createMatcher接收2个参数,一个是router,它是我们new VueRouter返回的实例,一个是routes,它是用户定义的路由配置,结合之前举的例子中的配置:
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
首先, createMathcer中执行的逻辑是const { pathList, pathMap, nameMap } = createRouteMap(routes) 创建一个路由映射表,createRouteMap 的定义在 src/create-route-map 中:
export function createRouteMap (
routes: Array<RouteConfig>, // 参数(用户定义的路由配置)
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>; // 返回对象
pathMap: Dictionary<RouteRecord>;
nameMap: Dictionary<RouteRecord>;
} {
**// 首先初始化路径列表、路径映射和名称映射,如果旧的路径列表、路径映射和名称映射存在则使用旧的,否则创建新的空对象**
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => { // 遍历用户定义的路由配置数组routes
**addRouteRecord(pathList, pathMap, nameMap, route)**
})
for (let i = 0, l = pathList.length; i < l; i++) { **// 通配符优先级逻辑**
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return { // 返回
pathList,
pathMap,
nameMap
}
}
createRouteMap函数的目标是把用户的路由配置转换成一张路由映射表,该路由映射表包含3个部分pathList 存储所有的path,pathMap表示一个path到RouteRecord的映射关系,而nameMap表示name到RouteRecord的映射关系。
那么RouteRecord到底是什么,先来看一下它的数据结构:
declare type RouteRecord = { // 类型声明
path: string; // 路由路径
regex: RouteRegExp; // 路径的正则表达式,用于匹配路由路径
components: Dictionary<any>; // 存储路由组件的字典,可以根据不同的命名视图找到对应的组件
instances: Dictionary<any>; // 存储路由组件实例
name: ?string; // 路由的名称
parent: ?RouteRecord; // 路由的父级路由记录
redirect: ?RedirectOption; // 示重定向选项,是一个可选的重定向对象
matchAs: ?string; // 要匹配的路径,是一个可选的字符串
beforeEnter: ?NavigationGuard; // 路由的导航守卫,是一个可选的导航守卫函数
meta: any; // 存储路由元信息,可以是任意类型的数据
props: boolean | Object | Function | Dictionary<boolean | Object | Function>; // 路由组件是否需要注入路由参数,可以是布尔值、对象、函数或者字典
}
RouteRecord的创建是通过遍历routes为每一个route执行addRouteRecord 方法生成一条记录,来看一下addRouteRecord的定义:
function addRouteRecord (
pathList: Array<string>, // 路由路径列表,存储所有路由记录的路径
pathMap: Dictionary<RouteRecord>, // 路由路径映射表,根据路径快速查找对应的路由记录
nameMap: Dictionary<RouteRecord>, // 路由名称映射表,根据路由名称快速查找对应的路由记录
route: RouteConfig, // 路由配置对象,包含了路由的各种信息,如路径、组件等
parent?: RouteRecord, // 父路由记录对象,用于构建路由的嵌套结构
matchAs?: string // 匹配路径的别名,用于处理路由的别名情况
) {
const { path, name } = route // 解构路由配置对象,获取路径和名称
// 在开发环境下进行一些验证,确保路由配置的正确性
if (process.env.NODE_ENV !== 'production') {
assert(path != null, `"path" is required in a route configuration.`) // 确保提供了路径属性
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String(path || name)} cannot be a ` +
`string id. Use an actual component instead.` // 确保组件不是一个字符串 ID,而是一个有效的组件
)
}
// 根据路由配置的path和pathToRegexpOptions生成正则表达式,并将其保存在regex属性中
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
const normalizedPath = normalizePath(
path,
parent,
**pathToRegexpOptions.strict**
)
// 如果路由配置中指定了是否大小写敏感,则设置正则表达式的 sensitive 属性
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
**// 创建路由记录对象,并设置各种属性值(关键)**
const record: RouteRecord = { **// 关键部分**
path: normalizedPath, // 路径
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 对应的正则表达式
components: route.components || { default: route.component }, // 组件
instances: {}, // 组件实例
name, // 路由名称
parent, // 父路由记录对象
matchAs, // 匹配路径的别名
redirect: route.redirect, // 重定向路径
beforeEnter: route.beforeEnter, // 进入前的钩子函数
meta: route.meta || {}, // 元信息
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props } // 路由组件的属性
}
**// 如果路由配置中存在子路由,则递归调用 addRouteRecord 处理子路由**
if (route.children) { **// 路由有Children时(嵌套路由)**
if (process.env.NODE_ENV !== 'production') {
if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
warn(
false,
`Named Route '${route.name}' has a default child route. ` +
`When navigating to this named route (:to="{name: '${route.name}'"), ` +
`the default child route will not be rendered. Remove the name from ` +
`this route and use the name of the default child route for named ` +
`links instead.`
)
}
}
route.children.forEach(child => { **// 遍历Children,递归调用addRouteRecord方法**
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, **child**, record, childMatchAs)
})
}
// 处理路由的别名,并将别名对应的路由也加入到pathMap和nameMap中(非主线逻辑)
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias)
? route.alias
: [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/'
)
})
}
// 将当前路由记录添加到pathMap中
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 如果存在路由名称,则将当前路由记录添加到 nameMap 中
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
只看几个关键逻辑,首先创建 RouteRecord 的代码如下:
const record: RouteRecord = { // 参数
path: normalizedPath, // 规范化后的路径,通过 normalizePath 函数生成
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 路径对应的正则表达式,通过 compileRouteRegex 函数生成
components: route.components || { default: route.component }, // 路由组件,可以是一个对象,也可以是一个组件名称
instances: {}, // 路由实例
name, // 路由名称
parent, // 父级路由记录
matchAs, // 匹配别名
redirect: route.redirect, // 重定向路径
beforeEnter: route.beforeEnter, // 进入路由前的钩子函数
meta: route.meta || {}, // 路由元信息,例如标题、描述等
props: route.props == null // 路由组件的 props
? {}
: route.components
? route.props
: { default: route.props }
}
这里要注意几个点:
path 是规范化后的路径,它会根据parent的path做计算;regex是一个正则表达式的扩展,它利用了path-to-regexp这个工具库,把path解析成一个正则表达式的扩展,举个例子:
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
components是一个对象,通常我们在配置中写的component实际上这里会被转换成 {components: route.component};
instances表示组件的实例,也是一个对象类型;
parent表示父的RouteRecord,因为我们配置的时候可能会配置子路由,所以整个RouteRecord也就是一个树型结构
if (route.children) { // 当前路由配置中存在子路由,就会遍历每一个子路由,并对其调用addRouteRecord 函数,以便添加到路由记录中
// ...
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) // 递归**
})
}
如果配置了children,那么递归执行addRouteRecord方法,并把当前的record作为parent传入,通过这样的深度遍历,我们就可以拿到一个route下的完整记录。
if (!pathMap[record.path]) { // 检查pathMap中是否已经存在了相同路径的路由记录
pathList.push(record.path) // 当前路由记录的路径record.path添加到pathList中。pathList 是一个数组,用于存储所有路由记录的路径
pathMap[record.path] = record // 当前路由记录record添加到pathMap中,以路径record.path为键。这样就可以通过路径快速查找对应的路由记录
}
为 pathList 和 pathMap 各添加一条记录。
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
// ...
}
如果我们在路由配置中配置了 name,则给nameMap添加一条记录。 由于pathList、pathMap、nameMap都是引用类型,所以在遍历整个routes过程中去执行 addRouteRecord方法,会不断给他们添加数据。
那么经过整个createRouteMap方法的执行,我们得到的就是 pathList、pathMap和nameMap。其中 pathList 是为了记录路由配置中的所有path,而pathMap和nameMap都是为了通过path和name能快速查到对应的RouteRecord。 再回到 createMatcher 函数,接下来就定义了一系列方法,最后返回了一个对象。
return {
match,
addRoutes
}
也就是说,matcher是一个对象,它对外暴露了match和addRoutes方法。
3.2 addRoutes
addRoutes方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由(例如服务端下发或者动态添加),所以Vue-Router也提供了这一接口:
function addRoutes (routes) {
// 接受一个路由配置数组作为参数,并调用了之前提到的createRouteMap函数来更新路由映射表
createRouteMap(routes, pathList, pathMap, nameMap)
}
addRoutes的方法十分简单,再次调用createRouteMap即可,传入新的routes配置,由于pathList、pathMap、nameMap 都是引用类型,执行addRoutes后会修改它们的值。
持续更新中...