Vue Router 源码解析(一)内部路由配置的生成

这几天遇到个问题,就是在vue-router嵌套子路由的时候,如果深层的router-view组件没有声明key,且路由配置中的component是函数式的组件,就会导致在router.push的时候无法更新。

对于函数式组件和函数式编程,需要了记得可以我之前的相关博客;

本文主要是分析vue-router的源码,vue-router的源码主要是两部分,第一部分是做路由匹配,第二部分是做路由跳转的,本文先主要关注第一部分

我们以3.6.5版本的的vue-router来讲

首先看一下src的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
.
├── components // vue-router提供的两个组件,即router-view和router-link
│   ├── link.js
│   └── view.js
├── composables // 暂时不需要关心这个目录
│   ├── globals.js
│   ├── guards.js
│   ├── index.js
│   ├── useLink.js
│   └── utils.js
├── create-matcher.js // 初始化路由的时候,创建matcher,用于后续进行路由匹配
├── create-route-map.js // 初始化路由时,创建route-map用于matcher
├── entries // 打包入口文件
│   ├── cjs.js
│   └── esm.js
├── history // 不同的路由模式下的路由切换实现
│   ├── abstract.js // 非浏览器环境下的路由切换
│   ├── base.js // 路由切换的基类
│   ├── hash.js // hash路由模式
│   └── html5.js // HTML5模式
├── index.js
├── install.js // Vue.use调用的文件
├── router.js // Vue-router对外暴露的api
└── util
├── async.js
├── dom.js
├── errors.js
├── location.js
├── misc.js
├── params.js
├── path.js
├── push-state.js
├── query.js
├── resolve-components.js
├── route.js
├── scroll.js
├── state-key.js
└── warn.js

简单的示例

我们下面的代码讲解给予这个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 0. 如果使用模块化机制编程,导入 Vue 和 VueRouter,要调用 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({
router
}).$mount('#app')

内部类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
declare var document: Document;

declare class RouteRegExp extends RegExp {
keys: Array<{ name: string, optional: boolean }>;
}

declare type PathToRegexpOptions = {
sensitive?: boolean,
strict?: boolean,
end?: boolean
}

declare module 'path-to-regexp' {
declare module.exports: {
(path: string, keys?: Array<?{ name: string }>, options?: PathToRegexpOptions): RouteRegExp;
compile: (path: string) => (params: Object) => string;
}
}

declare type Dictionary<T> = { [key: string]: T }

declare type NavigationGuard = (
to: Route,
from: Route,
next: (to?: RawLocation | false | Function | void) => void
) => any

declare type AfterNavigationHook = (to: Route, from: Route) => any

type Position = { x: number, y: number };
type PositionResult = Position | { selector: string, offset?: Position } | void;

declare type RouterOptions = {
routes?: Array<RouteConfig>;
mode?: string;
fallback?: boolean;
base?: string;
linkActiveClass?: string;
linkExactActiveClass?: string;
parseQuery?: (query: string) => Object;
stringifyQuery?: (query: Object) => string;
scrollBehavior?: (
to: Route,
from: Route,
savedPosition: ?Position
) => PositionResult | Promise<PositionResult>;
}

declare type RedirectOption = RawLocation | ((to: Route) => RawLocation)

declare type RouteConfig = {
path: string;
name?: string;
component?: any;
components?: Dictionary<any>;
redirect?: RedirectOption;
alias?: string | Array<string>;
children?: Array<RouteConfig>;
beforeEnter?: NavigationGuard;
meta?: any;
props?: boolean | Object | Function;
caseSensitive?: boolean;
pathToRegexpOptions?: PathToRegexpOptions;
}

declare type RouteRecord = {
path: string;
alias: Array<string>;
regex: RouteRegExp;
components: Dictionary<any>;
instances: Dictionary<any>;
enteredCbs: Dictionary<Array<Function>>;
name: ?string;
parent: ?RouteRecord;
redirect: ?RedirectOption;
matchAs: ?string;
beforeEnter: ?NavigationGuard;
meta: any;
props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}

declare type Location = {
_normalized?: boolean;
name?: string;
path?: string;
hash?: string;
query?: Dictionary<string>;
params?: Dictionary<string>;
append?: boolean;
replace?: boolean;
}

declare type RawLocation = string | Location

declare type Route = {
path: string;
name: ?string;
hash: string;
query: Dictionary<string>;
params: Dictionary<string>;
fullPath: string;
matched: Array<RouteRecord>;
redirectedFrom?: string;
meta?: any;
}

install.js 安装全局钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
// 防止重复安装
if (install.installed && _Vue === Vue) return
install.installed = true

// 保存Vue实例,并export出来供内部使用
_Vue = Vue

const isDef = v => v !== undefined

const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}

