前阵子在某个企业后端平台项目里,接了个代号叫“REA”的系统重构任务。一开始大家看到这个名字都挺懵,以为是哪个框架的新版本,后来开需求会才明白,REA 是 Reusable Enterprise Application 的缩写,简单说就是“可复用企业级应用”。这个项目要解决的事情很直接:把公司内部多个业务系统里重复造轮子的部分抽出来,做成一套统一的可复用骨架,包括权限、组织架构、基础组件、请求封装、异常处理这些公共能力,让新业务系统接入之后,只写业务代码,不碰基础设施。
这篇文章我就完整复盘一下 REA 从设计到落地过程中的关键环节,包括架构取舍、核心模块拆分、实操过程和几个特别典型的坑。如果你也在做企业内部中后台系统,或者正打算把多个前端项目沉淀成统一工程化底座,这篇内容应该能给你一些参考。
1. 整体设计与思路拆解:为什么非要做可复用企业应用
1.1 复用的核心问题:不是代码复用,而是能力复用
很多人一听到“可复用”,第一反应就是把公共代码抽出来放到一个公共包里,谁要用就引谁。这种思路在早期确实有效,但放到企业级环境里很快就会出问题。原因很简单:企业内部的中后台系统,业务场景高度相似,但又有各自的差异化需求,如果只是简单抽代码,维护成本会转移到依赖关系的协调上。A 系统升级了一个公共组件的版本,B 系统还在用旧版,两边行为不一致,出问题都不知道找谁。
REA 项目的核心思路一开始就不在“抽代码”,而在“沉淀能力”。所谓能力,指的是一个完整的功能闭环。拿权限来说,不是提供一个获取用户信息的工具函数,而是提供一套包含登录态检测、token 刷新、路由守卫、按钮级权限指令、用户信息缓存在内的完整链路。业务系统接入 REA 之后,不需要知道权限是怎么存的、token 刷新是怎么触发的,只需要关心自己在页面里怎么调用权限判断的 API。这就是能力复用和代码复用的本质区别。
当时我们做了一个挺重要的决策:所有公共能力必须自带默认实现,但允许业务系统覆盖默认行为。比如消息提示组件,默认是 antd 的 message 样式,但如果业务系统有自己的设计规范,可以传入自己的渲染函数。这种做法既保证了开箱即用,又照顾了不同业务线的个性化诉求。
1.2 为什么选用微前端与模块联邦这套组合
REA 面向的场景是公司内部多系统共存,过去每个系统都是独立部署、独立维护,前端仓库完全隔离,想要统一升级基础能力几乎不可能。讨论方案的时候,摆在面前的路线有三条:把所有系统合并成一个巨型单页应用(SPA),维护一套公共依赖包,或者引入微前端架构。
合并巨型 SPA 的方案第一个被否决了。原因倒不是技术上实现不了,而是组织协作上必然出问题——不同团队维护同一个仓库,发版节奏互相牵制,一个团队的红线改动可能让另一个团队的应用直接挂掉,这在内部工具类系统里尤其致命。统一公共依赖包的方式我们也试过,但版本漂移问题实在太严重,很多老系统停留在旧版本上不愿意升级,又退化回了“各自为政”的局面。
最终选定的方案是基于模块联邦的微前端架构。每个业务系统作为独立的微应用存在,有自己的仓库、自己的构建流程、自己的部署管道。REA 核心作为基座应用,负责整体布局、导航、用户会话等基础能力,同时通过模块联邦暴露一组共享的运行时依赖。业务系统在构建时可以告诉模块联邦“我需要共用 react 和 react-dom”,运行时直接从基座加载,不用重复打包。
这套组合最大的好处是升级路径平滑。基础能力升级时,只需要重新部署基座应用,各业务系统不需要改代码,只要测试核心流程就行。模块联邦的 shared 机制从运行时层面保证了一些关键依赖的版本一致性,从根上消灭了“你用的 React 版本和我用的不一样”这类奇葩 bug。
1.3 数据模型设计:权限、组织与用户信息怎么建模
企业级应用的复用,真正复杂的地方不在前端交互,而在数据模型。REA 项目里我们把核心数据模型分成三个域:用户域、组织域和权限域。
用户域比较简单,维护的是用户基础信息,包括用户ID、姓名、头像、联系方式、账号状态这些字段。组织域要稍微复杂一些,因为企业内部的组织架构通常是树形结构,而且存在一人多部门、部门多层级、跨部门兼职这类情况。我们设计组织模型的时候,特意区分了“主部门”和“兼任部门”两个概念,主部门用于默认归属,兼任部门用于多维度权限计算。
权限域是这三个域里最考验设计的。传统做法是给用户直接绑定角色,角色绑定权限点,这个模型在用户量小的时候很好用,但一旦组织复杂起来,权限的维护成本会指数级上升。REA 里采用的方案是 RBAC + 数据范围的双层模型。第一层是功能权限,也就是你能看到哪些菜单、能点哪些按钮;第二层是数据范围,也就是你能看到哪些数据行。数据范围用规则表达式描述,比如“仅本人”“本部门”“本部门及下级部门”“全部”,由后端在执行数据查询前统一拼接条件。
这个设计让我们在很多业务系统里做到了权限配置化。业务人员通过管理界面拖拽配置,不需要开发介入,这在以前几乎是不可想象的。
2. 核心模块实现与实操要点
2.1 基座应用:布局、导航与会话管理的实现顺序
基座应用是 REA 的核心载体,它承载了用户登录之后看到的第一屏内容,主要包括侧边导航、顶部栏、页签切换、用户信息下拉框等企业应用中几乎都会有的界面元素。实现顺序上,我们遵循了“先会话,后布局,再集成”的路线。
会话模块是第一个动手的。它要解决的核心问题是:用户打开任意一个子应用页面时,怎么判断他是否已登录,以及登录态失效时怎么处理。实现上我们封装了一个统一的 AuthProvider,挂在基座的最顶层。它负责在应用初始化时从 cookie 或者本地缓存读取会话标识,调用后端的会话校验接口确认有效性,同时监听一个自定义事件用于跨应用登出通知。
有几个细节值得说一下。会话校验接口一定要设计成幂等的,也就是说重复调用不会产生副作用,这样微应用加载时重复触发也不会压垮后端。另外 token 刷新要做到异步串行,多个请求同时遇到 401 时,只发出一个刷新请求,其余请求挂起等待刷新结果,避免刷新风暴。这个机制我们在REA 的请求层里用了一个简单的 promise 缓存实现,代码量不大但效果非常明显。
布局模块相对简单,用的是左右结构的经典企业后台布局。左侧导航支持两级菜单,第一级是业务域,第二级是具体功能页面。这里有一个很容易踩的坑:菜单数据不能只存名称和图标,必须存完整的路由映射信息。如果菜单数据里没有路由字段,前端还得自己做一层路由和菜单的匹配逻辑,后续维护时会变得非常痛苦。
2.2 通用组件库:从设计到发布的完整链路
组件库是 REA 复用能力中最直观的部分。我们总结了公司内部十几个业务系统的高频界面模式,归纳出 20 多个基础组件,包括搜索表单、数据表格、详情描述、弹窗表单、步骤条、批量操作栏等。这些组件不是把第三方组件库简单包一层,而是注入企业自身的交互规范和数据口径。
以数据表格为例,企业内部的数据表格几乎都会用到分页、筛选、排序、列配置、导出、行操作这些功能,如果每个系统各自实现,不仅工作量大,而且交互细节容易不一致。REA 的 ProTable 组件在封装时就定了几条硬规则:数据源统一走封装的请求层,分页参数统一用 current 和 pageSize,列配置支持本地持久化到 localStorage,导出动作统一调后端的导出接口并处理下载状态。
组件的发布流程我们是走 npm 私有仓库的,版本号遵循语义化版本规范,也就是主版本.次版本.补丁版本的结构。这里要特别提醒一点:组件库的 breaking change 一定要谨慎,因为企业内部用户众多,任何破坏性变更都可能引发大面积适配。我们内部约定,涉及 breaking change 必须先发布候选版本,给各业务系统留出至少两周的适配窗口,并且在升级文档里写清楚迁移路径。
另一个心得是组件库的文档必须和代码同步维护。这个道理大家其实都懂,但实际执行起来最难。REA 的做法是在组件源码里直接写 JSDoc 注释,自动化工具从注释中提取 API 表格和使用示例,生成文档站点,最大程度减少文档滞后的问题。
2.3 业务接入的标准流程:一个系统从零到上线需要几步
业务系统接入 REA,我们希望能做到“配置优先,代码补充”。也就是说,大部分基础能力通过配置文件声明即可完成接入,只有特殊业务逻辑才需要编写额外代码。整个接入流程大致分为五步。
第一步是创建应用配置,在配置文件里声明应用名称、路由前缀、菜单结构、所需权限点列表。第二步是在基座应用中注册路由,这样导航到对应地址时才不会 404。第三步是处理登录对接,把业务系统自身的登录请求替换成 REA 统一的登录入口。第四步是按需引入公共组件,替换掉原有的重复实现。第五步是回归测试,重点检查会话一致性、权限边界和关键业务流程。
这里有个小技巧:业务系统接入时,尽量让路由前缀和应用名称保持稳定,后续升级时可以减少很多麻烦。我们遇到过某个系统上线半年后改了路由前缀,结果所有外部分享的链接全部失效,只能一个一个找业务方重新生成,很折腾。
2.4 请求层与异常状态码体系:别把接口请求写进业务代码
请求层是 REA 里可能最不显眼但实际最提升效率的模块。企业内部系统前后端联调时最烦的事情是:每个接口都需要写 loading 状态、错误提示、成功提示、异常处理,这些代码塞在业务页面里,页面一多就全是重复劳动。
REA 的请求层封装了一个统一方法,内部完成的事情包括:拼接基础路径、自动带上会话凭证、处理请求超时、根据响应状态码统一提示错误信息、处理登录失效自动跳转登录页、支持取消重复请求。业务代码只需要关心接口的入参和返回数据,其余全部交给请求层处理。
我们定义了一套内部错误码规范。例如 200 表示成功,400 表示参数错误但前端不需要特殊处理,401 表示未登录或会话过期,403 表示权限不足,500 表示服务端异常。这套规范明确之后,前后端之间很多不必要的扯皮都消失了,因为两边对“什么场景返回什么状态码”有了共同语言。
还有一点很重要:请求层必须支持在微前端场景下工作,也就是说它不能依赖全局的 axios 实例,而是要在基座和子应用之间共享一个配置来源。REA 的做法是把请求层实例挂到基座的全局对象上,子应用通过模块联邦共享机制拿到这个实例,保证所有应用发出的请求都走同一套拦截逻辑。
3. 实操过程与关键环节实现
3.1 工程初始化:从零搭建基座应用的全过程
REA 基座应用的技术栈选了 React + TypeScript + Vite。选择 Vite 而不是 Webpack 的理由很简单:构建速度快,配置直观,对模块联邦也有成熟支持。初始化基座的时候,第一步不是写代码,而是先把项目结构搭清楚。我们的目录结构大致分为 src/app、src/layouts、src/components、src/services、src/utils 几个核心目录,每个目录承担明确职责,避免后续代码越放越乱。
初始化过程中有一个比较关键的配置是路径别名。我们统一用 @/ 指向 src 根目录,用 @/components 指向公共组件目录。路径别名配置好之后,后续写代码会舒服很多,import 路径干净利落,也方便做模块重构时统一替换。
基座应用的启动流程我建议做成可视化的,也就是启动时在控制台输出当前环境的配置信息、远程模块的加载状态、路由注册的数量等关键信息。这个看起来很小的功能,在多人协作时特别有用,因为成员在本地启动时能快速判断环境是否正常,而不是一上来就面对一个空白页面。
3.2 权限指令与菜单过滤:按钮级权限是怎么落到页面上的
权限是 REA 里用户感知最强的一个模块,做得不好就会被业务人员频繁投诉。菜单过滤的逻辑很直接:拿到当前用户的功能权限点列表,遍历菜单配置,把没有权限的菜单项过滤掉。但按钮级权限稍微复杂一点,因为它不是整页隐藏,而是页面上某个按钮根据权限决定显示还是隐藏。
REA 在这个环节封装了一个权限指令和一个权限判断函数。权限指令用在模板里,写法类似 v-permission=“'user:create'”,没有权限时指令会把对应元素从 DOM 中移除。权限判断函数则用在逻辑复杂的场景,比如某个按钮的显示条件除了权限还要叠加其他业务条件,这时直接调用权限判断函数更灵活。
这里要提醒一个容易踩的坑:按钮级权限不能只做前端隐藏,后端接口必须同步做强校验。前端的隐藏只是改善用户体验,后端不做校验的话,用户通过控制台手动调用接口一样能执行越权操作。REA 对接的每一个系统,我们都会在验收时做越权测试,确保前端控制和后端校验一致。
3.3 动态路由注册:微应用加载时路由是如何合并的
微前端架构里,路由是最容易出问题的地方。基座应用有自己的路由,子应用也有自己的路由,如果两边路由冲突,就会出现页面错乱。REA 采用的做法是以基座为主,子应用的路由在加载时注册到基座的路由表里,卸载时移除。这样基座对整体路由有完全的控制权,可以统一处理页面标题、菜单高亮、页面缓存这些公共逻辑。
路由注册的实现细节里,有一个问题需要特别处理:子应用内部跳转时,浏览器的地址栏会变化,但基座导航的高亮状态可能不会同步更新。解决办法是监听路由变化事件,根据当前地址计算所属的菜单项,手动触发高亮更新。这类问题平时不遇到,遇到了排查起来非常费时间,所以写在这里希望帮你提前避开。
3.4 全局状态管理:跨应用的数据共享怎么做才不乱
企业内部系统最常见的跨应用共享数据包括用户信息、会话状态、全局设置,以及一个应用跳转到另一个应用时需要携带的业务参数。REA 在全局状态管理上没有选择使用大型状态管理库,而是用了轻量方案:全局事件总线加共享 store。
共享 store 的核心是一个响应式对象,挂在基座全局,子应用通过模块联邦获取引用。任何应用修改 store 里的数据,读写操作都会同步到所有应用。这种模式简单直接,不引入太重的心智负担。事件总线则用于通知类场景,比如用户点退出登录,基座通过事件总线通知所有子应用清理本地状态。
使用共享 store 有一个务必遵守的规范:不允许子应用直接修改 store 里不属于自己的字段。否则不同应用之间会产生隐性的数据耦合,出了 bug 很难定位。REA 的做法是给每个子应用划分独立的命名空间,应用只能操作自己命名空间内的数据,公共数据只能通过基座提供的 API 来修改。
4. 常见问题与排查技巧实录
4.1 子应用加载后样式丢失,页面像没有皮肤的裸奔
这个问题出现频率极高。原因通常是子应用的样式没有按预期注入到主文档中。排查时可以先把子应用单独跑起来,看样式是否正常,如果单独运行没问题,那就一定是样式注入机制出了问题。常见的原因包括样式隔离配置错误、CSS 文件引用路径用的是相对路径、以及子应用使用了动态加载的 css 但微前端容器没有接管。
REA 的解决方案是让子应用的样式全部走 CSS Modules 或 CSS-in-JS,避免裸样式名的全局污染问题。另外在容器加载子应用时主动检查样式节点的挂载数量,如果显著低于预期就打印警告日志,把问题暴露在开发阶段而不是上线后。
4.2 会话过期后跳转登录页,但跳转后登录成功还是回到原页面
这个问题的表象是:用户会话过期,请求返回 401,前端自动跳到登录页。用户登录成功后,本应回到之前访问的页面,但实际却回到了应用首页。原因是跳转登录页之前,没有把当前地址记录下来,登录成功后又没有把地址拼回去。
解决思路就两步:发起 401 跳转前,把当前完整路由地址存入 sessionStorage;登录成功后读取这个地址并跳转。关键细节是要区分不同的应用,如果基座和子应用跳转登录页的方式不一样,记录地址的时机也要对应调整,否则记录的是基座地址,恢复的却是子应用的页面,就会出现跳转不对位的问题。
4.3 组件库升级后,线上出现两个版本的弹窗样式
某个业务系统没有及时升级组件库,但基座升级到了新版本,两个版本同时在运行时存在,页面上就会混用不同样式。这个问题的根源是组件库没有做到平滑升级,或者业务系统没有遵循升级流程。
REA 的应对措施是双管齐下:组件库发布时,用工具自动扫描 API 变化,生成完整的升级影响面报告,作为强制阅读材料;同时基座为每个子应用声明了组件库的最低版本要求,如果子应用版本过低,启动时直接给出警告弹窗。这个机制不能完全杜绝问题,但至少能把风险提前暴露出来。
4.4 排查技巧:微前端场景下,怎么快速定位问题归属
微前端最大的排查难点是问题可能出现在基座、子应用、公共组件库、后端接口四个位置中的任何一个。REA 的排查套路是“先隔离,再二分”。先看问题是否在子应用独立运行时出现,独立运行没问题就基本确认与基座相关。如果是与基座相关的问题,再看是否公共依赖导致,比如 React 实例是否重复加载。
这里推荐一个基础设施级的做法:基座和子应用都把关键运行信息打印到统一格式的控制台日志里,日志中带上应用标识。这样排查问题时,通过日志就能快速区分是谁输出的,不用在一堆 console 输出里猜来猜去。这个小工具能节省大量排查时间。
5. 经验沉淀与后续扩展方向
5.1 经历 REA 项目后沉淀的几条工程化心得
第一,企业内部的前端工程化,优先解决“重复”问题永远比解决“复杂”问题更见效。很多团队热衷于引入新框架、新工具来解决所谓的“技术债”,但真正让团队疲惫的往往是同一段代码在不同项目里反复写、反复调。REA 项目之所以能很快看到效果,就是因为它精准地打在了“重复”这个痛点上。
第二,可复用能力必须由基础设施团队和业务团队共同定义,不能关起门来自己设计。REA 组件库在做需求收集时,我们几乎把每个业务系统的主界面都截了图,逐块拆解哪些是一样的、哪些是有差异的。这样沉淀出来的组件才是真正被需要的,而不是设计者想象中的“通用”。
第三,复用要有边界,强行统一是灾难的开始。不同业务线的交互习惯有时候真的差很多,强行让所有系统都用同一套表格、同一个弹窗,业务团队会有怨言。REA 的做法是提供基础能力加可配置项,在需要统一的底线部分坚持统一,在允许变化的部分留足空间。
5.2 后续可以继续扩展的方向
REA 目前的形态还只是第一阶段的成果。可以继续扩展的方向,一个是低代码配置平台,让业务人员通过拖拽配置界面来生成简单的管理页面。这不是异想天开,因为有了 REA 的组件库和权限体系作为底座,配置平台需要解决的技术难题已经降到了可接受的范围内。另一个方向是数据可视化能力,把企业内部的常用图表沉淀成 BI 组件,直接接入 REA 组件库,业务系统做报表时不用再单独引入图表库。
我个人在实际操作中最深刻的体会是,可复用企业应用不是一锤子买卖,而是一个持续演进的过程。它做了发布之后不代表结束,后续的维护和迭代才是真正考验耐心的地方。如果你正在着手类似的方向,我的建议是先挑一个最疼的业务场景做试点,用实际成效说话,比写再多文档都管用。等到试点跑顺了,再慢慢扩大复用范围,这条路会比一次性追求大而全会稳很多。