Middleware核心原理与实战应用指南

UE丶秀兰 框架 阅读 2,702
赞 8 收藏
二维码
手机扫码查看
反馈

又踩坑了,Vue Router 的 middleware 顺序不对

昨天在搞一个权限控制的功能,用的是 Vue 3 + Vue Router 4,想在路由跳转前校验用户有没有权限。结果发现中间件(middleware)的执行顺序完全乱了,明明写了三个中间件,却只执行了最后一个,前面两个直接被跳过。折腾了快俩小时,差点以为是 Vue Router 的 bug。

Middleware核心原理与实战应用指南

其实一开始我写得挺简单的,就按文档里说的,在 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” 的概念,大家习惯把 beforeEachbeforeEnter 这些叫中间件,但要注意它们的执行时机不同。beforeEach 是全局的,beforeEnter 是路由级别的。我这次的问题纯粹是路由级中间件的管理问题,跟全局的没关系。

改完之后,三个 guard 都正常执行了,日志也按顺序打出来了。不过有个小瑕疵:如果用户快速连续点击,可能会触发多次导航,guard 也会多次执行。但这属于业务逻辑优化范畴,不影响核心功能,我就先不管了,反正线上环境加了防抖。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。比如有没有办法让 Vue Router 自动合并同一路由的 beforeEnter?或者用 composition API 的方式封装 guard?我都挺感兴趣的。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论