VueRouter

VueRouter

  1. 在Vue组件中注册VueRouter

    1
    Vue.use(VueRouter)
  2. 定义(路由)组件

    定义创建路由配置信息时所需要用到的组件。

  3. 创建VueRouter实例,并且配置路由匹配信息以及其他配置信息

    1
    2
    3
    4
    5
    let router = new VueRouter({
    routes: [
    {path: '', name: '', component: componentName}
    ]
    })
  4. Vue实例中挂载VueRouter实例

    1
    2
    3
    new Vue({
    router
    })

路由注册

1. Vue.use(VueRouter)

只有在new Vue实例时,传入routers配置执行install才是有意义的。

Vue.use方法调用的是VueRouter中的install方法。VueRouter中install方法利用了Vue.mixin混入beforeCreate和destroyed钩子函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Vue.mixin({
beforeCreate () {
if (this.$options.router !== undefined) {
// 根组件Vue实例
this._routerRoot = this
// 注册路由相关信息:$route, $router等
this._router = this.$options.router
// this._router实际是一个VueRouter实例
this._router.init(this)
// 将_route变成一个响应式的
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根组件
this._routerRoot = (this.$parent || this.$parent._routerRoot) || this
}
// 调用registerInstance方法,此方法会在RouterView组件中使用
registerInstance(this, this)
},
destroyed () {
// 销毁路由
registerInstance(this)
}
})

在Vue原型上挂载了$router, $route属性;注册了全局组件-RouterView和RouterLink;定义路由导航的合并策略-与vue的created钩子函数合并规则一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 挂载$router
Object.defineProperty(Vue.prototype, '$router', {
get () {
return this._routerRoot._router
}
})
// 挂载$route
Object.defineProperty(Vue.prototype, '$route', {
get () {
return this._routerRoot._route
}
})
// 注册全局组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 定义合并策略
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created

总结:

  1. Vue编写插件的时候需要提供静态的install方法
  2. VueRouter的install方法会给每个组件注入beforeCreate和destroyed钩子函数,在beforeCreate进行一些私有属性定义和路由初始化。

2. VueRouter、路由初始化

  1. init方法执行transitionTo方法做路由过渡
  2. 执行router.macth方法,此方法中执行matcher.match

路由初始化

1. 实例化VueRouter

new VueRouter会进入VueRouter类中的constructor构造器中。构造器接受一个配置项信息options对象,包含了路由配置信息routes,路由模式mode,降级处理fallback等。

  • fallback:降级处理,对于路由模式的降级处理,如果不支持history就会降级到hash模式。
  • 路由模式hash,history,abstract分别对应不同的类HashHistory,Html5History,AbstractHistory。这三个类都继承于history类。
1
2
3
4
5
6
7
8
class VueRouter {
constructor (options) {
...
// options.routes就是实例化VueRouter传递参数中的routes路由配置信息
this.matcher = createMatcher(options.routes || [], this)
// ...路由模式配置以及根据路由模式来创建对应的history实例
}
}

创建路由映射表,方便以后根据path或者name查找到想要的record。并且对外提供了addRoutes(routes)方法,方便动态添加routes信息。

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
function createMatcher (routes, router) {
const {pathList, pathMap, nameMap} = createRouteMap(routes)

function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}

// 对外提供一个addRoutes方法,方便自己动态添加routes信息
return {match, addRoutes}
}

// 创建路由映射表
function createRouteMap (routes, oldPathList, oldPathMap, oladNameMap) {
const pathList = oldPathList || []
const pathMap = oldPathMap || Object.create(null)
const nameMap = oldNameMap || Object.create(null)

// 对路由配置进行一个遍历:添加路由记录信息
routes.forEach(route => {
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}
}

递归实例化VueRouter时传递的路由信息配置routes

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
function addRouteRecord (pathList, pathMap, nameMap, route, parent, matchAs) {
// 获取路由配置信息中配置的path和name,path不能为空
const {path, name} = route
// route.component必须是一个组件
// routes中配置 编译正则
const pathToRegexpoptions = route.pathToRegexpOptions || {}

// 对路径path的一个处理以及对路径path拼接-parent.path/path
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

const record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || {default: route.component},
instance: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null ? {} : route.components ? route.props : {default: route.props}
}
// 遍历route中的children,递归添加addRouteRecord
if (route.children) {
route.children.forEach(child => {
const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}

// ... 对alias路由别名的处理

// 递归上面的addRouteRecord后一直到route没有children子节点会执行下面
// 会先push最底层的children,然后一层一层往上push【因为递归是从外向内,这个方法就会从内向外】
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}

// 如果也配置了name,会将record也保存在nameMap中
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
}

上面初始化一系列pathList,pathMap以及nameMap后,是为了方面后期的查找match【初始化开始beforeCreate=>init=>transitionTo=>router,match】,详情见下文。

2. 初始化开始:组件初始化阶段的beforeCreate-调用混入的router.init

Vue.use(VueRouter)之后混入了beforeCreate钩子函数执行时,会调用this._route.init(this)也就是调用VueRouter中的init方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// VueRouter类中,app其实就是一个vue组件实例
init (app) {
this.app.push(app)
// 确保后面的逻辑只执行一次
if (this.app) return
this.app = app
const history = this.history
if (history instanceof HTML5History) {
// transitionTo:路由跳转
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(history.getCurrentLocation(), setupHashListener, setupHashListener)
}

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

transitionTo进行路由跳转

1
2
3
4
5
transitionTo (location, onComplete) {
// 获取当前路径
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {...})
}

获取当前路径this.router.match指向的是VueRouter中的match函数,match本质指向Matcher中的match函数。此match函数中利用到前面初始化的pathList,pathMap,nameMap数据。

1
2
3
4
// VueRouter类中的match函数
match (raawLocation, current, redirectedFrom) {
return this.matcher.match(raawLocation, current, redirectedFrom)
}

Matcher中match函数,利用到前面初始化的pathList,pathMap,nameMap数据。此函数的作用:根据传递的location和当前route计算得出当前的路由。

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
// Matcher中的match函数
function match (raw, currentRoute, redirectedFrom) {
// 处理当前的路径等信息得到当前的path,query,hash以及_normalized数据信息
// 或者得到的是name,params,_normalized
const location = normalizeLocatin(raw, currentRoute, false, router)
const {name} = location

if (name) {
const record = nameMap[name]
if (!record) return _createRoute(null, location)
// 对于params参数进行处理
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.includes(key)) {
location.params[key] = currentRoute.params[key]
}
}
}
return _createRoute(record, location, redirectedFrom)
// 对于没有name属性,有path属性而言
} else if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
// 判断location.path是否匹配record.regex正则,并且如果有params会对params进行处理。
if (matchRoute(record.regex, location.path, location, params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
return _createRoute(null, location)
}

会执行 _createRoute 函数,创建一个路由路径route。_createRoute会对record路径进行一系列的处理后,调用真正创建路由route的createRoute方法。

1
2
3
4
5
6
7
8
9
function _createRoute (record, location, redirectedFrom) {
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)
}

