Middleware核心原理与实战应用指南
又踩坑了,Vue Router 的 middleware 顺序不对
昨天在搞一个权限控制的功能,用的是 Vue 3 + Vue Router 4,想在路由跳转前校验用户有没有权限。结果发现中间件(middleware)的执行顺序完全乱了,明明写了三个中间件,却只执行了最后一个,前面两个直接被跳过。折腾了快俩小时,差点以为是 Vue Router 的 bug。
其实一开始我写得挺简单的,就按文档里说的,在 route 上加个 beforeEnter:
{
path: '/admin',
component: Admin,
beforeEnter: [authGuard, roleGuard, logGuard]
}
然后每个 guard 都是标准的 (to, from, next) => { ... } 写法。但一跑起来,只有 logGuard 被调用了,前两个压根没进。我第一反应是:是不是数组写法不支持?赶紧去翻 Vue Router 官方文档,结果发现——文档里确实写了支持数组!这就奇怪了。
排查过程:从怀疑人生到灵光一闪
我先试了把数组拆成单个函数,比如只留 authGuard,能跑;再加 roleGuard,也能跑。但一旦三个一起上,就只执行最后一个。这时候我开始怀疑是不是某个中间件里没调 next(),导致流程卡住了。于是我把每个 guard 都加上了 console.log,结果发现:前两个根本没被调用!
这就更诡异了。难道 Vue Router 对数组中间件的处理有 bug?我甚至去 GitHub 翻了 issues,还真有人提类似问题,但基本都是自己代码写错了。我反复检查,确认每个 guard 都 return 了,也调用了 next(),逻辑看起来没问题。
后来我灵光一闪:会不会是我在某个地方不小心把 beforeEnter 覆盖了?因为项目里用了动态路由,有些路由是在登录后 addRoute 加进去的。我全局搜了一下,果然!在权限模块初始化的时候,我写了这么一段:
router.addRoute({
path: '/admin',
component: Admin,
beforeEnter: logGuard // 这里只传了一个!
})
而之前静态路由里已经定义过 /admin 了,结果动态添加的时候,把原来的 beforeEnter 数组整个替换成单个函数了。所以最后生效的只有 logGuard。我真是服了自己,这种低级错误居然花了这么久才发现。
核心代码就这几行
找到问题就好办了。解决方案很简单:要么别重复定义同一路由,要么合并中间件。我选了后者,因为有些中间件是全局的,有些是动态加的,需要灵活组合。
我写了个工具函数,用来合并中间件数组:
function mergeGuards(existing, newGuard) {
if (!existing) return Array.isArray(newGuard) ? newGuard : [newGuard];
const existingArray = Array.isArray(existing) ? existing : [existing];
const newArray = Array.isArray(newGuard) ? newGuard : [newGuard];
return [...existingArray, ...newArray];
}
然后在动态 addRoute 的时候这么用:
// 假设 routeConfig 是原始路由配置
const originalRoute = router.options.routes.find(r => r.path === '/admin');
const combinedGuards = mergeGuards(
originalRoute?.beforeEnter,
[roleGuard, logGuard] // 动态要加的
);
router.addRoute({
...originalRoute,
beforeEnter: combinedGuards
});
不过这样还是有点麻烦,因为 router.options.routes 在 Vue Router 4 里其实不推荐直接读取。更好的做法是:把所有中间件都集中管理,动态路由时直接传完整的 guard 列表。
所以我最终重构了一下,把权限相关的中间件抽成一个配置:
// guards.js
export const authGuard = (to, from, next) => {
console.log('auth check');
if (isAuthenticated()) next();
else next('/login');
};
export const roleGuard = (to, from, next) => {
console.log('role check');
if (hasRole(to.meta.requiredRole)) next();
else next('/unauthorized');
};
export const logGuard = (to, from, next) => {
console.log(navigating to ${to.path});
next();
};
// 按路由分组
export const adminGuards = [authGuard, roleGuard, logGuard];
然后静态路由直接引用:
{
path: '/admin',
component: Admin,
beforeEnter: adminGuards,
meta: { requiredRole: 'admin' }
}
动态添加时也用同一个数组,避免覆盖问题。这样就彻底解决了。
踩坑提醒:这三点一定注意
- 不要重复定义同一路由:Vue Router 的 addRoute 如果 path 相同,会覆盖已有路由,包括所有配置。这点文档里其实提了,但我当时没注意。
- beforeEnter 支持数组,但必须是完整的:你不能指望它自动合并,每次赋值都是全量替换。
- 中间件顺序很重要:比如 auth 必须在 role 之前,否则还没登录就去校验角色,会出错。我一开始顺序写反了,导致 hasRole 报错,又浪费了十分钟。
另外,虽然 Vue Router 本身没有叫 “middleware” 的概念,大家习惯把 beforeEach、beforeEnter 这些叫中间件,但要注意它们的执行时机不同。beforeEach 是全局的,beforeEnter 是路由级别的。我这次的问题纯粹是路由级中间件的管理问题,跟全局的没关系。
改完之后,三个 guard 都正常执行了,日志也按顺序打出来了。不过有个小瑕疵:如果用户快速连续点击,可能会触发多次导航,guard 也会多次执行。但这属于业务逻辑优化范畴,不影响核心功能,我就先不管了,反正线上环境加了防抖。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如有没有办法让 Vue Router 自动合并同一路由的 beforeEnter?或者用 composition API 的方式封装 guard?我都挺感兴趣的。

暂无评论