本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
源码专栏
感谢大家继续阅读《Vue Router 4 源码探索系列》专栏,你可以在下面找到往期文章:
开场
哈喽大咖好,我是跑手,本次给大家继续探讨vue-router@4.x
源码中有关导航守卫中,组件内守卫部分内容。假如你想更全面了解整个导航守卫的来龙去脉,建议你先阅读《导航守卫该如何设计(一)》这篇文章。
可获得的增益
在这章节中,你可以更系统并全面学习vue router的路由拦截模式和守卫设计模式,并可获得以下增益:
- 全面了解导航守卫核心源码;
- 掌握导航守卫设计模式;
- 组件内守卫的执行过程;
知识回顾
我们先回顾下vue-router@4.x
中导航守卫的基本概念。
导航守卫分类
总的来讲,vue-router@4.x
的导航守卫可以分三大类:
- 全局守卫:挂载在全局路由实例上,每个导航更新时都会触发。
- 路由独享守卫:挂载在路由配置表上,当指定路由进入时触发。
- 组件内守卫:定义在vue组件中,当加载或更新指定组件时触发。
完整的导航解析流程
解析:
- 首先是
vue-router
的history监听器监致使导航被触发,触发形式包括但不局限于router.push
、router.replace
、router.go
等等。 - 调用全局的
beforeEach
守卫,开启守卫第一道拦截。 - 审视新组件,判断新旧组件一致时(一般调用replace方法),先执行步骤2,再调用组件级钩子
beforeRouteUpdate
拦截。 - 若新旧组件不一致时,先执行步骤2,再调用路由配置表中的
beforeEnter
钩子进行拦截。 - 接下来在组件
beforeCreate
周期调用组件级beforeRouteEnter
钩子,在组件渲染前拦截。 - 执行解析守卫
beforeResolve
。 - 在导航被确认后,就是组件的
this
对象生成后,可以使用全局的afterEach
钩子拦截。 - 触发 DOM 更新。
- 销毁组件前(执行unmounted),会调用
beforeRouteLeave
守卫进行拦截。
本章讨论的组件内守卫,涉及上面相关步骤3、5、9。
源码解析
执行机制
在上篇文章讲到,vue-router
中所有守卫执行过程都遵循以下原则:
- 先通过 guardToPromiseFn 方法将回调方法封装成Promise,以便后续链式调用。
- 再执行 runGuardQueue 方法,调用步骤1生产的Promise,完成导航守卫拦截。
组件内守卫与其他守卫的区别
虽说都是通过上述2步完成守卫拦截,但是在封装Promise
前,组件内守卫和其他守卫还是有所区别。这在于,guardToPromiseFn
接受的第一个参数(自定义的回调逻辑),组件内守卫必须先调用extractComponentsGuards
将定义在vue
组件内的守卫钩子提取出来,而其他守卫则省去这步。
extractComponentsGuards
- 定义:提取
vue
组件内的守卫钩子函数。 - 入参(4个,必填项):
- matched:(从
to
和form
提取出来的路由记录,为以下三者之一:leavingRecords
「即将离开的路由」、updatingRecords
「即将更新的路由」、enteringRecords
「即将进入的路由」) - guardType:守卫类型(
beforeRouteEnter
或beforeRouteUpdate
或beforeRouteLeave
) - to:进入的路由
- from:离开的路由
- matched:(从
- 返回:守卫钩子函数
执行过程:
函数内有2层for循环,外层循环是对入参matched
的遍历,主要作用保证leavingRecords
, updatingRecords
和 enteringRecords
的所有record都得到处理;
内层循环对某record里面所有组件遍历,在这个循环中:
-
在开发环境下,程序帮忙做了一些容错措施。先对组件类型合法性判断,不为
object
或function
类型则直接退出,避免页面崩溃;接下来是对import('./component.vue')
方式引入的组件,会使用Promise函数包装;最后, 程序对defineAsyncComponent()
方式定义的组件打标签,方便后续的区分处理。 -
接下来是守卫类型的判断,因为在3个组件内导航守卫中,只有
beforeRouteEnter
允许在组件mounted之前执行,当出现另外2个并且组件未挂载好时,要终止守卫的插入; -
然后读取同步组件和异步组件守卫钩子,对里面的守卫逻辑进行提取,值得注意的是这两部分代码都支持了
vue-class-component
方式构建的组件; -
在同步组件中,先根据
guardType
读取要提取的导航,再通过guardToPromiseFn
方法直接转化成Promise
调用链放到守卫列表中保存; -
在异步组件中,当组件加载完成(即组件函数
Promise
状态为fulfilled
时),按步骤4的逻辑再执行一遍得到最终的调用链。关于Promise.resolve
出来的结果如下图: -
返回整个守卫列表,流程结束。
源码:
export function extractComponentsGuards(
matched: RouteRecordNormalized[],
guardType: GuardType,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
) {
const guards: Array<() => Promise<void>> = []
for (const record of matched) {
// 为空组件则提示警告
if (__DEV__ && !record.components && !record.children.length) {
warn(
`Record with path "${record.path}" is either missing a "component(s)"` +
` or "children" property.`
)
}
for (const name in record.components) {
let rawComponent = record.components[name]
// 对组件类型合法性判断,不为object或function类型则直接退出
if (__DEV__) {
if (
!rawComponent ||
(typeof rawComponent !== 'object' &&
typeof rawComponent !== 'function')
) {
warn(
`Component "${name}" in record with path "${record.path}" is not` +
` a valid component. Received "${String(rawComponent)}".`
)
// throw to ensure we stop here but warn to ensure the message isn't
// missed by the user
throw new Error('Invalid route component')
}
// 假如通过 import('./component.vue') 方式引入的,会自动转换成Promise函数组件
else if ('then' in rawComponent) {
// warn if user wrote import('/component.vue') instead of () =>
// import('./component.vue')
warn(
`Component "${name}" in record with path "${record.path}" is a ` +
`Promise instead of a function that returns a Promise. Did you ` +
`write "import('./MyPage.vue')" instead of ` +
`"() => import('./MyPage.vue')" ? This will break in ` +
`production if not fixed.`
)
const promise = rawComponent
rawComponent = () => promise
}
// 控制台提示 defineAsyncComponent()方式定义的组件
else if (
(rawComponent as any).__asyncLoader &&
// warn only once per component
!(rawComponent as any).__warnedDefineAsync
) {
;(rawComponent as any).__warnedDefineAsync = true
warn(
`Component "${name}" in record with path "${record.path}" is defined ` +
`using "defineAsyncComponent()". ` +
`Write "() => import('./MyPage.vue')" instead of ` +
`"defineAsyncComponent(() => import('./MyPage.vue'))".`
)
}
}
// 当遇到`beforeRouteUpdate`或`beforeRouteLeave`钩子时,检查组件没有挂载则终止
// record.instances会在组件mounted周期进行赋值
if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue
// 再次判断组件类型,这里同时支持 class component方式构建的组件
if (isRouteComponent(rawComponent)) {
// __vccOpts is added by vue-class-component and contain the regular options
const options: ComponentOptions =
(rawComponent as any).__vccOpts || rawComponent
const guard = options[guardType]
// 构建守卫的Promise链
guard && guards.push(guardToPromiseFn(guard, to, from, record, name))
}
// 对于异步加载的组件的守卫提取,这里同时支持异步 class component方式构建的组件
else {
// start requesting the chunk already
let componentPromise: Promise<
RouteComponent | null | undefined | void
> = (rawComponent as Lazy<RouteComponent>)()
// 当使用函数组件并且没有返回Promise时,要提醒必须在组件里添加displayName,否则会在构建后运行出错
if (__DEV__ && !('catch' in componentPromise)) {
warn(
`Component "${name}" in record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.`
)
// 开发环境下,用Promise做封装处理
componentPromise = Promise.resolve(componentPromise as RouteComponent)
}
// 提取组件内导航守卫到guards数组
guards.push(() =>
componentPromise.then(resolved => {
if (!resolved)
return Promise.reject(
new Error(
`Couldn't resolve component "${name}" at "${record.path}"`
)
)
const resolvedComponent = isESModule(resolved)
? resolved.default
: resolved
// replace the function with the resolved component
// cannot be null or undefined because we went into the for loop
record.components![name] = resolvedComponent
// 兼容class component
const options: ComponentOptions =
(resolvedComponent as any).__vccOpts || resolvedComponent
const guard = options[guardType]
// 构建守卫的Promise链并返回
return guard && guardToPromiseFn(guard, to, from, record, name)()
})
)
}
}
}
return guards
}
在完成守卫提取后,接下来就是执行里面逻辑了。
beforeRouteUpdate
源码:
.then(() => {
// check in components beforeRouteUpdate
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
beforeRouteEnter
源码:
.then(() => {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
beforeRouteLeave
源码:
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
let guards: Lazy<any>[]
const [leavingRecords, updatingRecords, enteringRecords] =
extractChangingRecords(to, from)
// `navigate`方法会在路由跳转时执行,因此`beforeRouteLeave`守卫得优先执行。下面的`guards`数组会保存`beforeRouteLeave`守卫回调逻辑
guards = extractComponentsGuards(
leavingRecords.reverse(),
'beforeRouteLeave',
to,
from
)
// leavingRecords is already reversed
for (const record of leavingRecords) {
record.leaveGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
null,
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeRouteLeave guards
return (
// 执行`beforeRouteLeave`守卫
runGuardQueue(guards)
// ...
)
}
落幕
到此,所有的导航守卫研读完毕,最后感谢大家阅览并欢迎纠错,欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