路由守卫的核心作用是在导航发生前后“拦一道”,让你有机会决定“让不让用户进这个页面”或者“离开页面前要不要提醒保存”。Vue Router 提供了三种粒度的守卫,覆盖了从全局到单个路由再到具体组件的控制需求。
全局守卫:把控所有路由的“总闸门”
全局守卫直接注册在 router 实例上,对每一次路由跳转都生效,最适合做登录拦截、页面标题修改、全局埋点这类通用逻辑。
beforeEach:进入任何路由前触发,最常用的守卫。beforeResolve:在所有组件内守卫和异步路由组件解析之后触发,使用频率较低。afterEach:导航确认之后触发,不能阻止导航,常用于修改页面标题或上报 PV。
// router/index.js
const router = createRouter({ ... })
router.beforeEach((to, from, next) => {
// 典型场景:登录拦截
const token = localStorage.getItem('token')
// 如果要去的页面需要登录,且当前没有 token
if (to.meta.requiresAuth && !token) {
// 跳转到登录页,并带上来源路径,登录后可以跳回来
next({ path: '/login', query: { redirect: to.fullPath } })
} else {
next() // 放行
}
})
router.afterEach((to) => {
document.title = to.meta.title || '默认标题'
})
关键点:next 函数是守卫放行的唯一出口(除了 afterEach 不需要)。你可以调用 next() 放行、next(false) 中断、next('/path') 强制跳转。在 Vue Router 4 中,也可以返回一个路径字符串或对象来替代 next 调用,更简洁:
router.beforeEach((to) => {
if (to.meta.requiresAuth && !getToken()) {
return '/login' // 相当于 next('/login')
}
})
路由独享守卫:给特定路由“上把锁”
有些逻辑只需要作用在某一条或某几条路由上,这时可以定义在路由配置的 beforeEnter 中:
const routes = [
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, role: 'admin' },
beforeEnter: (to, from, next) => {
// 仅 /admin 路由触发
const userRole = getUserRole()
if (userRole !== 'admin') {
next('/403') // 无权限
} else {
next()
}
}
}
]
beforeEnter 的签名与全局 beforeEach 完全一致,但它只负责当前路由。当你在一个复杂应用里,不同业务模块由不同权限控制时,把权限规则写在各自路由配置的 beforeEnter 中,能让代码更内聚,避免全局守卫变成一个巨大的 if-else 地狱。
组件内守卫:在组件级别精确控制
当你需要更细粒度的控制——比如“进入某组件前拉取数据”、“离开某组件前确认是否有未保存内容”——就可以在组件选项或组合式 API 中使用组件内守卫。Vue Router 提供了三个钩子:
| 选项式 API | 组合式 API (vue-router) | 触发时机 |
|-----------|----------------|---------|
| beforeRouteEnter | onBeforeRouteEnter | 进入该组件的路由被确认前,此时组件实例尚未创建,无法访问 this。 |
| beforeRouteUpdate | onBeforeRouteUpdate | 当前路由改变但组件被复用时调用(如 /user/1 → /user/2)。 |
| beforeRouteLeave | onBeforeRouteLeave | 离开当前组件对应的路由时调用,常用于阻止用户意外离开。 |
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// 场景1:表单未保存时阻止离开
onBeforeRouteLeave((to, from, next) => {
if (formChanged.value) {
const answer = window.confirm('你有未保存的更改,确定离开吗?')
if (!answer) return next(false)
}
next()
})
// 场景2:路由参数变化时重新请求数据
onBeforeRouteUpdate(async (to, from, next) => {
await fetchUserData(to.params.id)
next()
})
</script>
三个注意点:
beforeRouteEnter中拿不到组件实例:因为组件还没创建。如果确实需要在导航确认后操作组件,可以通过next的回调参数延迟访问:
beforeRouteEnter(to, from, next) {
next(vm => {
// vm 就是组件实例
vm.loadData()
})
}
不过组合式 API 通常建议用 onMounted 替代这种“远古”写法。
beforeRouteUpdate是处理动态路由复用的关键。假如同一个组件展示不同id的文章,切换id时组件不会销毁重建(keep-alive 下更明显),此时onMounted不会重新触发,需要用这个钩子来响应参数变化。
- 守卫的完整执行顺序(导航触发后):
beforeRouteLeave(离开组件)beforeEach(全局)beforeRouteUpdate(复用组件,如果有)beforeEnter(路由独享)- 解析异步路由组件
beforeRouteEnter(进入目标组件)beforeResolve(全局)- 导航确认
afterEach(全局)- 触发组件生命周期
权限控制的标准实现
结合以上三种守卫,一个典型的中后台系统权限控制可以这样分步落地:
第一步:定义路由时标记权限
const routes = [
{ path: '/login', component: Login },
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true, roles: ['admin', 'editor'] }
},
{
path: '/settings',
component: Settings,
meta: { requiresAuth: true, roles: ['admin'] }
}
]
第二步:全局守卫做统一登录拦截
router.beforeEach((to) => {
const token = getToken()
if (to.meta.requiresAuth && !token) {
return `/login?redirect=${to.fullPath}`
}
})
第三步:路由独享守卫(或全局守卫扩展)做角色校验
// 可以在全局守卫中加入角色判断,也可以像下面这样用路由独享守卫
{
path: '/settings',
component: Settings,
meta: { roles: ['admin'] },
beforeEnter(to, from, next) {
const user = getUserInfo()
if (!to.meta.roles.includes(user.role)) {
next('/403')
} else {
next()
}
}
}
实际项目中,为了避免在每个路由里重复写角色判断逻辑,通常会把它封装成一个函数,并在全局守卫里统一读取 to.meta.roles 来判断,保持代码清爽。
总结:路由守卫让页面级的“安检”变得结构化。全局守卫管大门,路由独享守卫管敏感区域,组件内守卫管局部细节。三者搭配,既能写出安全的登录拦截,又不会让权限逻辑散落各处难以维护。