大型业务前端里,路由为什么不只是导航
业务后台刚起步时,路由通常只是页面地址的映射:访问 /users,展示用户列表;访问 /roles,展示角色管理。
但页面和角色逐渐增加后,真正麻烦的问题会接连出现:这个页面该不该出现在菜单里?没有某项权限时,是隐藏入口还是阻止访问?从详情页返回时,列表条件要不要保留?某个页面属于哪个二级导航?
这些问题都和路由有关,却很容易被分别放进菜单组件、权限指令、keep-alive 配置和页面自身的判断里。最后,同一条业务规则会散落在许多地方,修改起来既难查也难验。
这篇文章想讲一种在大型业务前端里用过的做法:**让 RouteMeta 承担菜单、页面权限和导航状态的共同声明。**守卫和缓存没有接入这份声明的地方,也需要如实保留。
本文是「教务前端架构实践」系列的一篇。系列以一个已脱敏的 Vue 3 教务管理前端为背景,分别讨论双端体验、导航与权限、会话治理和复杂业务交互。
它不会让复杂度消失,但至少能把页面规则放到评审时看得见的地方。
从“页面能打开”到“页面应该如何出现”
以教务系统为例,院校审核、培训、课程、考试和系统管理等业务域会持续扩张。一个页面是否存在,往往不是唯一问题;更常见的是它在不同上下文中应当以什么方式出现。
例如,一个路由除了 path 和 component,还需要表达这些信息:
- 哪些权限可以访问它;
- 哪些角色可以看到它;
- 它是否应出现在菜单中;
- 它是否需要被缓存;
- 从详情页返回时,哪个菜单项应保持激活;
- 它是独立页面、扁平菜单,还是某个分组下的入口。
这些判断一旦分散在各层,很快就会长出几套彼此不完全一致的“页面规则”。我更愿意把它们收敛成一份页面级声明,让路由元数据成为共同的起点。
flowchart LR
R[路由配置 + RouteMeta] --> M[菜单过滤与导航生成]
R --> P[页面渲染权限判断]
R --> T[页签与激活态]
U[用户角色与权限] --> M
U --> P
G[路由守卫:Token 与用户信息] --> P
C[缓存组件名白名单] --> K[keep-alive]
路由只负责把“页面级规则”放在一个入口声明。菜单、页面渲染权限和布局已经读取其中一部分,守卫和缓存有没有读取同一份声明,要回到实际实现确认。
RouteMeta 是一份页面契约,不是一袋随手塞进去的字段
这个项目没有另起一套路由配置类型,而是在 Vue Router 的 RouteMeta 上做类型增强。title、icon、permission、role、hideInMenu、flatMenu、activeMenu、keepAlive、group、type 与 hideInBreadcrumb 都在这里声明。各路由模块统一使用 RouteRecordRaw 的别名,字段会得到编辑器提示,也给各层留下同一份读取约定。
可以把它理解为一种页面契约。下面是经过简化的示意:
1 | { |
类型增强的最小形态如下。它先把允许出现的页面级字段收窄,业务规则仍由各层实现:
1 | import 'vue-router' |
实际配置里,列表页与详情页往往成对出现。列表页用 permission 和 keepAlive 表达入口与状态保留;详情、创建、编辑页设置 hideInMenu,再用 activeMenu 回指列表入口。同一页面下有多个 Tab 时,activeMenu 可以是数组,分别给一级和二级导航提供高亮依据。
这段配置只声明页面规则:页面需要什么权限语义;它不应单独成为菜单入口;用户身处这个页面时,菜单应高亮列表页。详情页的权限校验、菜单高亮和缓存仍由对应消费者实现,声明本身不等于准入。
这样做最直接的好处,是页面规则能跟着路由一起被评审。新增一个功能时,评审者看到的不只是页面组件挂到了哪个地址,还能看到它怎样接入现有的权限和导航体系。同时也少维护一份平行规则:详情页不必再手写 activeMenu 高亮逻辑,菜单过滤也不需要单独维护“哪些路由要隐藏”的名单。
前提是字段必须有稳定、有限的语义。比如 permission 用于描述页面权限语义,activeMenu 用于描述导航归属;如果一个字段同时承担“是否显示”“是否缓存”“是否允许编辑”等多重含义,元数据只会变成另一种难以维护的配置堆。
一份元数据,如何被不同层消费
把规则集中在 RouteMeta,不等于所有代码都挤进路由文件。更合理的分工是:路由负责声明,各个基础设施层按自己的职责消费。
菜单层解决“入口是否可见”
布局层会先根据路由树和当前用户的角色、权限筛选节点。permission、role 决定节点能否进入菜单树,hideInMenu 排除详情等非入口页,flatMenu 则把子项提升一层。
菜单还会读取更细的组织信息:二级菜单按 group 归类,type: 'button' 按按钮式入口处理,type: 'tab' 标识同页下的 Tab 路由。它不是把路由机械地平铺出来。
这让菜单不再是一份独立维护的数据结构。对于大多数业务入口,路由即菜单的主要数据来源,减少了两份配置逐渐漂移的风险。
但“可见”不是安全结论。隐藏菜单只能改善界面和减少误操作,真正的接口授权与数据范围控制仍必须由后端负责。
组件与指令层补足“页面和操作是否显示”
菜单过滤只能决定入口是否可见,不能决定已进入页面后的内容如何呈现。项目把当前路由的 meta.permission、meta.role 交给 Permission 组件判断:满足条件时渲染默认插槽,不满足时渲染可选的 fallback 插槽。页面因此可以保留统一的布局壳,并在权限不足时给出明确反馈。
更细粒度的操作入口则由 v-hasPermission、v-hasRole 指令处理。它们从用户 Store 读取当前权限或角色,绑定值支持单个标识或数组;不满足条件时移除对应 DOM 节点。组件解决页面级内容的替换,指令解决按钮、操作列等局部可见性,两者不需要重复维护另一份权限规则。
这三层分工要说清:filterAsyncRoutes 递归生成可见菜单;Permission 控制页面内容与 fallback;指令控制细粒度操作。它们都复用同一份权限判断工具,但并不等同于导航前准入。
路由守卫解决“登录态下页面是否可达”
用户直接输入 URL、从收藏夹进入页面,或者在旧标签页恢复时,都可能绕开菜单入口。守卫因此需要在导航发生前读取 Token、用户信息及路由元数据,决定是否允许进入。
这里必须区分目标与现状:当前实现中的全局守卫已检查 Token、用户信息和少量移动端身份校验,但尚未全局读取 RouteMeta.permission 或 RouteMeta.role。菜单关心“给不给入口”,页面渲染层会根据路由元数据展示权限 fallback;若希望在导航前阻止无权限 URL 访问,仍需把同一份权限语义接入守卫。
布局层解决“用户现在位于哪里”
详情、编辑、预览这类页面通常不适合作为独立菜单项,但用户进入后仍需要明确的导航上下文。activeMenu 可以把这些页面映射回一个稳定的父入口。
同样的思路也可以服务页签和缓存:由 keepAlive 描述页面是否保留实例,再由布局层和缓存容器据此执行,而不是要求每个页面自己处理生命周期策略。当前项目的 keepAlive 已存在于路由元数据中,但缓存容器仍维护组件名白名单,尚未读取该字段;这正是值得补齐的契约断点。
这一层的价值在于,页面组件可以把注意力放回业务交互。路由相关的“位置感”和“返回感”不再依赖页面之间彼此约定。
收益不只是少写几段 if 判断
它确实能少写一些重复判断,但我更看重的是:后来接手的人能从路由配置里看懂一个页面的边界。
一个业务域新增页面,可以沿着同一张检查表补全:它需要哪类权限、在菜单中的位置、是否应缓存、详情页激活哪个入口。配置本身就是对业务边界的一次显式说明。
依赖方向也更容易收住:路由模块声明页面能力,菜单、页面渲染权限和布局/页签各自消费已经接入的字段,业务页面不用反过来知道菜单怎样生成。导航前权限准入和缓存是例外,只有消费者真的读取元数据,它们才算进入这条链路。这样至少不会在页面里调用布局逻辑,或让菜单配置反向引用具体业务状态。
对于按业务域拆分的单体前端,这种方式尤其有效。API、路由和页面可以按领域各自组织,而跨领域的导航与权限规则则能通过统一契约获得一致的声明语义。
代价是路由会成为高价值的耦合点
规则收进路由后,路由配置的地位也会随之上升:菜单、页面渲染权限、激活态和分组导航都依赖它,导航前准入与缓存以后也可能接入。规则集中是好事,但一次错误配置的影响范围也更大。
在该项目中,项目实施域曾出现超过两千行的单个路由模块。这是一个信号:业务子域已经多到需要进一步拆分,继续把所有页面塞进同一个文件会让审阅和定位成本不断上升。
常见风险还有两类:
- 字段语义漂移:新增需求时,为了图快给既有字段附加新含义,最后没有人能确定某个值真正影响了哪些行为。
- 消费者不一致:菜单、守卫和缓存各自解释元数据,导致同一权限规则在不同入口下表现不同。
所以,路由元数据适合成为契约,但不适合成为无边界的“万能配置中心”。
让集中化可控:拆分、约束与验证
如果继续沿用这条路线,我会优先做三件事。
按业务子域组合路由
保留统一的 RouteMeta 类型,但把一个大领域继续拆成可组合的子模块。例如将“项目实施”按计划、任务、检查、成果等子域拆分,再由领域入口统一汇总。这样既不会破坏整体路由树,也能让修改尽量停留在局部。
给元数据建立最小契约
每个字段都应有明确的类型、默认行为和使用者。新增字段前,先回答三个问题:它描述的是页面事实,还是某个组件的私有实现?哪些基础设施层会消费它?缺失时系统应采用什么默认值?
只有前两个问题都有稳定答案时,它才适合进入 RouteMeta。
为关键规则增加自动校验
路由很适合做静态检查。例如校验菜单类型路由是否具备标题和权限声明,校验 activeMenu 是否指向有效入口,校验同一业务域是否出现重复 path。对于权限、缓存和菜单这类高复用规则,少量校验比依赖人工记忆可靠得多。
进一步说,路由守卫和菜单过滤也应成为最早补齐测试的对象。没有这些反馈,路由拆分或元数据调整很难做到安心重构。
结语:路由不该替代架构,但可以承载页面级架构
权限、菜单、缓存和详情页归属这些问题不会因框架变化而消失。它们可以散落在各处、彼此牵扯,也可以被写成一份能阅读、评审和验证的契约。
路由元数据作为业务编排层,只描述页面应具备的规则。菜单、页面渲染权限和布局已经消费其中一部分,守卫与缓存还要继续接入,才能形成完整闭环。
边界清楚后,路由就不只是一张导航表,也是一份页面如何融入系统的架构说明。
系列文章:一个仓库,如何同时承载 PC 后台与移动端|企业后台的登录态治理:如何处理 Token 刷新风暴与跨标签同步|从领域组件到考试入口:教务前端中的复杂交互抽象