createRoute创建路由,location指的是当前路径解析得到的数据对象,record则是指初始化路由routes时所保存初始的route配置相关信息,router是VueRoute实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createRoute (record, location, redirectedFrom, router) {
const stringifyQuery = router && router.options.stringifyQuery
let query = location.query || {}
try {
query = clone(query)
} catch(e) {}

const route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}

if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
// 避免对route进行修改
return Object.freeze(route)
}

总结:

  1. createMatcher的初始化就是根据路由的配置描述routes创建映射表(一个含有pathList,pathMap,nameMap对象),包含路径、名称到路由record的映射关系。
  2. match方法会根据传入的位置和路径计算出新的位置,并匹配到对应的路由route,然后根据新的位置和record创建新的路径route并返回。

路由切换

路由切换关系到导航守卫管理,URL变化,路由组件渲染等。路由切换主要就是上面有提到过的transitionTo,而transitionTo这个方法除了在初始化的时候有触发,也在VueRoute实例router的push和replace方法触发(HTML5History和HashHistory的push和replace方法)。

1. 开始:路由切换-transitionTo

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
function transitionTo (location, onComplete, onAbort) {
// 得到匹配的路由信息
const route = this.router.match(location, this.current)
// 完成路径的切换
this.confirmTransition(route, () => {
// 当前updateRoute方法中含有全局router.afterEachr执行
this.updateRoute(route)
// 执行push或者replace中传入的onComplete方法
onComplete && onComplete(route)
this.ensureURL()

// 执行注册的onReady方法
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
}, error => {

})
}


// updateRoute方法
function updateRoute (route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
// 全局router.afterEach
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}

真正路由切换confirmTransition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function confirmTransirionTo (route, onComplete, onAbort) {
// 当前路径,也就是form旧路径
const current = this.current
// 取消或者失败的方法
const abort = error => {
...
onAbort && onAbort(error)
}
// 对于当前路径form和要跳转的路径to
if (isSameRoute(route, current) && route.matched.length === current.matched.length) {
this.ensureURL() // 与URL变化相关的
return abort()
}
}

2. 导航守卫触发

在confirmTransition方法中,判断from和to不是相同路径后,会开始触发导航守卫

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
function confirmTransition (route, onComplete, onAbort) {
// ... 前期处理,current就是form旧路径
const current = this.current
const abort = error => {}

// 获取得到的updated是两个共同拥有的,deactived表示form所拥有的,active的表示to所拥有的
const {updated, deactivated, activated} = resolveQueue(this.current.matched, route.matched)

const queue = [].concat(
// 组件内的beforeRouteLeave
extractLeaveGuards(deactivated),
// 全局配置的router.beforeEach
this.router.beforeHooks,
// 组件内的beforeRouteUpdate
extractUpdateGuards(updated),
// 路由信息配置routes中的beforeEnter
activated.map(m => m.beforeEnter)
// async组件,异步组件
resolveAsyncComponents(activated)
)

this.pending = route
const iterator = (hook, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to) => {
// 前面判断进行容错
if (typeof to === 'string' ||
(typeof to === 'object' && (typeof to.path === 'string') || typeof to.name === 'string')) {
abort()
if (route.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
next(to)
}
})
} catch(error) {
abort(error)
}
}

//
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// 组件beforeRouteEnter
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
// 全局router.beforeResolve
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
// onComplete来源于transitionTo方法,此方法里面包含了updateRoute执行,并且updateRoute方法中含有
// 全局router.afterEach的执行
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
}

根据上面的代码逻辑,可以得到有关路由的守卫函数等的执行顺序是:组件beforeRouteLeave => 全局router.beforeEach => 组件beforeRouteUpdate => 路由配置routes的beforeEnter => 解析异步组件 => 组件beforeRouteEnter => 全局router.beforeResolve => 全局router.afterEach。