Vue Router Source Code Analysis (1) Generation of Internal Routing Configuration

I have encountered a problem these days, that is, when the vue-router nests subroutes, if the deep router-view component does not declare a key, and the component in the route configuration is a function-style component, it will cause the router.push to fail to update.

For function-based components and function-based programming, if you need it, remember my previous one.相关博客;

This article mainly analyzes the source code of vue-router. The source code of vue-router is mainly in two parts. The first part is to do route matching, and the second part is to do route jumping. This article mainly focuses on the first part

Let’s talk about it with version 3.6.5 of vue-router.

First, let’s take a look at the directory structure of 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
.
The two components provided by vue-router, namely router-view and router-link
│   ├── link.js
│   └── view.js
- Composables//I don't need to care about this directory for now
│   ├── globals.js
│   ├── guards.js
│   ├── index.js
│   ├── useLink.js
│   └── utils.js
🥰 ─ ─ Create-matcher.js//When initializing the route, create a matcher for subsequent route matching
- create-route-map.js//Create route-map for matcher when initializing route
- entries//package entry file
│   ├── cjs.js
│   └── esm.js
Hui ─ ─ history//Routing handover implementation in different routing modes
│ │ ─ ─ abstract.js//Routing switching in non-browser environment
│ │ ─ ─ base.js//The base class for route switching
│ │ ─ ─ hash.js//hash routing mode
│ │ ─ ─ html5.js//HTML5 mode
├── index.js
The file called by install.js//Vue.use
- router.js//Vue-router externally exposed 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

Simple example

Our following code explanation gives this simple example.

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. If using the Modularization mechanism to program, import Vue and VueRouter, to call Vue.use (VueRouter)

//1. Define (route) components.
Can be imported from other files
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

//2. Define routes
//Each route should map a component. Where "component" can be
//component constructor created by Vue.extend (),
//Or, just a component configuration object.
We will discuss nested routing later.
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]

//3. Create a router instance and pass the routes configuration
//You can also pass other configuration parameters, but let's make it simple for now.
const router = new VueRouter({
Routes//(abbreviation) Equivalent to routes: routes
})

Create and mount the root instance.
//Remember to inject routes through router configuration parameters.
//so that the entire application has routing functions
const app = new Vue({
router
}).$mount('#app')

Internal type

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) {
Prevent repeated installation
if (install.installed && _Vue = Vue) return
install.installed = true

Save the Vue instance and export it for internal use
_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)
}
}

//Global mix configuration, add hook function for each vue component
Vue.mixin({
beforeCreate () {
//If the router parameter is defined by itself, it will only appear in the case of the component, because the parameter we passed in when new Vue was mounted to #app
if (isDef(this.$options.router)) {
//_routerRoot point to the root component
this._routerRoot = this
this._router = this.$options.router
//Defined in router.js, see the analysis later
this._router.init(this)
//When entering here, this is the root vue instance, and this._route points to _router history.current
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
//Enter here to explain that it is not the root Vue component, then the _routerRoot of the subcomponent is the _routerRoot on the subcomponent
//There is no need for recursion to look up here because the construction of the Vue component tree is top-down, so you only need to look up one layer
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})

//_routerRoot point to the root Vue instance, this._routerRoot _router point to the vue-router instance passed in when new Vue
//So this. $router in each Vue component will find Vue.prototype. $router through the prototype chain, and will find the vue-router instance passed in when new Vue
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})

//_routerRoot point to the root Vue instance, then this._routerRoot _route points to _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

Above we mounted VueRouter through install.js. Let’s take a look at what this VueRouter is.

This VueRouter is imported via index.js:

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

export default VueRouter

‘./entries/cj’ reads as follows:

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

export default VueRouter

Then there is the content of 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 = []
//This is very important. The matcher constructs a route matcher based on the routes passed in when we new Router. We will talk about it later
this.matcher = createMatcher(options.routes || [], this)

//The mode passed in, the default is hash, the mode
let mode = options.mode || 'hash'
//supportsPushState indicates whether the current router supports the pushstate method, and only when it is supported can the history api be used.
this.fallback =
mode = 'history' && !supportsPushState && options.fallback ! false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode

//Take different routing history according to different modes
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

This function is actually the init method called in 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
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 actually supports multi-instance mode, but it is generally not used
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

//If it is HTML5History or HashHistory, call setupListeners of both
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

This function returns a Matcher with the following type declaration:

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>;
};

The first line of code: '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)

//traverse the incoming routes, here only need to traverse the first layer because addRouteRecord itself will perform recursion
//This code is a multi-source DFS traversal route configuration, and then generate a record for each route configuration, stored in pathList, pathMap, nameMap
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})

//put the route of * at the end
// 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.`
)
}

//The above content is mainly parameter detection, you can ignore it.

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

//format the path and concatenate the path of the child route with its parent route, such as/foo and/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 }
}

//if there are children nested routes, recursion calls itself
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)
})
}

//Store the calculated record in pathList and 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
)
}
}

//If there is a name attribute in the route configuration, add a mapping of name and 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}" }`
)
}
}
}