Skip to content

第 8.2 节:Vue Router - 导航守卫与切换流程

在上一节,RouterMatcher(匹配器)解决了“去哪里”(resolve)的问题。本节,我们将探讨“导航守卫”(Navigation Guards)是如何解决“能不能去”和“如何去”这两个问题的。

导航守卫是 Vue Router 的流程控制系统。它允许我们在路由切换的关键节点“暂停”导航,执行检查,并决定是“继续”、“取消”还是“重定向”。


核心机制:“Promise 化的守卫队列”

router.push() 触发后,Vue Router 并不立即切换 URL 和组件。相反,它会:

  1. 调用 matcher.resolve 确定“目标路由” to 和“当前路由” from
  2. 调用 Maps 函数,创建一个“守卫任务队列”(guards 数组)。
  3. 串行执行这个队列。
  4. 只有当队列中所有任务都“放行”后,才真正“提交”导航(更新 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)Promiseresolve(即导航成功),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 的流程控制系统:

  1. Maps (主函数):当 router.push 被调用时,Maps 负责按精确的顺序(Leave -> Global Each -> Update -> Enter -> Async -> Resolve)组装“守卫队列”(guards)。
  2. guardToPromiseFn (包装器):它将 next() 转换为 Promiseresolvereject,将回调式next 统一为Promise 式的控制流。
  3. 异步组件import()包装成队列中的一个异步步骤,确保导航会暂停,直到组件加载完毕。
  4. runGuardQueue (执行器):通过 Promise.reduce 串行执行队列。任何 reject(如 next(false))都会立即中断整个导航。
  5. finalizeNavigation (提交):在队列全部成功后,才执行 history.push()currentRoute.value = to,完成状态切换。
  6. afterEach (后置钩子):在导航最终完成(无论成败)后被调用的“清理”钩子。

微信公众号二维码

Last updated: