四、路径切换和导航守卫
vue-router在全局mixin混入的beforeCreate会执行,init方法,init方法会执行history.transitionTo进行路由的跳转,同样当执行this.$router.push方法,也同样会去执行transitionTo。transitionTo函数首先用current变量定义了当前的route,然后通过this.pending保留传入的路径。然后保留了,route.matched.length - 1和current.matched.length - 1,route.matched在createRoute函数中的定义是 record ? formatMatch(record) : [],formatMatch函数的目的是创建一个res数组,把当前的record,unshift到res这个数组中,如果发现当前的record有parent,那么会递归执行,也就是说matched保留了当前route的record和他所有的父路由的record。之后会做一个判断。首先通过isSameRoute(route, current)来判断当前的route和传入的route是否是相同,然后比较刚才保留的length是否相同,以及他们对应的最后一位的record是否相同,如果相同的话抛出异常,然后通过resolveQueue( this.current.matched, route.matched )拿到要触发updated, deactivated, activated生命周期的路由,resolveQueue函数接收两个参数,当前的route的matched和要跳转路由的matched,通过for循环比对路由在哪个index发生了不同,以此index,划分出哪些route是更新的,哪些是销毁的,些是创建的。然后创建了queue来记录类型为NavigationGuard的数组,在types/router.d.ts下可以看到,NavigationGuard是接受了to,form,next三个参数的函数,这其实和router.beforeEach这样的导航守卫钩子传入的函数是相同的。之后定义了iterator这样一个迭代器函数,最后执行了runQueue这个函数
// src/history/base.js
export class History {
...
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
// catch redirect option https://github.com/vuejs/vue-router/issues/3201
try {
route = this.router.match(location, this.current)
} catch (e) {
this.errorCbs.forEach(cb => {
cb(e)
})
// Exception should still be thrown
throw e
}
const prev = this.current
this.confirmTransition(
route,
...
)
}
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
this.pending = route
const abort = err => {
...
}
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
this.ensureURL()
return abort(createNavigationDuplicatedError(current, route))
}
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
const queue: Array<?NavigationGuard> = ...
const iterator = (hook: NavigationGuard, next) => {
...
}
runQueue(queue, iterator,...)
}
}
...
runQueue函数中定义了step为一个匿名函数,这个匿名函数接受一个index,如果index>= queue.length也就是队列中的全部执行完,会执行传入的cb回调函数。否则,根据传入的当前index,把queue中的数据作为参数,去执行传入的fn函数,fn函数执行完毕,让index+1,再去执行step,这是比较典型的一个队列的实现。
// src/util/async.js
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
runQueue的第二个参数传入的是之前提到的iterator,iterator的第一个参数是传的的queue[index],也就是NavigationGuard类型的函数,对应到我们导航守卫钩子beforeEach的三个参数,to,form和next,当我们调用beforeEach的next函数,那么会触发iterator中定义的的next函数,如果执行没有问题,那么最终会调用hook函数的next(to),也就是iterator传入的第二个参数next,也就是runQueue函数中的fn的第二个参数, step(index + 1),所以在导航守卫中为什么一定需要执行next,是因为如果不执行next,那么就不会执行queue数组之后数据的处理。
// src/history/base.js
const iterator = (hook: NavigationGuard, next) => {
...
try {
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
queue通过类型Array<?NavigationGuard>可以判断出他是一个导航守卫钩子组成的数组,那么queue是如何生成的,他首先调用了extractLeaveGuards函数,把deactivated也就是要销毁的record队列传入,extractLeaveGuards函数返回extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)是一个函数数组,vue-router的官方文档是这样描述导航守卫的
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)
extractGuards中文意思是解析守卫,首先会调用flatMapComponents函数,flatMapComponents函数第一个参数传入records,第二个参数传入函数,flatMapComponents函数会拿到records进行map操作,取到records中的components进行遍历,执行fn(m.components[key],m.instances[key],m,key),components[key]就是命名视图中的具体component,m.instances[key]是具体的import的对象,m就是当前的record,key就是components定义的key(默认为default)。flatMapComponents的第二个参数是一个函数,这个函数先调用了extractGuard,extractGuard函数会判断传入的def,也就是import的内容,判断他是否是一个函数,如果不是函数会通过_Vue.extend把他扩展成一个构造函数,然后通过传入的name,去尝试拿到他定义的生命周期函数,在extractLeaveGuards函数中,name是beforeRouteLeave也就是说,他会尝试拿到deactivatedrecord数组中的beforeRouteLeave这个生命周期,如果拿到的话,那么会去执行bind(guard, instance, match, key),在extractLeaveGuards中,bind是定义的bindGuard函数,bindGuard函数首先会判断是否有instance也就是VueComponent,如果有,那么会返回boundRouteGuard函数。flatMapComponents函数执行完,最终返回这样的拥有boundRouteGuard函数的一维数组,在extractGuards最后,因为extractLeaveGuards执行extractGuards传入的第四个参数reverse为true,所以会return guards.reverse(),deactivatedrecord数组的顺序是先父后子,所以最终reverse之后,extractLeaveGuards的顺序是先子后父,也就是说queue执行的时候会先触发子组件的beforeRouteLeave,然后再触发父组件的beforeRouteLeave,这样就如同官方的描述相同
在触发导航守卫,接着会在失活的组件里调用 beforeRouteLeave 守卫,对应的就是queue数组中的第一个值extractLeaveGuards(deactivated)
// src/history/base.js
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
const guards = flatMapComponents(records, (def, instance, match, key) => {
const guard = extractGuard(def, name)
if (guard) {
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
})
return flatten(reverse ? guards.reverse() : guards)
}
function extractGuard (
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {
if (typeof def !== 'function') {
// extend now so that global mixins are applied.
def = _Vue.extend(def)
}
return def.options[key]
}
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
// src/util/resolve-components.js
export function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {
return flatten(matched.map(m => {
return Object.keys(m.components).map(key => fn(
m.components[key],
m.instances[key],
m, key
))
}))
}
queue的第二个值是this.router.beforeHooks,VueRouter实例方法beforeEach会执行registerHook(this.beforeHooks, fn),registerHook函数会往传入的this.beforeHooks数组push传入的回调函数。也就是定义的beforeEach函数的回调,作为了queue的第二个值,这也对应了3. 调用全局的 beforeEach 守卫。。queue的第三个值extractUpdateHooks(updated)和第一个值 extractLeaveGuards(deactivated)的逻辑几乎是相同的,他去寻找的是updatedrecord数组中,带有beforeRouteUpdate生命周期的record也就是对应4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。第四个值 activated.map(m => m.beforeEnter),也就是对新创建的record执行beforeEnter,对应5. 在路由配置里调用 beforeEnter。第五个值resolveAsyncComponents(activated),resolveAsyncComponents函数也是返回了一个导航守卫的函数,返回的这个函数也是通过flatMapComponents,他会遍历record所有的components,去判断 if (typeof def === 'function' && def.cid === undefined)这说明这是一个异步组件,执行的逻辑和之前提到的异步组件实现的原理是相同的。执行完resolve之后,他会重新定义给match.components[key],最终全部执行完成后,会执行next,也就是说这个next是异步的,因为他可能会遇到解析懒加载的组件,这也对应了6. 解析异步路由组件。
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
return (to, from, next) => {
let hasAsync = false
let pending = 0
let error = null
flatMapComponents(matched, (def, _, match, key) => {
// if it's a function and doesn't have cid attached,
// assume it's an async component resolve function.
// we are not using Vue's default async resolving mechanism because
// we want to halt the navigation until the incoming component has been
// resolved.
if (typeof def === 'function' && def.cid === undefined) {
hasAsync = true
pending++
const resolve = once(resolvedDef => {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default
}
// save resolved on async factory in case it's used elsewhere
def.resolved = typeof resolvedDef === 'function'
? resolvedDef
: _Vue.extend(resolvedDef)
match.components[key] = resolvedDef
pending--
if (pending <= 0) {
next()
}
})
const reject = once(reason => {
const msg = `Failed to resolve async component ${key}: ${reason}`
process.env.NODE_ENV !== 'production' && warn(false, msg)
if (!error) {
error = isError(reason)
? reason
: new Error(msg)
next(error)
}
})
let res
try {
res = def(resolve, reject)
} catch (e) {
reject(e)
}
if (res) {
if (typeof res.then === 'function') {
res.then(resolve, reject)
} else {
// new syntax in Vue 2.3
const comp = res.component
if (comp && typeof comp.then === 'function') {
comp.then(resolve, reject)
}
}
}
}
})
if (!hasAsync) next()
}
}