Skip to content
导航守卫机制
概述
Vue Router 的导航守卫不是事件系统,而是一个异步串行管道。
push、replace、popstate、手动修改 hash——所有路由跳转最终都收敛到同一个入口 navigate()。守卫按固定顺序执行,任意一环拒绝(返回 false 或抛出错误)都会终止整条管道,路由停留在当前位置。
完整解析流程如下:
text
触发导航
→ 离开组件: beforeRouteLeave
→ 全局: beforeEach
→ 复用组件: beforeRouteUpdate
→ 路由配置: beforeEnter
→ 解析异步组件
→ 激活组件: beforeRouteEnter
→ 全局: beforeResolve
→ 导航确认
→ 全局: afterEach
→ DOM 更新
→ beforeRouteEnter 的 next 回调执行注意 afterEach 在 DOM 更新之前执行,beforeRouteEnter 的 next 回调则在 DOM 更新之后触发。
守卫类型与注册
全局守卫
js
const router = createRouter({ /* ... */ })
router.beforeEach((to, from, next) => { /* ... */ })
router.beforeResolve((to, from, next) => { /* ... */ })
router.afterEach((to, from, failure) => { /* ... */ })beforeEach 与 beforeResolve 的差别在于触发时机:beforeResolve 在所有组件内守卫执行完毕、异步组件加载完成后才会触发。如果校验逻辑依赖异步组件内定义的信息(比如组件自身的 meta),需要用 beforeResolve;只依赖路由配置本身的 meta 时,beforeEach 就够了。
afterEach 不接受 next 参数,此时导航已经确认。第三个参数 failure 在导航被取消或出错时有具体值,正常完成时为 undefined。
路由级守卫
js
const routes = [
{
path: '/admin',
component: AdminLayout,
beforeEnter: (to, from, next) => {
// 仅在进入 /admin 时触发,离开不触发
}
}
]beforeEnter 在 beforeRouteUpdate 之后、beforeRouteEnter 之前执行,仅在进入该路由配置时触发一次。
组件内守卫
js
export default {
beforeRouteEnter(to, from, next) {
// 组件实例尚未创建,this 不可用
next(vm => { /* vm 是组件实例 */ })
},
beforeRouteUpdate(to, from, next) {
// 路由参数变化但组件复用时触发,this 可用
},
beforeRouteLeave(to, from, next) {
// 离开当前路由时触发,this 可用
}
}关键行为:
beforeRouteEnter执行时组件实例还未创建,无法访问this。如果需要在组件创建后操作,只能通过next(callback),回调中的vm即为组件实例。beforeRouteUpdate只在复用同一组件、路由参数变化时触发(如/user/1→/user/2)。从其他路由跳过来不会触发。beforeRouteLeave是整个管道中第一个执行的钩子,用于判断是否允许离开当前页。常见用途是表单未保存拦截。注意不要在其中做长时间的异步网络请求——用户在点击跳转后会直接感知延迟。一般做法是维护一个isDirty状态,beforeRouteLeave同步检查该状态即可,实际的保存操作在其他地方触发。
工作原理
navigate() 入口
navigate() 是 vue-router 4 中所有导航的统一入口。去除错误处理和边界条件后的核心路径如下:
js
function navigate(to, from) {
// 1. 提取被激活、失活、复用的组件记录
const [entering, leaving, updating] = extractChangingRecords(to, from)
// 2. 按固定顺序组装守卫队列
const guards = [
...extractComponentsGuards(leaving, 'beforeRouteLeave'),
/* 全局 beforeEach 注入为守卫对象 */
...extractGuards(globalBeforeGuards),
...extractComponentsGuards(updating, 'beforeRouteUpdate'),
/* 路由配置的 beforeEnter */
...extractGuards(routeBeforeEnterGuards),
/* 解析异步组件 */
...extractComponentsGuards(entering, 'beforeRouteEnter'),
/* 全局 beforeResolve */
...extractGuards(globalResolveGuards),
]
// 3. 串行执行
return runGuardQueue(guards, to, from)
.then(() => {
// 守卫全部通过 → 确认导航
})
.catch(err => {
// 任何守卫拒绝 → 中止导航
})
}extractChangingRecords(to, from) 通过对比新旧路由的 matched 数组将记录分为三类:
leaving:旧路由有、新路由没有的记录 → 提取beforeRouteLeaveupdating:新旧共享的记录 → 提取beforeRouteUpdateentering:新路由有、旧路由没有的记录 → 提取beforeRouteEnter
对比过程在匹配记录的 diff 阶段完成,复杂度 O(n)。
runGuardQueue:promise 链
runGuardQueue 是守卫的执行引擎,用 reduce 将守卫数组折叠为串行的 promise 链:
js
function runGuardQueue(guards, to, from) {
return guards.reduce(
(promise, guard) => promise.then(() => guard(to, from)),
Promise.resolve()
)
}每个守卫的执行结果通过 guardToPromiseFn 统一转换为 promise,决定下一环是继续、重定向还是中止:
js
function guardToPromiseFn(guard, to, from) {
return () => new Promise((resolve, reject) => {
const result = guard.call(null, to, from, (nextArg) => {
// next() 被调用
if (nextArg === false || isError(nextArg)) {
reject(nextArg)
} else if (nextArg && typeof nextArg === 'string') {
reject(createRouterError(2, to, from, nextArg)) // NavigationGuardRedirect
} else {
resolve()
}
})
// guard 返回了 promise
if (isPromise(result)) {
result.then(resolve, reject)
} else if (result !== undefined) {
resolve()
}
})
}几个关键行为:
next(false)与return false等价:导航中止,停留在当前路由。next('/login')与return '/login'等价:触发重定向导航,内部产生一个错误码为 2 的NavigationGuardRedirect。next()(无参数)与return undefined等价:通过守卫。- 如果同一守卫内既调了
next又return,以先执行的next为准。尽管如此,不应依赖这种写法,应明确二选一。
next() 与 return 值的选择
next() 存在一个隐患:如果守卫内先调了 next(),后续又执行了其他逻辑,或者异步逻辑中多次调 next(),行为会不可预测。
js
// 危险:next 被多次触发
router.beforeEach((to, from, next) => {
fetchUser().then(user => {
if (!user) next('/login')
})
next() // 先执行到这里,导航已经继续
})return 值是单向的、只生效一次,promise 模式天然避免了这个问题。Vue Router 4 保留 next() 主要是向后兼容,新代码应当使用 return 值:
js
// return 值:单向,不会多次触发
router.beforeEach(async (to, from) => {
const user = await fetchUser()
if (!user) return '/login'
})导航取消与重定向
当守卫返回 false 或重定向路径时,Vue Router 内部会抛出一个 NavigationFailure 对象。常见的失败类型如下:
js
// 源码中的类型常量
// aborted: 4 (新导航中止了当前导航)
// cancelled: 8 (守卫返回 false)
// duplicated: 16 (重复导航,如 router.push 当前相同路由)重定向并不是独立的 NavigationFailure 类型,而是用错误码 2 的 NavigationGuardRedirect 来表示。router.push() 返回的 promise 会 reject 这个失败对象:
js
// 如果导航被 beforeEach 取消,push 的返回值会 reject
try {
await router.push('/admin')
} catch (e) {
if (isNavigationFailure(e, NavigationFailureType.cancelled)) {
// 守卫取消导航,属于预期行为
} else {
throw e // 真正的错误,继续向上抛
}
}新导航中断旧导航
如果当前有一条正在执行的导航管道(守卫处于异步等待中),此时用户触发了新的导航,旧管道会被中止:
text
导航 A: 正在执行 beforeRouteEnter 中的异步逻辑...
导航 B: router.push('/other') 触发
→ 导航 A 的 promise 被 reject(NavigationAborted, type 4)
→ 导航 B 的管道开始执行Vue Router 内部维护了当前导航的 abort controller,新导航触发时调用 abort(),旧导航在下一个守卫的 promise.then 中检测到 abort 信号后终止。同一时间只有一条活跃的导航管道。
popstate 与浏览器历史栈
浏览器前进/后退同样经过守卫管道:
text
用户点击浏览器后退
→ popstate 事件触发
→ router 将 popstate 的 state 转换为目标路由
→ 调用 navigate(to, from) ← 与 push/replace 同一入口
→ 守卫管道正常执行
→ 守卫返回 false → popstate 无效,回补当前页的记录关键时序:浏览器 URL 的更新发生在守卫全部通过之后,而不是 router.push() 被调用时。
js
function pushWithRedirect(to) {
navigate(to, currentRoute)
.then(finalRoute => {
// 守卫通过后才更新 URL
history.pushState(finalRoute.state, '', finalRoute.fullPath)
// 触发响应式更新
currentRoute.value = finalRoute
})
.catch(err => {
// 守卫取消 → URL 不变,currentRoute 不变
if (isNavigationFailure(err, NavigationFailureType.cancelled)) {
// 不做任何事
}
})
}因此在 beforeEach 中做异步操作(如权限校验)时,URL 不会提前变化。
scrollBehavior 的时机
scrollBehavior 不属于守卫,但执行时机与守卫管道紧密相关:
text
守卫管道全部通过
→ 导航确认
→ DOM 更新完成
→ 等待 nextTick(Vue 的 DOM flush)
→ 执行 scrollBehavior()如果守卫中触发了重定向(return '/login'),scrollBehavior 不会在原路由上执行——它只在新导航首次成功时触发。多个连续重定向(A → B → C)只在最终的 C 路由上执行一次。
注意事项
守卫死循环
text
/login → beforeEach 检测到无 token → return '/login' → 再次进入 /login 的 beforeEach → ...Vue Router 4 对此有检测:连续重定向到同一路由超过 10 次(默认),会抛出 maxRedirected 错误。
修复方式是在守卫的 return 条件中排除目标路由已经是自身的情况:
js
// 错误:没有排除目标路由
router.beforeEach((to) => {
if (!hasToken()) return '/login'
})
// 正确:检测目标路由
router.beforeEach((to) => {
if (!hasToken() && to.path !== '/login') return '/login'
})守卫未返回任何值
守卫执行完毕但没有 return 也没有调用 next,导航会一直卡在 pending 状态。表现为 URL 不变,页面不跳转,没有报错。
在 async 函数中更容易出现:
js
router.beforeEach(async (to, from) => {
const permissions = await fetchPermissions()
// 忘记 return —— 导航永远不会完成
if (!permissions.includes(to.meta.requiredPermission)) {
return false
}
})确保每个分支都有明确的 return 值。
异步组件加载失败
在 beforeEnter 之后、beforeRouteEnter 之前,Vue Router 会解析所有异步组件(() => import('./Foo.vue'))。如果组件加载失败(网络错误、chunk 缺失),导航中止。
异步组件加载失败不会触发 afterEach。可以通过 router.onError 捕获这类错误:
js
router.onError((error) => {
// error 可能是 chunk load failed
})守卫错误不被 Vue 的 errorHandler 捕获
守卫执行在 Vue Router 内部独立的 promise 链中,Vue 组件的 errorCaptured 和 app.config.errorHandler 不会收到守卫中抛出的错误。需要使用 router.onError() 或在守卫内部 try/catch。
与 React Router 的差异
React Router 6 没有等价的“导航守卫”概念。路由切换是声明式的——匹配到新组件就渲染,没有管道。权限控制通常通过高阶组件或布局路由内的 useEffect 重定向实现:
jsx
function ProtectedRoute({ children }) {
if (!hasToken()) return <Navigate to="/login" replace />
return children
}时序上的关键区别在于,React Router 的 Navigate 是渲染阶段的重定向——组件树需要先渲染 ProtectedRoute,发现没有 token,再渲染 <Navigate>,再触发新导航。而 Vue Router 的守卫在进入组件之前拦截,组件根本不会被实例化。
React Router 中没有等价的 beforeRouteLeave。表单未保存拦截通常通过 useBlocker(v6.4+)或 window.addEventListener('beforeunload') 实现。
