导航守卫 12 步解析顺序与三个触发边界
站内另一篇讲的是用守卫做鉴权时容易踩到的坑(无限重定向、刷新丢动态路由、redirect 开放跳转),这篇只解决一个问题:导航被触发之后,这些守卫按什么顺序执行,以及哪些情况下根本不会进入守卫。
先记住 12 步,而不是记一堆钩子名
官方文档给出的完整导航解析流程:
- 导航被触发
- 在失活的组件里调用
beforeRouteLeave - 调用全局
beforeEach - 在重用的组件里调用
beforeRouteUpdate(2.2+) - 在路由配置里调用
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter - 调用全局
beforeResolve(2.5+) - 导航被确认
- 调用全局
afterEach - 触发 DOM 更新
- 执行
beforeRouteEnter中传给next的回调,组件实例作为参数传入
这张表的顺序可以直接推出三个结论:
beforeEnter排在beforeRouteUpdate之后。同一个路由记录内参数变化(/users/2→/users/3)走的是第 4 步,第 5 步根本不执行。- 全局守卫的顺序是
beforeEach→beforeResolve→afterEach,越靠后能改的东西越少:afterEach不接收next,也改不了导航。 beforeRouteEnter在第 7 步执行,那时组件实例还没创建,this是undefined;能拿到实例的只有第 12 步的next回调。
边界一:beforeEnter 只管「从别的路由进来」
很多人写:
{
path: '/users/:id',
component: UserDetail,
beforeEnter: (to, from) => {
// 想把「这个 id 我有没有权限看」放这里
if (!canView(to.params.id)) return '/403'
},
}
第一次从列表页点进 /users/2,它执行;用户在详情页里点「下一条」跳到 /users/3,URL 变了、组件复用了,但 beforeEnter 不跑。官方文档写得很直白:beforeEnter 只在进入路由时触发,不会在 params、query 或 hash 改变时触发,只有从一个不同的路由导航时才触发。
要处理复用场景,有两种写法:
// 选项式 API
beforeRouteUpdate(to, from) {
this.load(to.params.id)
}
// 里没有这个选项,用组合式函数
import { onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate((to) => {
load(to.params.id)
})
或者干脆 watch 参数。还有第三种情况容易漏:/users/2#info → /users/2#projects 只改 hash,同样不进任何进入类守卫。
边界二:数据请求放 beforeResolve,不放 afterEach
beforeResolve 是最后一个全局守卫,位置在「所有组件内守卫和异步路由组件都解析完、导航还没确认」之间。官方对它用途的描述就是:拉数据,或者执行那些「用户进不去这个页面就不该做」的操作。
对比一下三个位置:
| 位置 | 能拦导航 | 异步组件是否已加载 | 适合做什么 |
|---|---|---|---|
| beforeEach | 能 | 否 | 登录态、路由级权限判断 |
| beforeResolve | 能 | 是 | 依赖已加载组件的取数、权限二次确认 |
| afterEach | 不能 | 是 | 埋点、改标题、滚动复位 |
放 afterEach 里请求也能跑,但那时导航已确认、DOM 已更新,请求失败只能自己在页面里兜——用户看到的是先闪出一个空页面再报错。要「进不去就别请求」,就得往前放。
边界三:返回值只有三种语义,与 next 等价
Vue Router 4 里守卫可以返回值,也可以继续用第三个参数 next,两者等价:
| 返回值 | next 写法 | 结果 |
|---|---|---|
false | next(false) | 取消导航;若浏览器 URL 已变(手输地址或点后退),URL 重置回 from |
| 一个路由地址 | next('/login') 或 next({ name: 'Login' }) | 中断当前导航,以同一个 from 起一个新导航;可带 replace: true |
undefined 或 true | next() | 放行,进入下一个守卫 |
抛 Error | next(new Error()) | 取消导航并交给 router.onError 注册的回调 |
next 的经典错误是调用两次:
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
next() // 上面已经 next 过一次了
})
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
官方说明 next 仍然被支持,但已经通过 RFC 决定移除,原因是它是最常见的错误来源。新代码直接返回值,不要混着写——混用 async/await 和 next 时,守卫可能永远不 resolve,页面就停在那里不动。
异步守卫挂起时,URL 和视图本来就可以不一致
守卫是异步解析的,所有守卫 resolve 之前导航一直处于 pending。这期间如果用户手改了地址栏、或者按了后退,浏览器 URL 已经变了,但导航没确认:
- 最终放行,视图跟上去;
- 最终
return false,URL 被重置回from。
所以调试时看到「地址栏是 /orders,页面还是 /users」,先别怀疑 router 坏了,去检查某个守卫里是不是有个没 return 的 await 分支。
快速自检
- 权限判断放在
beforeEach,取数据放在beforeResolve,埋点放在afterEach。 - 路由参数变化要不要刷新,用
onBeforeRouteUpdate或 watch 参数,不要指望beforeEnter。 - 新代码只用返回值,不用
next。 - 有
await的守卫,每条分支都要return。
官方文档:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html