Vue Router 导航守卫(路由守卫)详解
在做 Vue 单页应用(SPA)时,路由跳转往往不只是“换个页面”这么简单:
- 需要登录才能访问
/profile - 进入某个页面前要先拉取数据
- 表单没保存就离开要二次确认
- 需要做埋点统计、动态设置标题
这些“在路由跳转的某个阶段插入逻辑”的能力,就是 Vue Router 导航守卫(Navigation Guards)。
本文以 Vue Router 4(Vue 3) 为主,讲清楚:守卫有哪些、怎么写、最重要的 执行顺序 是什么,以及实战与常见坑。
1. 导航守卫到底是什么?
你可以把一次导航(router.push() / 点击 <router-link>)理解成一条“流水线”:
- 计算从哪里来(
from)到哪里去(to) - 判断是否允许离开当前页面
- 判断是否允许进入目标页面
- 解析异步组件(路由懒加载)
- 确认导航,更新 URL,渲染新页面
守卫就是这条流水线上的“关卡”。它可以:
- 放行:继续执行后续步骤
- 取消:终止这次导航
- 重定向:跳去另一个路由(常见:跳登录页)
2. 守卫分哪几类?(在哪里写)
Vue Router 常见守卫分三大类(再加上组合式 API 的写法):
| 类别 | 典型 API | 写在哪里 | 常见用途 |
|---|---|---|---|
| 全局守卫 | beforeEach / beforeResolve / afterEach | router 实例上 | 登录鉴权、埋点、标题、全局数据准备 |
| 路由独享守卫 | beforeEnter | 某条路由配置里 | 某个模块的权限、局部拦截 |
| 组件内守卫 | beforeRouteLeave / beforeRouteUpdate / beforeRouteEnter | 组件选项里(Options API) | 离开确认、参数变化刷新、进入后拿实例 |
| 组合式守卫 | onBeforeRouteLeave / onBeforeRouteUpdate | setup() 内 | 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 对应的文字版顺序(便于背诵)
- 触发离开组件的
beforeRouteLeave - 调用全局
beforeEach - 在复用组件中调用
beforeRouteUpdate - 调用路由配置的
beforeEnter - 解析异步路由组件
- 在被激活的组件中调用
beforeRouteEnter - 调用全局
beforeResolve - 导航被确认
- 调用全局
afterEach - 触发 DOM 更新
- 调用
beforeRouteEnter中next的回调函数
离开 → 全局前置 → 复用更新 → 独享进入 → 异步解析 → 组件进入 → 全局解析 → 确认 → 后置 → DOM → 回调
8. 进阶:嵌套路由下,组件内守卫的“层级顺序”
当页面是嵌套结构时(父子 router-view),组件内守卫还会受“路由层级”的影响:
- 离开类守卫(Leave):通常是 从子到父(先离开最内层)
- 进入类守卫(Enter):通常是 从父到子(先进入外层,再进入内层)
比如离开组件的 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 → 全局 beforeEach → beforeRouteUpdate → 路由 beforeEnter → 解析异步组件 → beforeRouteEnter → 全局 beforeResolve → 确认导航 → 全局 afterEach → DOM 更新 → beforeRouteEnter 的回调执行。
Q3:beforeEach 和 beforeResolve 的区别?
参考回答:
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,避免用户感觉“卡住”。