// 全局混入配置,为每个vue组件都添加钩子函数
Vue.mixin({
beforeCreate () {
// 如果本身定义了router参数,这里只会出现在跟组件的情况下,因为我们在new Vue并挂载到#app的时候传入的参数
if (isDef(this.$options.router)) {
// _routerRoot 指向根组件
this._routerRoot = this
this._router = this.$options.router
// 定义在router.js中,具体看后面的解析
this._router.init(this)
// 进入这里的时候,this是根vue实例,this._route指向的是this._router.history.current
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 进入这里说明不是根Vue组件,那么子组件的_routerRoot就是副组件上的_routerRoot
// 这里不需要递归向上寻找是因为Vue组件树的构建是自上而下的,所以只需要寻找上一层就好
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})

// _routerRoot都指向根Vue实例,则this._routerRoot._router指向的就是new Vue时传入的vue-router实例
// 所以每个Vue组件中的this.$router,会通过原型链找到Vue.prototype.$router,就会找到new Vue时传入的vue-router实例
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})

// _routerRoot都指向根Vue实例,则this._routerRoot._route指向的就是this._router.history.current
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})

Vue.component('RouterView', View)
Vue.component('RouterLink', Link)

const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

router.js

上面我们通过install.js挂载VueRouter,我们看一下这个VueRouter是什么。

这个VueRouter是通过import index.js引入的:

1
2
3
4
import VueRouter from './entries/cjs'

export default VueRouter

./entries/cj内容如下:

1
2
3
4
import VueRouter from '../router'

export default VueRouter

然后就是router.js的内容了:

constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
constructor (options: RouterOptions = {}) {
if (process.env.NODE_ENV !== 'production') {
warn(this instanceof VueRouter, `Router must be called with the new operator.`)
}
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 这里很关键,matcher根据我们new Router的时候传入的routes构造路由匹配器,我们后面再说
this.matcher = createMatcher(options.routes || [], this)

// 传入的mode,默认为hash,模式
let mode = options.mode || 'hash'
// supportsPushState 表示当前路由器是否支持pushstate方法,支持了才能使用history api
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
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}`)
}
}
}

init

这个函数其实就是install.js里面调用的init方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
init (app: any /* Vue component instance */) {
process.env.NODE_ENV !== 'production' &&
assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)

// vue-router实际上支持多实例的模式,不过一般用不到
this.apps.push(app)

// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null

if (!this.app) this.history.teardown()
})

// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}

this.app = app

const history = this.history

// 如果是HTML5History或者HashHistory,调用二者的setupListeners
if (history instanceof HTML5History || history instanceof HashHistory) {
const handleInitialScroll = routeOrError => {
const from = history.current
const expectScroll = this.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll

if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError)
}
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}

history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}

create-matcher.js

这个函数返回了一个Matcher,它的类型声明如下:

1
2
3
4
5
6
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
getRoutes: () => Array<RouteRecord>;
};

代码第一行:const { pathList, pathMap, nameMap } = createRouteMap(routes)

create-route-map.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>,
parentRoute?: RouteRecord
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// the path list is used to control path matching priority
const pathList: Array<string> = oldPathList || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

// 遍历传入的routes,这里只需要遍历第一层是因为addRouteRecord本身就会进行递归
// 这段代码就是一个多源DFS遍历路由配置,然后为每一个路由配置生成record,存储进pathList,pathMap,nameMap中
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})

// 把 * 的路由放在最后
// ensure wildcard routes are always at the end
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}

if (process.env.NODE_ENV === 'development') {
// warn if routes do not include leading slashes
const found = pathList
// check for missing leading slash
.filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')

if (found.length > 0) {
const pathNames = found.map(path => `- ${path}`).join('\n')
warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)
}
}

return {
pathList,
pathMap,
nameMap
}
}

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.`
)

warn(
// eslint-disable-next-line no-control-regex
!/[^\u0000-\u007F]+/.test(path),
`Route with path "${path}" contains unencoded characters, make sure ` +
`your path is correctly encoded before passing it to the router. Use ` +
`encodeURI to encode static segments of your path.`
)
}

// 上面的内容主要是参数检测,可以不看

const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}

// 格式化路径,并且把子路由的路径和它的父级路由拼接,比如把/foo和/bar拼接起来
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}

const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
alias: route.alias
? typeof route.alias === 'string'
? [route.alias]
: route.alias
: [],
instances: {},
enteredCbs: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}

// 如果有children嵌套路由,递归调用本身
if (route.children) {
// Warn if route is named, does not redirect and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
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 => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}

// 将计算出来的record存储到pathList和pathMap中
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}

if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
for (let i = 0; i < aliases.length; ++i) {
const alias = aliases[i]
if (process.env.NODE_ENV !== 'production' && alias === path) {
warn(
false,
`Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
)
// skip in dev to make it work
continue
}

const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
}
}

// 如果路由配置中有name属性,添加name和routeConfig的映射
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}" }`
)
}
}
}