【Vue 路由系列 02】Vue Router 的导航系统:路由守卫与死循环陷阱
"针对登录页放行,非登录页才检验登录状态。" —— 这是我在模拟面试中对死循环问题的回答,方向对了但不够完整。
在上一篇中,我们搞清楚了前端路由的"物理层"——Hash 和 History 模式分别依赖什么浏览器 API、
pushState为什么不会触发popstate。有了这个基础,本篇进入 Vue Router 的**"交通管制系统"**——路由守卫(Navigation Guards)。用不好会死循环,用好了一行代码就能实现权限控制。这篇文章把守卫体系、执行流程、导航过程中能做的所有事情(包括数据预取)、以及那些经典的坑全部讲清楚。
一、导航是什么
在 Vue Router 中,导航(Navigation) 指的是路由正在发生改变的过程:
用户点击链接 / 调用 router.push / 浏览器后退
↓
触发一个"导航"
↓
依次通过一系列的"钩子函数"(路由守卫)
↓
最终确认:导航成功(渲染新组件)或导航失败(留在当前页/跳到别处)
可以把导航想象成过安检:
入口 → 安检1 → 安检2 → 安检3 → 登机口
↑ ↑ ↑
全局守卫 路由独享 组件内守卫
任何一个安检点说"不行",你就过不去。
二、路由守卫的完整分类
Vue Router 提供三种层级的路由守卫:
2.1 全局前置守卫 router.beforeEach
const router = createRouter({ ... })
// 注册全局前置守卫(每次导航都会经过这里)
router.beforeEach((to, from, next) => {
// to: 即将进入的目标路由对象
// from: 当前正要离开的路由对象
// next: 函数,调用后才能继续下一步导航
console.log(`从 ${from.path} 导航到 ${to.path}`)
next() // 必须调用 next(),否则导航会卡住
})
2.2 全局解析守卫 router.beforeResolve
// 在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用
router.beforeResolve((to, from) => {
// 常用于数据预取等需要在进入页面前完成的操作
if (to.meta.requiresData) {
return fetchData(to.params.id)
}
})
2.3 全局后置钩子 router.afterEach
// 导航完成后调用(不接受 next 函数,因为这时已经结束了)
router.afterEach((to, from) => {
// 常用于修改页面标题、上报埋点等
document.title = to.meta.title || '默认标题'
})
2.4 路由独享守卫 beforeEnter
const routes = [
{
path: '/admin',
component: Admin,
// 只在进入 /admin 时生效
beforeEnter: (to, from) => {
if (!isAdmin()) {
return { name: 'login' } // 重定向到登录页
}
}
}
]
2.5 组件内守卫
export default {
// 进入组件时(在渲染该组件的路由被验证前调用)
beforeRouteEnter(to, from) {
// ⚠️ 此时组件实例还未创建!不能访问 this!
// 如果需要访问组件实例,可以通过回调传递:
return (vm) => {
// vm 就是组件实例
vm.setData(someData)
}
},
// 路由改变但组件被复用时(如 /user/1 → /user/2)
beforeRouteUpdate(to, from) {
// ✅ 可以访问 this
this.userId = to.params.id
this.fetchUser()
},
// 离开组件时
beforeRouteLeave(to, from) {
// ✅ 可以访问 this
// 典型场景:用户在有未保存的表单时试图离开
if (this.hasUnsavedChanges && !confirm('确定要离开吗?未保存的数据会丢失')) {
return false // 取消导航
}
}
}
三、完整的执行顺序 —— ⚠️ 面试必考
这是整篇文章最重要的图。假设你从 /home 点击链接跳转到 /user/123:
导航开始 (用户点击链接 / router.push / 浏览器后退)
↓
① beforeRouteLeave (在离开的组件中,如果有)
↓
② router.beforeEach (全局前置守卫)
↓
【目标组件是重用的吗?(如 /user/1 → /user/2)】
├─ 是 → ③ beforeRouteUpdate (组件内更新守卫)
└─ 否 → 跳过
↓
④ beforeEnter (路由独享守卫,在目标路由配置中)
↓
(异步路由组件开始加载,如有需要)
↓
⑤ beforeRouteEnter (在进入的组件中,如果有)
↓
⑥ router.beforeResolve (全局解析守卫)
↓
导航被确认 ✓
↓
⑦ router.afterEach (全局后置钩子)
↓
DOM 更新
↓
⑧ beforeRouteEnter 的回调执行(如果传入了回调)
记忆口诀(基于 Vue Router 官方文档):离 → 全局前 → 更新(如适用)→ 独享 → 入 → 全局解 → 全局后 → 入回调
📌 官方参考顺序:Vue Router 导航守卫的执行顺序是固定的,官方文档给出了明确的 12 步流程。掌握这个顺序是面试中的绝对加分项。
关键时间线示例(验证官方顺序)
// 场景:从 /a 导航到 /b(组件不同)
// 遵循 Vue Router 官方顺序:
// 1. 首先执行:离开组件的 beforeRouteLeave
const ComponentA = {
beforeRouteLeave() { console.log(1, 'A组件离开(beforeRouteLeave)') }
}
// 2. 然后:全局 beforeEach
router.beforeEach((to, from) => {
console.log(2, '全局前置(beforeEach):', to.path)
})
// 3. 如果 /b 配置了 beforeEnter(路由独享守卫)
{ path: '/b', component: B, beforeEnter: () => { console.log(3, '路由独享守卫(beforeEnter)') } }
// 4. 接下来:进入组件的 beforeRouteEnter(组件尚未创建,无法访问 this)
const ComponentB = {
beforeRouteEnter(to, from) {
console.log(4, 'B组件进入(beforeRouteEnter)')
// 如果需要访问组件实例,可以传回调:
// next(vm => { console.log('组件实例:', vm) })
}
}
// 5. 然后:全局 beforeResolve(在所有异步路由组件和组件内守卫解析之后调用)
router.beforeResolve(() => { console.log(5, '全局解析(beforeResolve)') })
// 6. 最后:全局 afterEach(导航完成后)
router.afterEach(() => { console.log(6, '全局后置(afterEach)') })
// 输出顺序:1 → 2 → 3 → 4 → 5 → 6
💡 注意:如果目标组件是重用的(例如 /user/1 → /user/2),那么会先执行 beforeRouteUpdate,而不是 beforeRouteLeave → beforeRouteEnter。
四、next() 的用法详解 —— ⚠️ 另一个面试重灾区
4.1 Vue Router 3 vs Vue Router 4 的变化
这是很多人不知道的重要差异:
Vue Router 3(next 是必须的):
// Vue Router 3 的写法
router.beforeEach((to, from, next) => {
if (to.meta.auth && !isLoggedIn()) {
next('/login') // 重定向
// 或
next(false) // 取消导航
// 或
next() // 放行
} else {
next()
}
})
⚠️ Vue Router 3 中如果不调用 next(),导航会永远挂起——白屏!
Vue Router 4(推荐 return,不再强制要求 next):
// Vue Router 4 的新写法(更清晰)
router.beforeEach((to, from) => {
if (to.meta.auth && !isLoggedIn()) {
// 直接 return 重定向目标
return { name: 'login', query: { redirect: to.fullPath } }
}
// 或者 return true 表示放行
return true
// 或者 return false 表示取消
// return false
})
// 当然,next 仍然兼容(向后支持)
router.beforeEach((to, from, next) => {
if (to.meta.auth && !isLoggedIn()) {
next('/login')
} else {
next()
}
})
4.2 next() 的所有用法一览
| 调用方式 | 效果 |
|---|---|
next() | 放行,导航正常进行 |
next(false) | 取消导航,留在当前页面 |
next('/home') 或 next({ path: '/home' }) | 重定向到 /home |
next(error) | 导航终止,传递给 router.onError 回调 |
4.3 ⚠️ 经典错误:多次调用 next
// ❌ 错误写法:条件分支都调用了 next
router.beforeEach((to, from, next) => {
if (to.path === '/login') {
next() // 第一次调用
}
// 这里没有 else!所以下面的代码也会执行
if (!isLogin()) {
next('/login') // 第二次调用 💥 报错!
}
next() // 可能第三次调用 💥
})
// ✅ 正确写法:确保 next 只调用一次
router.beforeEach((to, from, next) => {
if (to.path === '/login') {
return next() // return 确保后续代码不执行
}
if (!isLogin()) {
return next('/login')
}
next()
})
Vue Router 4 用 return 替代 next 很大程度上就是为了避免这个问题。
五、死循环陷阱 —— 我在面试中答对的题
5.1 经典死循环场景
这是最常见的面试题之一:
// ❌ 死循环代码
router.beforeEach((to, from, next) => {
if (!isLogin()) {
next('/login') // 未登录 → 跳转登录页
} else {
next()
}
})
为什么死循环?
用户未登录,访问 /dashboard
↓
beforeEach 触发:!isLogin() === true
↓
next('/login') → 开始跳转到 /login
↓
beforeEach 再次触发(因为又是新导航)!
↓
!isLogin() === true(还是未登录)
↓
next('/login') → 又开始跳转到 /login
↓
beforeEach 又触发…… ♻️♻️♻️ 无限循环!
5.2 我的回答 vs 标准答案
我在面试中的回答:
"针对登录页放行,非登录页才检验登录状态。"
方向完全正确! 但需要补充完整:
// ✅ 正确写法:对登录页放行
router.beforeEach((to, from) => {
const whiteList = ['/login', '/register'] // 白名单
// 白名单内的路径直接放行
if (whiteList.includes(to.path)) {
return true
}
// 非白名单检查登录状态
if (!isLogin()) {
// 未登录,重定向到登录页,并记录原路径用于登录后回跳
return {
path: '/login',
query: { redirect: to.fullPath }
}
}
// 已登录,放行
return true
})
5.3 还有一个隐蔽的死循环场景
// ❌ 同样是死循环,但更隐蔽
router.beforeEach((to, from, next) => {
if (to.path !== '/login' && !isLogin()) {
next('/login')
} else {
next()
}
})
// 问题在哪?
// 当 isLogin() 内部依赖 token 判断,
// 而 token 存储在 Pinia/Vuex 里,
// F5 刷新后 store 被重置,isLogin() 返回 false
// 即使已经在 /login 页面了,to.path !== '/login' 为 true
// 又要 next('/login')……
防御性写法:
// ✅ 双重保险:白名单 + 目标路径判断
router.beforeEach((to, from) => {
// 1. 先看是不是公开页面(白名单优先级最高)
if (isPublicPage(to)) {
return true
}
// 2. 再检查是否已登录
if (!isLogin()) {
// ⚠️ 只有当目标不是登录页时才重定向
if (to.name !== 'login') {
return { name: 'login', query: { redirect: to.fullPath } }
}
// 已经要去登录页了,直接放行(避免循环)
return true
}
return true
})
六、并发竞态问题 —— 我在这里翻了大车
6.1 面试题回顾
Qwen 问:多个路由几乎同时触发 beforeEach(比如快速连续点击),你的权限校验逻辑怎么避免竞态?
我的回答:
"把 Promise 缓存在 localStorage 里……"
❌ 这完全是错误的!
6.2 为什么 localStorage 不能存 Promise
| 特性 | 内存变量 | localStorage |
|---|---|---|
| 数据类型 | 任意 JS 对象 | 只能存字符串 |
| 序列化 | 不需要 | 自动 JSON.stringify/parse |
| 生命周期 | F5 刷新后消失 | 永久存储(除非手动删) |
| 能否存 Promise | ✅ 可以 | ❌ 不可以! Promise 无法序列化 |
// ❌ 完全无效的代码
const p = fetch('/api/user/token')
localStorage.setItem('authPromise', p) // 存进去的是 "[object Promise]" 字符串!
const restored = localStorage.getItem('authPromise') // 取出来是字符串,不是 Promise
await restored // 不会有任何等待效果,因为它不是 Promise
而且即使能存,F5 刷新后整个 JS 运行时都被销毁了,内存里的 Promise 早就没了。
6.3 正确的解决方案
方案一:内存单例 Promise(适用于 SPA 内部导航)
// 在模块级别维护一个 Promise 引用
let authCheckPromise = null
router.beforeEach(async (to) => {
if (isPublicPage(to)) return true
// 如果已经有一个正在进行的校验请求,直接复用它
if (authCheckPromise) {
await authCheckPromise
return isLogin() ? true : { name: 'login' }
}
// 发起新的校验请求
authCheckPromise = checkAuth() // checkAuth 返回 Promise
try {
await authCheckPromise
return isLogin() ? true : { name: 'login' }
} finally {
authCheckPromise = null // 无论成败,都要清空引用
}
})
这个方案如何解决竞态?
时刻 T=0ms: 用户点击 /dashboard
↓
beforeEach 触发
authCheckPromise = fetch('/api/check-auth')
↓
时刻 T=50ms: 用户又点击 /settings(快速连点)
↓
beforeEach 触发
发现 authCheckPromise 不为 null!
↓
await authCheckPromise ← 复用同一个 Promise!
(不会发第二个请求,等第一个的结果就行)
时刻 T=100ms: 第一个 fetch 返回
两个 beforeEach 同时拿到结果
各自独立决定放行还是拦截
方案二:F5 刷新后的处理
F5 刷新后 JS 上下文重建,内存中的 authCheckPromise 肯定是空的。这种情况下:
// 方案 A:让后端做(推荐)
// 后端接口设置 Cache-Control: max-age=300(5分钟强缓存)
// 这样短时间内重复请求不会真正到达后端
// 方案 B:前端请求去重
function createDedupedFetch(key, fn) {
const cache = new Map()
return async (...args) => {
if (cache.has(key)) return cache.get(key)
const promise = fn(...args).finally(() => cache.delete(key))
cache.set(key, promise)
return promise
}
}
6.4 完整的权限校验模板
综合以上所有要点,一份生产级别的 beforeEach 写法:
// auth.js
let pendingCheck = null
export function setupRouterGuards(router) {
router.beforeEach(async (to, from) => {
// ===== 第一关:白名单放行 =====
const publicPages = ['/login', '/register', '/forgot-password']
if (publicPages.includes(to.path)) {
// 已登录状态下访问登录页 → 重定向首页
if (isLogin() && to.path === '/login') {
return { path: '/' }
}
return true
}
// ===== 第二关:Token 存在性检查(同步,立即返回)=====
const token = getToken()
if (!token) {
return {
path: '/login',
query: { redirect: to.fullPath }
}
}
// ===== 第三关:Token 有效性检查(异步,可能网络请求)=====
// 使用单例 Promise 避免快速导航时的重复请求
if (!pendingCheck) {
pendingCheck = validateToken(token).finally(() => {
pendingCheck = null
})
}
try {
const isValid = await pendingCheck
if (!isValid) {
clearToken()
return {
path: '/login',
query: { redirect: to.fullPath }
}
}
return true
} catch (error) {
// 网络异常等情况:允许通行还是阻止?根据业务决定
console.error('Token 校验失败:', error)
return true // 保守策略:网络问题时先让用户进
}
})
router.afterEach((to) => {
// 设置页面标题
document.title = to.meta.title || 'My App'
})
}
七、导航过程中还能做什么:数据预取模式
守卫不只是用来做权限校验的。在导航流程中,什么时候获取页面数据是一个经典的前端架构决策。
7.1 三种数据预取模式对比
| 模式 | 获取时机 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 导航时获取(守卫中) | beforeEach / beforeResolve | 数据就绪后才渲染,体验一致 | 导航前有等待时间,用户可能觉得"卡了" | 首屏数据必须就绪的页面 |
| 组件内获取(created/mounted) | 组件生命周期 | 导航立即完成,先展示骨架屏 | 数据到达前是空态,需要 loading 状态 | 大多数 CRUD 页面 |
| 导航后获取(异步) | 组件 mounted + 异步请求 | 最快的首屏渲染 | 布局闪烁(FOUC) | 非关键数据、次要内容 |
7.2 模式一:导航时获取(在守卫中预取)
这是"数据没到就不让进页面"的策略:
// 方式 A:使用 beforeResolve(推荐用于数据预取)
// beforeResolve 在所有组件内守卫之后、导航确认之前触发
// 此时已经知道要进入哪个组件,适合做数据预取
router.beforeResolve(async (to) => {
// 只有目标路由标记了需要预取数据才执行
if (to.meta.requiresData) {
const store = useDataStore()
// 如果数据还没加载过,则加载
if (!store.hasData(to.params.id)) {
// 显示全局 loading(可选)
showLoading()
try {
await store.fetchDetail(to.params.id)
} finally {
hideLoading()
}
}
}
})
// 路由配置中标记需要预取
{
path: '/user/:id',
component: UserDetail,
meta: { requiresData: true, title: '用户详情' }
}
⚠️ 注意:不要在 beforeEach 中做重量级数据请求!
beforeEach 是全局前置守卫,每次导航都会经过。如果把数据请求放在这里:
- 即使只是从
/user/1切换到/user/2(同一组件复用),也会重新请求 - 如果用户快速连续点击,会触发多个并发请求
正确的分层:
beforeEach→ 只做轻量级判断(登录检查、权限验证)beforeResolve→ 做数据预取(此时已确定目标组件)
7.3 模式二:组件内获取(最常用)
这是大多数 Vue 项目采用的策略:
<template>
<div v-if="loading" class="skeleton">骨架屏...</div>
<div v-else-if="error" class="error">加载失败: {{ error }}</div>
<div v-else>
<!-- 真实内容 -->
<h1>{{ user.name }}</h1>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { api } from '@/api'
const route = useRoute()
const loading = ref(true)
const error = ref(null)
const user = ref({})
onMounted(async () => {
try {
user.value = await api.getUser(route.params.id)
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
</script>
为什么这种方案最流行?
- 导航即时响应——用户点击后立刻切换页面,感知延迟低
- 配合 Suspense——Vue 3 的
<Suspense>可以优雅处理异步组件的加载态 - 关注点分离——数据逻辑在组件内部,不在路由配置里
7.4 模式对比:如何选择?
你的页面属于哪种类型?
│
├─ SEO 要求高?(SSR 场景)
│ └─ → 必须用"导航时获取",服务端等数据到了再返回 HTML
│
├─ 首屏数据必须是完整的?(如报表页、审批详情)
│ └─ → 用 beforeResolve 预取 + 全局 loading
│
├─ 用户可容忍短暂空态?(列表页、表单页)
│ └─ → 用组件内 onMounted 获取 + 骨架屏/Loading 态
│
└─ 数据量大且非核心?(评论、推荐)
└─ → 用导航后懒加载,甚至滚动到底部时再触发
八、Navigation Failure:导航失败的类型判别
8.1 什么是 Navigation Failure
当路由守卫返回 false 或重定向路径时,导航会被取消或中断。Vue Router 4 会把这个行为包装成一个特殊的错误对象:
// ❌ 这种写法无法区分"真正的错误"和"正常的导航取消"
router.beforeEach(async (to) => {
if (!isLogin()) {
return { name: 'login' }
}
})
// 在 router.push 之后
try {
await router.push('/admin')
} catch (e) {
console.error('导航失败:', e)
// 但这个 e 到底是网络错误?还是被 beforeEach 拦截了?
// 无法区分!
}
8.2 isNavigationFailure 来救场
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
try {
await router.push('/dashboard')
} catch (e) {
// 判断是不是"导航失败"(而非真正的异常)
if (isNavigationFailure(e)) {
// 是导航层面的失败(被拦截/重定向/取消),不是 bug
console.log('导航被拦截,原因:', e.type)
// 还可以精确判断是哪种类型
if (e.type === NavigationFailureType.redirected) {
console.log('被重定向到了:', e.to.path)
} else if (e.type === NavigationFailureType.aborted) {
console.log('导航被取消了(return false)')
} else if (e.type === NavigationFailureType.duplicated) {
console.log('重复导航到相同路径(无操作)')
}
} else {
// 这是真正的异常!需要上报或提示用户
console.error('导航过程出现异常:', e)
}
}
8.3 四种 Navigation Failure 类型
| 类型 | 常量 | 触发场景 | 是否应该视为错误 |
|---|---|---|---|
| redirected | NavigationFailureType.redirected | 守卫返回了重定向对象 | ❌ 正常业务行为 |
| aborted | NavigationFailureType.aborted | 守卫返回了 false | ❌ 正常业务行为 |
| cancelled | NavigationFailureType.cancelled | 一个新的导航取代了当前进行中的导航(如快速连点) | ⚠️ 视情况处理 |
| duplicated | NavigationFailureType.duplicated | 导航到的位置与当前完全相同 | ❌ 完全无害 |
8.4 实际应用场景
// 封装一个安全的导航方法
async function safeNavigate(router, target) {
try {
await router.push(target)
return true // 导航成功
} catch (e) {
if (isNavigationFailure(e, NavigationFailureType.redirected)) {
// 被重定向(如跳转到登录页),不需要报错
return false
}
if (isNavigationFailure(e, NavigationFailureType.duplicated)) {
// 重复导航,忽略即可
return true
}
if (isNavigationFailure(e, NavigationFailureType.aborted)) {
// 被守卫取消,可能有业务逻辑需要处理
console.warn('导航被守卫取消')
return false
}
// 其他是真正需要关注的错误
throw e
}
}
// 使用
const success = await safeNavigate(router, { name: 'dashboard' })
if (!success) {
// 可以在这里做降级处理
}
面试加分点:提到
isNavigationFailure说明你对 Vue Router 的错误处理机制有深入理解。很多人只知道beforeEach能拦截导航,但不知道如何在router.push()的调用端区分"被拦截"和"真报错"。
九、组合式 API 中的路由守卫
9.1 Composition API 的路由钩子函数
Vue Router 4 提供了一组专门用于 <script setup> / setup() 的组合式函数:
<script setup>
import {
onBeforeRouteLeave, // 对应 Options API 的 beforeRouteLeave
onBeforeRouteUpdate // 对应 Options API 的 beforeRouteUpdate
} from 'vue-router'
import { ref, onMounted } from 'vue'
const userId = ref(null)
// ✅ 组合式 API 中没有 onBeforeRouteEnter!
// 因为 setup() 在组件创建后执行,不存在"进入前"的概念
// 如果需要类似功能,请用 onMounted 替代
// 路由改变但组件被复用时(如 /user/1 → /user/2)
onBeforeRouteUpdate((to) => {
// ✅ 这里可以访问 this(通过变量闭包)
userId.value = to.params.id
fetchUserData(to.params.id)
})
// 离开当前路由时
onBeforeRouteLeave((to) => {
// ✅ 也可以访问组件内部状态
if (hasUnsavedChanges.value && !confirm('确定离开?')) {
return false // 取消导航
}
})
</script>
9.2 ⚠️ 没有 onBeforeRouteEnter
这是一个重要的区别:
| 守卫 | Options API | Composition API |
|---|---|---|
| beforeRouteEnter | ✅ 有 | ❌ 没有替代品 |
| beforeRouteUpdate | ✅ 有 | ✅ onBeforeRouteUpdate |
| beforeRouteLeave | ✅ 有 | ✅ onBeforeRouteLeave |
为什么没有 onBeforeRouteEnter?
因为 beforeRouteEnter 的设计初衷是在组件实例创建之前执行回调,它通过回调参数传递组件实例:
// Options API 的写法(Composition API 不支持)
beforeRouteEnter(to, from) {
// 这里的 this 是 undefined!组件还没创建
// 只能通过回调访问实例
return (vm) => {
vm.setData(someData) // vm 就是组件实例
}
}
而在 <script setup> 中,代码本身就在 setup() 里执行,相当于组件已经在初始化过程中了,不存在"进入之前"的时间点。
如果确实需要在进入前做某些事:
<script setup>
// 方案一:用 onMounted 代替(绝大多数场景够用)
import { onMounted } from 'vue'
onMounted(() => {
// 组件首次挂载时执行,效果类似于 beforeRouteEnter
initData()
})
// 方案二:配合 inject 从路由守卫中接收数据
// 在 beforeEach 中把数据存入 route.meta 或 provide
</script>
十、导航流程的状态机视角
还记得我们在静默搜索复盘笔记中讨论过的"隐式状态机"吗?路由导航同样可以用状态机建模:
触发导航(push/点击链接/后退)
↓
PENDING(待决态)
↓
┌─────────┴─────────┐
↓ ↓
通过所有守卫 被某个守卫拦截
↓ ↓
CONFIRMED CANCELLED
(确认导航) (取消/重定向)
↓
组件加载+渲染
↓
COMPLETED(完成)
面试中如果被问到"你的路由守卫是怎么组织的",你可以这样描述:
我的路由守卫分为三层:第一层是全局
beforeEach做统一的鉴权逻辑(白名单放行 + Token 校验),第二层是路由级别的beforeEnter做特定页面的权限粒度控制(如管理员页面),第三层是组件内的beforeRouteLeave做脏数据保护。导航过程中使用内存单例 Promise 来防止并发竞态,确保同一时刻只有一个 Token 校验请求在飞行。
十一、本篇小结
| 概念 | 一句话记忆 |
|---|---|
| 路由守卫执行顺序(官方) | 离 → 全局前 → 更新(如适用)→ 独享 → 入 → 全局解 → 全局后 → 入回调 ① beforeRouteLeave → ② beforeEach → ③ beforeRouteUpdate(如适用)→ ④ beforeEnter → ⑤ beforeRouteEnter → ⑥ beforeResolve → ⑦ afterEach → ⑧ beforeRouteEnter回调 |
| Vue Router 4 新特性 | 推荐 return true/false/路由对象,不再强制 next() |
| next() 多次调用 | 绝对禁止!用 return 确保只执行一次 |
| 死循环原因 | 登录拦截器没有排除登录页本身 → 跳转到登录页又触发拦截器 → 再跳转到登录页 → 无限循环 |
| 解决死循环 | 白名单放行:登录页/注册页不检查登录状态 |
| 并发竞态 | 快速连续导航导致多次权限校验请求 |
| ❌ localStorage 存 Promise | 不可能!localStorage 只能存字符串,且 Promise 无法序列化 |
| ✅ 正确做法 | 模块级别单例 Promise:let pendingCheck = null,复用同一个 Promise |
| beforeRouteEnter 不能访问 this | 因为此时组件还没创建;如需访问用回调:return (vm) => { ... } |
| beforeRouteLeave 的经典用途 | 有未保存表单时阻止用户离开 |
| 数据预取三模式 | 导航时(beforeResolve) vs 组件内(onMounted) vs 导航后(异步);按需选择 |
| isNavigationFailure | 区分"导航被拦截"(正常业务)和"真正的异常"(需要处理) |
| 组合式 API 守卫 | onBeforeRouteUpdate + onBeforeRouteLeave;没有 onBeforeRouteEnter(用 onMounted 代替) |
下一篇预告:搞清楚了导航流程中的守卫系统,下一篇我们进入动态路由与权限控制——这是 Vue Router 在企业级 SaaS 项目中最核心的应用场景。addRoute 怎么用?F5 刷新后为什么 404?404 通配符应该在什么时候挂载?以及——在讲 addRoute 之前,我会先把 Vue Router 的完整路由体系(嵌套路由、编程式导航、命名路由、动态参数)系统梳理一遍。
👉 Vue 路由系列 03:动态路由与权限控制 —— 路由体系全貌 + addRoute + 404 问题标准解法
🔗 回顾上一篇:Vue 路由系列 01:前端路由的本质 —— Hash vs History vs Abstract 三种模式
参考来源:Gemini 3.1 Pro 模拟面试记录(路由与单页应用章节)、Vue Router 4 官方文档