Appearance
第 8.2 节:Vue Router - 导航守卫与切换流程
在上一节,RouterMatcher(匹配器)解决了“去哪里”(resolve)的问题。本节,我们将探讨“导航守卫”(Navigation Guards)是如何解决“能不能去”和“如何去”这两个问题的。
导航守卫是 Vue Router 的流程控制系统。它允许我们在路由切换的关键节点“暂停”导航,执行检查,并决定是“继续”、“取消”还是“重定向”。
核心机制:“Promise 化的守卫队列”
router.push() 触发后,Vue Router 并不立即切换 URL 和组件。相反,它会:
- 调用
matcher.resolve确定“目标路由”to和“当前路由”from。 - 调用
Maps函数,创建一个“守卫任务队列”(guards数组)。 - 串行执行这个队列。
- 只有当队列中所有任务都“放行”后,才真正“提交”导航(更新 URL 和组件)。
1. guardToPromiseFn:守卫的 Promise 包装
next() 函数是守卫的核心控制机制。Vue Router 通过 guardToPromiseFn (navigationGuards.ts) 将所有守卫函数**“包装”成一个返回 Promise 的函数**。
typescript
// core/packages/router/src/navigationGuards.ts
export function guardToPromiseFn(
guard: NavigationGuard,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded,
// ...
): () => Promise<void> {
// 返回一个“待执行”的 Promise 函数
return () =>
new Promise((resolve, reject) => {
// 1. 【核心】
// 定义 next() 函数,它被传给“守卫”
const next: NavigationGuardNext = (
valid?: boolean | RouteLocationRaw | Error
) => {
if (valid === false) {
// 2. 【阻止】
// next(false) -> 拒绝 Promise (导航中止)
reject(
createRouterError(ErrorTypes.NAVIGATION_ABORTED, { from, to })
)
} else if (valid instanceof Error) {
// 3. 【错误】
// next(Error) -> 拒绝 Promise (导航出错)
reject(valid)
} else if (isRouteLocation(valid)) {
// 4. 【重定向】
// next('/login') -> 拒绝 Promise,
// 并标记为“重定向”
reject(
createRouterError(ErrorTypes.NAVIGATION_GUARD_REDIRECT, {
from: to,
to: valid, // 携带重定向目标
})
)
} else {
// 5. 【继续】
// next() -> 解决 Promise (本守卫通过)
resolve()
}
}
// 6. 执行“守卫”
// (Vue 3.x+ 中,如果守卫不带 next 参数,
// 或返回 Promise<true>,也会自动 resolve)
const guardReturn = guard.call(..., to, from, next)
// ... (省略自动调用 next() 的逻辑) ...
Promise.resolve(guardReturn).catch(reject)
})
}2. runGuardQueue:“串行”执行队列
Maps 函数将所有“Promise 化的守卫”(guardToPromiseFn) 按顺序放入 guards 数组。runGuardQueue 负责串行执行它们。
它的实现是 Promise 链的经典用法:
typescript
// core/packages/router/src/router.ts
function runGuardQueue(guards: Lazy<any>[]): Promise<void> {
// 从一个“已解决”的 Promise 开始,
// 依次 .then() 链式调用队列中的每一个守卫
return guards.reduce(
(promise, guard) => promise.then(() => guard()),
Promise.resolve()
)
}如果任何一个守卫 reject(调用了 next(false) 或 next('/login')),整个 Promise 链会立即中断,导航失败。
8.2.2 Maps:完整的守卫执行序列
Maps (router.ts) 函数是导航的主函数。它的职责就是**“按正确的顺序,组装守卫队列”**。
typescript
// core/packages/router/src/router.ts
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
// 1. 找出“离开”、“更新”、“进入”的路由记录
const [leavingRecords, updatingRecords, enteringRecords] =
extractChangingRecords(to, from)
let guards: Lazy<any>[] = [] // 守卫队列
// 【第 1 步】:组件“离开”守卫 (beforeRouteLeave)
guards = extractComponentsGuards(
leavingRecords.reverse(), // 从子到父
'beforeRouteLeave',
to,
from
)
// 【第 2 步】:全局“前置”守卫 (beforeEach)
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
// 【第 3 步】:组件“更新”守卫 (beforeRouteUpdate)
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
// 【第 4 步】:路由“独享”守卫 (beforeEnter)
for (const record of enteringRecords) {
if (record.beforeEnter) {
guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
// 【第 5 步】:解析“异步组件” (懒加载)
// (这一步被实现在了 extractComponentsGuards 内部)
// 【第 6 步】:组件“进入”守卫 (beforeRouteEnter)
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
// 【第 7 步】:全局“解析”守卫 (beforeResolve)
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
// 【开始执行队列】
return runGuardQueue(guards)
}8.2.3 关键步骤:解析“异步组件” (extractComponentsGuards)
在第 5 步和第 6 步之间,extractComponentsGuards (navigationGuards.ts) 增加了一个关键的异步步骤:解析“懒加载”组件。
typescript
// core/packages/router/src/navigationGuards.ts
export function extractComponentsGuards(
matched: RouteRecordNormalized[],
guardType: GuardType,
// ...
) {
const guards: Array<() => Promise<void>> = []
for (const record of matched) {
for (const name in record.components) { // 'default', 'sidebar'
let rawComponent = record.components[name]
if (isRouteComponent(rawComponent)) {
// 【路径 A:同步组件】
// 组件已加载,直接提取守卫 (beforeRouteLeave 等)
const guard = rawComponent[guardType]
guard && guards.push(guardToPromiseFn(guard, ...))
} else {
// 【路径 B:异步组件】
// component: () => import('./About.vue')
// rawComponent 是一个“加载函数”
// 【关键】
// 将“加载”这个动作本身,也包装成一个“守卫”
// 并 push 到队列中!
guards.push(() =>
// 1. 执行 import()
(rawComponent as Lazy<RouteComponent>)().then(resolved => {
// 2. 加载完成!
const resolvedComponent = isESModule(resolved)
? resolved.default
: resolved
// 3. 将加载到的组件,存回路由记录
record.components![name] = resolvedComponent
// 4. 【核心】
// 现在组件加载完了,
// “立即”提取它内部的守卫 (如 beforeRouteEnter)
// 并返回一个“执行它”的 Promise
const guard = resolvedComponent[guardType]
return guard && guardToPromiseFn(guard, ...]()
})
)
}
}
}
return guards
}runGuardQueue 在执行到“异步组件”这一步时,会自动 await 这个 import()。import() 完成后,它会立即提取并执行 beforeRouteEnter 守卫,然后再执行队列的下一步(beforeResolve)。
8.2.4 导航完成:finalizeNavigation (提交) 与 afterEach (后置)
如果 runGuardQueue(guards) 的 Promise 被 resolve(即导航成功),router.push() 的 .then() 会被触发,并调用 finalizeNavigation。
finalizeNavigation (提交导航)
finalizeNavigation (router.ts) 负责**“提交”**真正的状态变更:
typescript
function finalizeNavigation(
toLocation: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
// ...
) {
// ... (省略取消检查) ...
// 1. 【更新 URL】
// 调用 history.push() 或 history.replace()
// 这是真正改变浏览器地址栏的时刻
if (isPush) {
routerHistory.push(toLocation.fullPath, ...)
} else {
routerHistory.replace(toLocation.fullPath, ...)
}
// 2. 【更新 Vue 状态】
// currentRoute 是一个 ref,
// 更新它的 .value 会触发 Vue 的响应式更新,
// 从而导致 <RouterView> 重新渲染
currentRoute.value = toLocation
// 3. (处理滚动行为)
handleScroll(...)
}afterEach (后置钩子)
router.push() 的最后一步,是在 .then() 或 .catch()(无论成功、失败还是重定向)的 finally 块中,执行 afterEach 钩子。
typescript
// router.push() 调用的 navigate().then().catch().then()
.then(failure => {
// ... (finalizeNavigation 或 处理重定向) ...
// 【最后】
// afterEach 总是会被调用,
// 它接收“导航结果” (failure),
// 且它“不”能阻止导航(没有 next)
triggerAfterEach(toLocation, from, failure)
})总结
Vue Router 的导航守卫是一个有序的、基于 Promise 的流程控制系统:
Maps(主函数):当router.push被调用时,Maps负责按精确的顺序(Leave -> Global Each -> Update -> Enter -> Async -> Resolve)组装“守卫队列”(guards)。guardToPromiseFn(包装器):它将next()转换为Promise的resolve和reject,将回调式的next统一为Promise 式的控制流。- 异步组件:
import()被包装成队列中的一个异步步骤,确保导航会暂停,直到组件加载完毕。 runGuardQueue(执行器):通过Promise.reduce串行执行队列。任何reject(如next(false))都会立即中断整个导航。finalizeNavigation(提交):在队列全部成功后,才执行history.push()和currentRoute.value = to,完成状态切换。afterEach(后置钩子):在导航最终完成(无论成败)后被调用的“清理”钩子。
