跳到主要内容

Vue Router 导航守卫(路由守卫)详解

在做 Vue 单页应用(SPA)时,路由跳转往往不只是“换个页面”这么简单:

  • 需要登录才能访问 /profile
  • 进入某个页面前要先拉取数据
  • 表单没保存就离开要二次确认
  • 需要做埋点统计、动态设置标题

这些“在路由跳转的某个阶段插入逻辑”的能力,就是 Vue Router 导航守卫(Navigation Guards)

本文约定

本文以 Vue Router 4(Vue 3) 为主,讲清楚:守卫有哪些、怎么写、最重要的 执行顺序 是什么,以及实战与常见坑。

1. 导航守卫到底是什么?

你可以把一次导航(router.push() / 点击 <router-link>)理解成一条“流水线”:

  1. 计算从哪里来(from)到哪里去(to
  2. 判断是否允许离开当前页面
  3. 判断是否允许进入目标页面
  4. 解析异步组件(路由懒加载)
  5. 确认导航,更新 URL,渲染新页面

守卫就是这条流水线上的“关卡”。它可以:

  • 放行:继续执行后续步骤
  • 取消:终止这次导航
  • 重定向:跳去另一个路由(常见:跳登录页)

2. 守卫分哪几类?(在哪里写)

Vue Router 常见守卫分三大类(再加上组合式 API 的写法):

类别典型 API写在哪里常见用途
全局守卫beforeEach / beforeResolve / afterEachrouter 实例上登录鉴权、埋点、标题、全局数据准备
路由独享守卫beforeEnter某条路由配置里某个模块的权限、局部拦截
组件内守卫beforeRouteLeave / beforeRouteUpdate / beforeRouteEnter组件选项里(Options API)离开确认、参数变化刷新、进入后拿实例
组合式守卫onBeforeRouteLeave / onBeforeRouteUpdatesetup()Composition API 项目更常用
afterEach 不能拦截导航

afterEach 是“后置钩子”,只能做副作用(埋点/日志/标题),不能取消或重定向

3. Vue Router 4 推荐写法:用“返回值”控制导航

Vue Router 4 里,大多数守卫都支持用返回值表达“要不要继续”:

  • return true / return undefined:放行
  • return false:取消导航
  • return { name: 'Login' } / return '/login':重定向
  • throw new Error():中断并进入错误处理(可配合 router.onError
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isLoggedIn()) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
还要不要用 next

Vue Router 4 仍兼容 next() 风格,但更推荐“返回值”写法:更直观,也更不容易出现“忘记调用 next / 调用两次 next”的坑。

4. 速查:每种守卫怎么写(最小可用示例)

4.1 全局守卫:beforeEach / beforeResolve / afterEach

// 1) 最常用:鉴权 / 统一拦截
router.beforeEach((to) => {
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth)
if (requiresAuth && !isLoggedIn()) return { name: 'Login' }
})

// 2) 更靠后:适合做“最后一道统一准备”
router.beforeResolve(async (to) => {
if (to.meta.prefetch) await prefetchPageData(to)
})

// 3) 后置钩子:做副作用(不能拦截)
router.afterEach((to) => {
document.title = (to.meta.title as string) || '默认标题'
reportPV(to.fullPath)
})

4.2 路由独享守卫:beforeEnter

const routes = [
{
path: '/admin',
name: 'Admin',
component: () => import('./views/Admin.vue'),
beforeEnter: () => {
if (!isAdmin()) return { name: 'Forbidden' }
},
},
]

4.3 组件复用时参数变化:beforeRouteUpdate / onBeforeRouteUpdate

典型场景:/users/1/users/2组件没销毁,但路由参数变了,需要重新请求数据。

