Vue Router 路由守卫(Navigation Guards)指南:概念、执行顺序、beforeResolve、异步路由组件
目标读者:有 Vue2/Vue3 项目经验、希望系统梳理 Vue Router 路由与守卫机制,能在实际工程协作与技术交流中解释清楚"为什么这样设计、怎么落地、有哪些边界与坑"。
目录
- [1. 路由与导航的基本概念](#1. 路由与导航的基本概念)
- [2. 路由守卫是什么:它解决了什么问题](#2. 路由守卫是什么:它解决了什么问题)
- [3. 守卫分类:为什么分全局、路由独享、组件内](#3. 守卫分类:为什么分全局、路由独享、组件内)
- [4. 固定导航流水线:执行顺序与"看起来不同"的原因](#4. 固定导航流水线:执行顺序与“看起来不同”的原因)
- [5. beforeEach / beforeEnter / beforeResolve / afterEach:分别用来干什么](#5. beforeEach / beforeEnter / beforeResolve / afterEach:分别用来干什么)
- [6. 异步路由组件是什么:为什么影响导航阶段](#6. 异步路由组件是什么:为什么影响导航阶段)
- [7. 典型案例与实践指南](#7. 典型案例与实践指南)
- [8. Vue Router 3 vs 4:API 差异与迁移注意点](#8. Vue Router 3 vs 4:API 差异与迁移注意点)
- [9. 常见坑点与排查清单](#9. 常见坑点与排查清单)
- [10. 记忆化总结(要点速记版)](#10. 记忆化总结(要点速记版))
- 术语表
1. 路由与导航的基本概念
1.1 路由(Route)是什么
- 路由是一种 URL → 页面/组件状态 的映射规则。
- 在 SPA 中,路由变化通常不会触发整页刷新,而是改变:
- 当前要渲染的组件树
- 组件接收的参数(
params/query) - 与页面相关的副作用(标题、埋点、数据加载等)
1.2 导航(Navigation)是什么
- 导航是从一个路由状态
from切换到另一个路由状态to的过程。 - 导航不是"单纯切换组件",而是一次可控的状态迁移:
- 可能需要鉴权
- 可能需要异步准备(加载路由组件/获取数据)
- 可能需要阻止离开(未保存提示)
2. 路由守卫是什么:它解决了什么问题
2.1 定义
路由守卫(Navigation Guards)本质是:
- 在一次导航的生命周期中插入 可拦截、可异步、可重定向 的钩子函数。
- 它让你能在"导航发生前/中/后"执行控制逻辑。
2.2 为什么需要守卫(工程视角)
如果没有守卫,常见问题是:
- 只能在组件
mounted里做鉴权 → 会闪屏(页面先渲染再被踢走) - 鉴权、动态路由注入逻辑分散在各页面 → 重复、难维护
- 导航过程涉及异步逻辑(权限、配置、远程菜单)→ 需要 可等待 的机制
- 快速连续跳转会产生竞态 → 需要 可取消/可重入 的导航控制
3. 守卫分类:为什么分全局、路由独享、组件内
可以用"作用域(Scope)"来理解。
3.1 三类守卫分别是什么
| 分类 | 作用域 | 典型 API | 适用问题 | 关键特点 |
|---|---|---|---|---|
| 全局守卫(Global) | 全站 | beforeEach / beforeResolve / afterEach |
全站统一规则:鉴权、动态路由、埋点、进度条 | 一次配置,处处生效 |
| 路由独享守卫(Per-route) | 某条路由记录 | beforeEnter |
某模块门禁:只对 admin/finance 等模块 | 跟着路由表走,集中管理 |
| 组件内守卫(In-component) | 某个组件实例 | beforeRouteLeave/Update/Enter |
与页面状态强相关:未保存离开、同组件复用刷新 | 就近维护,贴近状态 |
3.2 为什么要这么区分(高分解释)
- 责任边界清晰:应用策略、路由规则、页面状态各管各的
- 复用成本最小化:能全局复用的不要散落到组件里
- 降低耦合:路由层不依赖具体组件细节,组件也不承载全站策略
- 执行顺序可控:先出门(leave),再过总闸(global),再过分闸(route),最后进屋(enter)
3.3 记忆口诀(作用域)
- 全站 → 这条路 → 这个页面
4. 固定导航流水线:执行顺序与"看起来不同"的原因
重点:顺序不是"多种情况",而是一条固定流水线在不同导航里有些步骤为空。
4.1 固定导航流水线(快速复述版)
一次导航从 from → to,按照以下阶段执行(按语义理解即可):
- 离开阶段(Leave)
beforeRouteLeave(离开的组件里有才触发)
- 全局前置(Global before)
router.beforeEach
- 复用更新(Update)
beforeRouteUpdate(同组件复用、参数变化才触发)
- 路由门禁(Route before)
beforeEnter(目标路由有才触发)
- 解析阶段(Resolve / Components)
- 解析匹配组件、加载异步路由组件(
() => import()) - 运行
beforeRouteEnter(进入组件里有才触发)
- 解析匹配组件、加载异步路由组件(
- 全局解析后(Global resolve)
router.beforeResolve
- 导航完成(After)
router.afterEach
记忆化:先出门 → 过总闸 → 同屋更新 → 过分闸 → 拿钥匙进屋 → 总检 → 收尾
4.2 为什么有时看起来"顺序不同"
因为某些阶段根本不存在:
- 没有离开确认 → 没有
beforeRouteLeave - 不是同组件复用 → 没有
beforeRouteUpdate - 目标路由没写
beforeEnter→ 跳过 - 目标路由组件不是异步 import → "解析阶段"很快,感觉不到
- 组件没写
beforeRouteEnter→ 进入阶段跳过
4.3 为什么是 "beforeRouteLeave → beforeEach"
- 语义:先问"旧页面放不放你走",再谈"全站策略让不让你去"
- 避免副作用:如果
beforeEach里会请求/埋点/开启 loading,用户最终取消离开会产生脏副作用 - 体验:离开确认应该立即响应,不应等全局异步逻辑跑完
4.4 为什么是 "beforeEach → beforeRouteUpdate → beforeEnter"
beforeEach是总闸:先决定本次导航是否继续,避免无意义的实例级工作beforeRouteUpdate依赖"匹配结果 + 复用判定 + 已存在组件实例",因此在全局通过后收集执行beforeEnter是目标路由记录门禁:在组件进入前保证复用组件已同步到新参数,避免状态交错
5. beforeEach / beforeEnter / beforeResolve / afterEach:分别用来干什么
5.1 beforeEach:全站总闸门
职责:对每一次导航执行统一前置逻辑,决定放行/取消/改道。
常见用途:
- 登录鉴权(未登录 →
/login) - 动态路由注入(拉权限 →
addRoute→ 重新进入) - 全局进度条开始(如
NProgress.start())
5.2 beforeEnter:某条路由/某模块门禁
职责:只对特定模块/路由生效的门禁。
典型场景:
/admin模块需要管理员权限- 进入
/tenant/*前必须选择租户
对比 beforeEach:
beforeEach适合"全站规则",否则会变成一堆 if-elsebeforeEnter适合"模块规则",跟路由配置集中管理
5.3 beforeResolve:最后的全局兜底(总检)
一句话 :beforeResolve 是导航确认前的最后一道全局前置检查。
它的价值在于:
- 保证路由级守卫与组件相关解析流程(含异步组件)都已走到"接近 ready"的阶段
- 适合做最终兜底:
- 权限/配置最终检查
- 最终确认 loading 是否应该结束
- 动态路由注入后的 matched 校验
直觉理解:
beforeEach是"进流程前的总闸",beforeResolve是"进门前的总检"。
5.4 afterEach:导航完成后的收尾
关键限制 :afterEach 不能拦截/重定向,只能做副作用。
典型用途:
- PV 埋点(确认落地路由)
- 更新标题、面包屑
- 结束进度条(
NProgress.done())
6. 异步路由组件是什么:为什么影响导航阶段
6.1 定义
异步路由组件是指路由组件用函数返回动态 import:
js
// Vue Router 3/4 均可
const routes = [
{
path: '/report',
component: () => import('@/pages/Report.vue'),
},
]
6.2 为什么要用(选型建议)
- 路由组件通常体积大(图表、地图、编辑器、复杂表格)
- 异步加载能实现 按路由拆包(code splitting)
- 目标:减少首包体积、加快首屏
6.3 为什么会影响守卫顺序
导航到 /report 时,必须先把 Report.vue 对应的 chunk 加载并解析,才能进入"渲染新页面"。这就是"解析阶段"的来源。
beforeResolve 的设计目的之一:在异步组件与组件内守卫都处理到位后做最后兜底。
7. 典型案例与实践指南
7.1 案例:登录鉴权 + 白名单(beforeEach)
目标:未登录访问需要登录的页面 → 跳转登录;登录后回跳原页面。
Vue Router 4(推荐 return 风格)
ts
// router/guard.ts
router.beforeEach(async (to) => {
const isPublic = Boolean(to.meta.public)
const token = auth.getToken()
if (isPublic) return true
if (!token) {
return {
name: 'login',
query: { redirect: to.fullPath },
}
}
return true
})
Vue Router 3(next 风格)
js
router.beforeEach((to, from, next) => {
const isPublic = Boolean(to.meta.public)
const token = auth.getToken()
if (isPublic) return next()
if (!token) {
return next({ name: 'login', query: { redirect: to.fullPath } })
}
next()
})
最佳实践:
- beforeEach 里避免多次调用
next() - 推荐用
return(Router 4)减少分支错误
7.2 案例:动态路由注入(权限路由)
场景:登录后根据权限生成菜单与路由。第一次进入系统必须先拉权限,再 addRoute。
关键点:
- 用 flag 避免重复注入
- 注入完成后重新进入目标路由(通常
next(to.fullPath)或return to.fullPath)
ts
let routesInited = false
router.beforeEach(async (to) => {
const token = auth.getToken()
if (!token) return to.name === 'login' ? true : { name: 'login' }
if (!routesInited) {
const perms = await api.getPermissions()
const dynRoutes = buildRoutes(perms)
dynRoutes.forEach(r => router.addRoute(r))
routesInited = true
// 关键:重新进入,确保 matched 重新计算
return to.fullPath
}
return true
})
7.3 案例:离开确认(beforeRouteLeave)
场景:编辑页未保存,离开提示确认。
js
export default {
data() {
return { dirty: false }
},
beforeRouteLeave(to, from, next) {
if (!this.dirty) return next()
const ok = window.confirm('内容未保存,确定离开?')
next(ok)
},
}
实践建议:
- 离开确认属于页面状态,放组件内最合适
- 不要放全局 beforeEach(会污染全局逻辑)
7.4 案例:同组件复用刷新(beforeRouteUpdate)
场景:
/user/1→/user/2复用同一个详情组件。
js
export default {
async beforeRouteUpdate(to, from, next) {
await this.fetchDetail(to.params.id)
next()
},
}
7.5 案例:路由 PV 埋点 + sendBeacon
结论
- PV/路由到达事件 :建议放
router.afterEach(拿到最终落地路由,减少中途重定向脏数据) - 停留时长/退出上报 :建议放
pagehide/visibilitychange,并使用navigator.sendBeacon提高卸载场景成功率
路由 PV(afterEach)
ts
router.afterEach((to, from) => {
analytics.track('page_view', {
path: to.fullPath,
referrer: from.fullPath,
})
})
退出/停留(pagehide + sendBeacon)
ts
function reportLeave(payload: any) {
const url = '/analytics/leave'
const body = JSON.stringify(payload)
// sendBeacon 适合卸载场景
navigator.sendBeacon?.(url, body)
}
window.addEventListener('pagehide', () => {
reportLeave({
path: location.pathname,
ts: Date.now(),
})
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
reportLeave({
path: location.pathname,
ts: Date.now(),
})
}
})
最佳实践:
sendBeacon不适合做所有请求;更适合"卸载/切后台"这类容易丢包的上报- afterEach 里更多是"到达页"事件;离开/停留要用 pagehide/visibilitychange
8. Vue Router 3 vs 4:API 差异与迁移注意点
| 对比项 | Vue Router 3(Vue2) | Vue Router 4(Vue3) |
|---|---|---|
| 控制导航 | next() |
推荐 return true/false/routeLocation |
| 异步守卫 | 手动 next() |
await 更自然 |
| 常见坑 | next() 多次/漏调 |
return 更不易写错 |
8.1 Router 4 的推荐写法(return 风格)
return true:放行return false:取消return { name: 'login' }:重定向
9. 常见坑点与排查清单
9.1 导航卡死
- Vue Router 3:忘记
next()/ 分支没返回 - Vue Router 4:async 分支未
return
9.2 next 调用两次(Router 3)
- 用
return next(...)保证单路径
9.3 重定向循环
- 登录页也被鉴权拦截
- 解决:白名单 /
meta.public
9.4 动态路由注入后 404
addRoute后不重新进入- 解决:重新进入
to.fullPath
9.5 组件复用导致数据不刷新
mounted不会重跑- 用
beforeRouteUpdate/ watch route params
9.6 afterEach 里做鉴权
- afterEach 不能拦截,只会产生"闪一下再跳"
9.7 sendBeacon 使用误区
- afterEach 更适合 PV;退出类上报使用 pagehide/visibilitychange
10. 记忆化总结(要点速记版)
10.1 作用域口诀
- 全站 → 这条路 → 这个页面
10.2 流水线口诀
- 出门(leave)→ 总闸(beforeEach)→ 同屋更新(beforeRouteUpdate)→ 分闸(beforeEnter)→ 拿钥匙进屋(异步组件/enter)→ 总检(beforeResolve)→ 收尾(afterEach)
10.3 一分钟摘要
- 路由守卫是导航生命周期中的拦截器链,支持异步、取消、重定向。
- 按作用域分全局、路由独享、组件内,目的是清晰边界、降低耦合、提高复用。
- 执行上是一条固定流水线,某些步骤在特定导航里不存在所以看起来不同。
- beforeEach 做全站门禁,beforeEnter 做模块门禁,beforeResolve 做最后兜底,afterEach 做收尾埋点。
- 异步路由组件通过
() => import()实现按路由拆包,会引入解析阶段,因此 beforeResolve 在工程里很重要。
术语表
- 导航(Navigation) :一次从
from到to的路由迁移过程。 - 路由记录(Route Record):路由表中的一条配置(包含 path/component/meta/beforeEnter)。
- 匹配(matched):当前路由匹配到的路由记录链条(父子嵌套)。
- 异步路由组件 :路由组件使用
() => import()动态加载,实现按路由拆包。 - PV(Page View):页面访问量统计,通常在 afterEach 记录"最终落地页"。
- sendBeacon:浏览器提供的卸载期上报 API,适合页面离开时保证上报成功率。