【Vue Router 路由守卫(Navigation Guards)指南:概念、执行顺序、beforeResolve、异步路由组件】

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 固定导航流水线(快速复述版)

一次导航从 fromto,按照以下阶段执行(按语义理解即可):

  1. 离开阶段(Leave)
    • beforeRouteLeave(离开的组件里有才触发)
  2. 全局前置(Global before)
    • router.beforeEach
  3. 复用更新(Update)
    • beforeRouteUpdate(同组件复用、参数变化才触发)
  4. 路由门禁(Route before)
    • beforeEnter(目标路由有才触发)
  5. 解析阶段(Resolve / Components)
    • 解析匹配组件、加载异步路由组件(() => import()
    • 运行 beforeRouteEnter(进入组件里有才触发)
  6. 全局解析后(Global resolve)
    • router.beforeResolve
  7. 导航完成(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-else
  • beforeEnter 适合"模块规则",跟路由配置集中管理

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) :一次从 fromto 的路由迁移过程。
  • 路由记录(Route Record):路由表中的一条配置(包含 path/component/meta/beforeEnter)。
  • 匹配(matched):当前路由匹配到的路由记录链条(父子嵌套)。
  • 异步路由组件 :路由组件使用 () => import() 动态加载,实现按路由拆包。
  • PV(Page View):页面访问量统计,通常在 afterEach 记录"最终落地页"。
  • sendBeacon:浏览器提供的卸载期上报 API,适合页面离开时保证上报成功率。
相关推荐
Ticnix4 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人4 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl4 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人4 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼4 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空4 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_4 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus4 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空4 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范