// Options API
export default {
async beforeRouteUpdate(to) {
this.user = await fetchUser(to.params.id)
},
}
<!-- Composition API(<script setup>) -->
<script setup lang="ts">
import { onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteUpdate(async (to) => {
await fetchUser(to.params.id)
})
</script>

4.4 进入守卫:beforeRouteEnter 里如何拿到组件实例?

beforeRouteEnter 执行时组件实例还没创建,所以拿不到 this。如果需要访问实例,用 next((vm) => {})

export default {
beforeRouteEnter(to, from, next) {
next((vm) => {
vm.loadData()
})
},
}

5. 实战 1:最常见的“登录鉴权”守卫

5.1 在路由上标记 meta

const routes = [
{
path: '/profile',
name: 'Profile',
component: () => import('./views/Profile.vue'),
meta: { requiresAuth: true, title: '个人中心' },
},
{ path: '/login', name: 'Login', component: () => import('./views/Login.vue') },
]

5.2 用全局 beforeEach 统一拦截

router.beforeEach((to) => {
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth)
if (!requiresAuth) return

if (!isLoggedIn()) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
为什么用 to.matched.some(...)

因为 meta 会在嵌套路由中“叠加”,to.matched 能拿到从父到子的所有路由记录,更稳。

5.3 登录后跳回原页面

// 登录成功后
const redirect = (route.query.redirect as string) || '/'
router.replace(redirect)
防止无限重定向

如果登录页本身也被标记了 requiresAuth,会造成死循环。通常要保证 /login 不做鉴权,或在守卫里排除它。

6. 实战 2:离开页面前“未保存确认”

离开确认最适合用 组件内离开守卫(因为它和当前页面状态强相关)。

6.1 Options API:beforeRouteLeave

<script>
export default {
data() {
return { dirty: false }
},
beforeRouteLeave() {
if (!this.dirty) return true
return window.confirm('内容还没保存,确定要离开吗?')
},
}
</script>

6.2 Composition API:onBeforeRouteLeave

import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'

export default {
setup() {
const dirty = ref(false)

onBeforeRouteLeave(() => {
if (!dirty.value) return true
return window.confirm('内容还没保存,确定要离开吗?')
})

return { dirty }
},
}

7. 重点:导航守卫的执行顺序(完整流程)

下面这个顺序,是你排查“为什么某个守卫没执行 / 执行时机不对”的关键。

7.1 一张图记住执行链路

7.2 对应的文字版顺序(便于背诵)

  1. 触发离开组件的 beforeRouteLeave
  2. 调用全局 beforeEach
  3. 在复用组件中调用 beforeRouteUpdate
  4. 调用路由配置的 beforeEnter
  5. 解析异步路由组件
  6. 在被激活的组件中调用 beforeRouteEnter
  7. 调用全局 beforeResolve
  8. 导航被确认
  9. 调用全局 afterEach
  10. 触发 DOM 更新
  11. 调用 beforeRouteEnternext 的回调函数
记忆口诀(可选)

离开 → 全局前置 → 复用更新 → 独享进入 → 异步解析 → 组件进入 → 全局解析 → 确认 → 后置 → DOM → 回调

8. 进阶:嵌套路由下,组件内守卫的“层级顺序”

当页面是嵌套结构时(父子 router-view),组件内守卫还会受“路由层级”的影响:

  • 离开类守卫(Leave):通常是 从子到父(先离开最内层)
  • 进入类守卫(Enter):通常是 从父到子(先进入外层,再进入内层)
注意:不同类型守卫“混在一起”时仍按第 7 节总顺序推进

比如离开组件的 beforeRouteLeave(子→父)执行完后,才会进入全局 beforeEach,再到 beforeEnter 等。

9. 常见坑与最佳实践

9.1 不要在 afterEach 里做鉴权

鉴权应该在 beforeEach / beforeEnter,否则页面都渲染了才发现没权限,体验很差,而且 afterEach 也无法拦截。

9.2 小心“守卫里再跳转”导致循环

守卫里重定向时一定要加条件:

router.beforeEach((to) => {
if (to.name === 'Login') return
if (!isLoggedIn()) return { name: 'Login', query: { redirect: to.fullPath } }
})

9.3 守卫要“快”,副作用要“少”

  • 守卫越慢,用户越感觉“点了没反应”
  • 能放到页面内部做的事情,尽量别阻塞导航
  • 如果必须等接口:建议配合 loading(或骨架屏)给反馈

10. 面试高频问答

Q1:Vue Router 有哪些导航守卫?各自适合做什么?

参考回答:

导航守卫常分三类:全局守卫(beforeEach / beforeResolve / afterEach)、路由独享守卫(beforeEnter)、组件内守卫(beforeRouteLeave / beforeRouteUpdate / beforeRouteEnter,以及 Composition API 的 onBeforeRouteLeave / onBeforeRouteUpdate)。鉴权一般放 beforeEach,局部权限放 beforeEnter,离开确认放 beforeRouteLeave,数据准备可放 beforeResolve,埋点/标题等副作用放 afterEach

Q2:完整的守卫执行顺序是什么?

参考回答:

典型顺序是:beforeRouteLeave → 全局 beforeEachbeforeRouteUpdate → 路由 beforeEnter → 解析异步组件 → beforeRouteEnter → 全局 beforeResolve → 确认导航 → 全局 afterEach → DOM 更新 → beforeRouteEnter 的回调执行。

Q3:beforeEachbeforeResolve 的区别?

参考回答:

beforeEach 更靠前,适合做“尽早拦截”(鉴权/重定向);beforeResolve 更靠后,会在组件内守卫和异步组件解析完成后触发,适合做“确保组件已解析再做的事情”,比如需要依赖路由组件已经确定的预加载逻辑。

Q4:为什么 beforeRouteEnter 里不能用 this

参考回答:

因为它执行时组件实例还没创建,所以拿不到 this。如果需要访问实例,可以通过 next((vm) => {}) 的回调在导航确认、DOM 更新后拿到组件实例。

Q5:Vue Router 4 为什么更推荐“返回值”而不是 next()

参考回答:

返回值写法更直观:return false 取消、return { name: 'Login' } 重定向、return true/undefined 放行。next() 风格容易踩坑(忘记调用、调用两次、异步分支遗漏),维护成本更高。

Q6:beforeRouteUpdate 什么时候触发?和 watch($route) 有什么区别?

参考回答:

当从 /users/1 跳到 /users/2 这类“同一路由组件复用、参数变化”的场景时,组件实例不会销毁重建,会触发 beforeRouteUpdate / onBeforeRouteUpdate。它的优势是更贴近“路由生命周期”,触发时机更明确;而 watch($route) 更偏向响应式监听,适合在组件内部做 UI 联动,但容易把“路由变化逻辑”分散到多个 watcher 里。

Q7:守卫里如何写异步逻辑?失败时怎么处理?

参考回答:

可以把守卫写成 async,在里面 await 接口;需要拦截就 return false 或返回重定向位置。请求失败可以 return false(取消导航)或 throw 抛出错误并交给 router.onError/全局错误上报处理;同时建议配合 loading,避免用户感觉“卡住”。