微前端的悖论:拆得越独立,耦合越深

2021年前后,微前端是前端圈最火的架构概念。qiankun、single-spa、Module Federation,各种方案轮番上阵,每过几个月就有一篇"微前端最佳实践"刷屏。团队的架构升级PPT里如果没挂一个微前端的示意图,都不好意思拿出来汇报。

三年过去了,真正在线上跑过微前端的人,大多有一种心照不宣的沉默:方案落地了,页面跑起来了,但总觉得哪里不对劲。

不对劲的地方,通常不在PPT里。

 

三个承诺和一个现实

 

微前端刚出来的时候,给了三个承诺:技术栈无关、独立部署、团队自治。每一个听起来都很美,每一个在实践中都打了折。

技术栈无关意味着子应用可以自由选择框架。React的团队用React,Vue的团队用Vue,听起来皆大欢喜。但"无关"只存在于第一个版本。三个月后,你会发现共享组件怎么办——A子应用用React写了一套表格组件,B子应用用Vue想要相同的交互,是再写一遍还是搞iframe嵌套?六个月后,你的招聘成本翻倍了,因为新人得同时熟悉两套技术栈的代码风格。一年后,某个子应用的技术栈要升级,但它和其他子应用之间的样式隔离出了问题——因为当初"无关"的时候,没人定义样式边界的规范。

技术栈无关真正的代价不是今天允许你自由选择,是明天让你无法统一收口。

独立部署是第二个承诺。每个子应用有自己的CI/CD流水线,可以独立发版,不用等其他团队。但在实际操作中,独立部署的前提是接口契约稳定。子应用A改了共享状态的结构,子应用B还没发布新版本,线上就出现了状态不一致。你开始引入版本协商机制——然后发现这个协商机制本身的更新需要所有子应用同时升级。独立部署的尽头是协商部署,协商部署的尽头是"大家商量一下这周一起发个版"。

团队自治听起来最性感。每个子应用团队像一个微型创业公司,拥有自己的代码仓库、发布节奏、技术选型。但自治的前提是边界清晰,而大部分业务系统的边界从一开始就是模糊的。用户从产品A的页面跳到产品B的页面,中间的过渡态算谁的?跨子应用的用户操作链路——比如从理财列表到持仓详情再做一笔交易——这个流程中任何一个环节的改动都需要协调多个"自治"团队。

团队自治不是给个独立仓库就能实现的,它需要业务边界的天然切分。而大部分系统的业务边界,恰恰是在运行时才变得清晰的。

 

耦合的真相:不是减少了,是转移了

 

这是微前端最大的悖论。你把一个大应用拆成五个小应用,以为解耦了,实际上是把编译时的耦合变成了运行时的耦合。

编译时的耦合是好处理的——同一个仓库里,改了一个类型,TypeScript直接报错,CI直接挡住。但运行时的耦合是隐式的:子应用A往全局状态里写了一个标志位,子应用B依赖这个标志位做渲染判断。这个依赖关系不在任何import语句里,不在任何类型定义里,它存在于一个只有运行时才会触发的隐式契约中。

这种隐式契约比显式依赖可怕得多。显式依赖你看得见,可以管理,可以升级,可以删除。隐式依赖你看不见,直到某个版本发布后线上出bug才发现——而这个bug只会在特定子应用组合、特定加载顺序、特定用户路径下触发。

我见过一个场景:两个子应用都监听了同一个全局事件,但在某个版本迭代中,一个子应用改变了事件的派发时机(从mount后改成了mount前),另一个子应用还假设事件在mount后触发。这个bug在联调环境完全无法复现,因为联调环境只有两个子应用同时加载。只有在生产环境,其他子应用的加载时序影响了事件派发顺序,问题才暴露出来。

调试这种问题,比调试一个单体应用里的循环依赖痛苦十倍。因为至少循环依赖会直接报错,而运行时耦合只在它想出现的时候出现。

 

沙箱的妥协

 

qiankun的JS沙箱是最被高估的特性之一。它承诺了子应用之间的JavaScript隔离,让你觉得可以放心大胆地在每个子应用里装不同的依赖版本。

但沙箱是有漏洞的。最常见的问题是:子应用通过fetch/ajax发出的请求携带的cookie和header是宿主应用的,不是被沙箱隔离的。子应用往document上挂载的事件监听器,在子应用卸载后未必能正确清理。子应用创建的Web Worker、SharedWorker,沙箱根本管不到。

更根本的问题是:CSS隔离比JS隔离难得多。qiankun默认的CSS隔离方案是shadow DOM或者动态添加scope前缀,但shadow DOM会破坏全局样式继承(你的antd组件库会出问题),scope前缀会破坏子应用中的动态样式注入。大部分团队最后都选择了"约定命名规范 + 手动review"这种最原始的方式,相当于回到了没有CSS Modules的时代。

沙箱的问题不在于它做得不够好,而在于它试图用运行时隔离来解决一个应该在设计时解决的问题。如果你的子应用之间真的需要严格隔离,那说明你的拆分边界可能划错了。

 

通讯的膨胀

 

微前端的通讯方案是一个不断膨胀的故事。一开始用CustomEvent——简单,解耦,够用。后来发现事件多了以后根本不知道是谁在监听、谁在派发、事件流是什么样的,调试起来是噩梦。于是引入了全局状态管理——但全局状态是另一个名字的耦合。再后来搞了shared module——把公共依赖抽出来作为共享层,子应用通过这个共享层交换数据。

到这里,你其实已经重新发明了一个单体应用的依赖系统,只不过它比原来的单体应用更复杂,因为这个依赖系统是分布式的、异步的、没有编译时检查的。

我在金融业务里见过一种更隐蔽的通讯膨胀:交易链路。用户从产品列表(子应用A)→ 产品详情(子应用B)→ 交易确认(子应用C)→ 持仓页(子应用D),整个过程涉及四个子应用。每一步都需要传递产品信息、用户风险评测结果、账户状态。这些数据——基金代码、风险等级、是否首购、是否需要重新评测——在单体应用里就是一个Context或者几个props,但在微前端架构里,你需要设计一个跨子应用的数据传递协议。

这个协议本身就成了一个共享层。而这个共享层的任何改动,都需要协调所有相关子应用。你不是在解耦,你是在用一个更脆弱的耦合替代了一个更稳固的耦合。

 

为什么要拆?回到问题的原点

 

说到这里,可能有人会觉得我是在全盘否定微前端。不是。

我想回到一个更根本的问题:当初为什么拆?

在我观察到的微前端落地案例里,拆分的理由大概有三类。

第一类是因为团队组织结构。多个团队共同维护一个前端应用,合并代码的冲突和发布节奏的不一致让团队很痛苦。微前端把代码冲突变成了运行时隔离,把发布冲突变成了独立部署。这个理由是成立的,但它解决的是组织问题,不是技术问题。用技术手段解决组织问题,治标不治本——除非你同时调整团队边界和职责划分,否则微前端只是把代码仓库的冲突转移到了通讯协议的冲突。

第二类是因为历史包袱。老系统技术栈过旧,新系统想用新框架,但老系统又不能一次性重构。微前端让老系统和新系统可以在同一个页面中共存。这个理由也成立,但请注意——它是过渡方案,不是目标架构。很多团队在过渡完成了之后没有把微前端拆掉,反而把它当成了标准架构。一个过渡方案变成标准架构,意味着你在为过渡期间的妥协长期买单。

第三类是因为单页应用太大,构建和部署太慢。这个理由在Webpack时代是成立的,但在Vite和esbuild时代已经不那么成立了。如果拆分的主要动机是构建速度,你更应该考虑的是Monorepo + 增量构建,而不是微前端。毕竟Monorepo可以在保留单体应用优势的同时获得构建性能提升,而微前端是以放弃单体应用的诸多优势为代价的。

真正需要微前端的场景其实很窄:多个团队、多条业务线、不同技术栈、需要在一个入口下共存,而且业务边界相对稳定。如果你的场景不满足这些条件,微前端带来的复杂度很可能超过它解决的问题。

 

全栈视角:微前端制造的新BFF难题

 

还有一个问题很少被讨论:微前端对BFF层的影响。

单体应用对应一个BFF服务。所有页面的数据需求集中在一个服务里,虽然有聚合逻辑,但至少在一个代码仓库里可以全局搜索。

微前端架构下,每个子应用可能需要自己的BFF接口。这意味着BFF层要么跟着拆分(增加服务数量和维护成本),要么保持单体(但面临和前端类似的耦合问题——多个子应用的接口需求耦合在一个BFF里)。

更麻烦的是跨子应用的数据一致性。子应用A的BFF返回了用户的风险等级是"保守型",子应用B的BFF返回了用户的可用产品列表。如果两个BFF对风险等级的定义不一致——比如一个用枚举值,一个用字符串——用户就会看到不符合风险偏好的产品推荐。在金融业务里,这是合规问题,不只是体验问题。

微前端解决了前端的拆分问题,但把耦合往下推了一层。BFF层的拆分和协调,是微前端方案里最容易被低估的成本之一。

 

拆容易,合回来难

 

所有拆分方案都有一个共同的特性:拆的时候很爽——每个团队拿到自己的仓库,开始写自己的代码,部署自己的服务,有一种从大泥球中解放出来的快感。但合回来的时候很痛苦——跨团队的需求、共享的状态、统一的体验,这些问题的解决成本远高于拆分时省下的成本。

而且拆分有一个不可逆效应:一旦团队按照微前端的边界组织起来了,想合回去就不仅仅是技术重构,更是组织重组。你需要合并仓库、统一技术栈、对齐发布节奏、重新划分团队职责。这比当初拆开的时候难一个数量级。

所以我的建议很直白:在拆之前,认真想清楚你到底在解决什么问题。如果问题是代码冲突,先试Monorepo。如果问题是构建速度,先试增量构建。如果问题是团队协作,先调整团队职责边界。微前端应该是最后一个选项,不是第一个。

大部分系统还没有大到必须微前端的程度。而那些真正大到的系统——比如金融交易后台、大型管理平台——它们的业务边界通常是自然清晰的,拆分反而不会遇到那么多耦合问题。恰恰是那些业务边界模糊、团队职责交叉的系统,最容易掉进微前端的悖论里:拆得越独立,耦合越深。

微前端不是银弹,也不是毒药。它是一种有明确适用范围的架构风格,被过度宣传和盲目采用推到了它不该在的位置上。三年后的今天,是时候诚实地面对这个现实了。

You voted 3. Total votes: 13

添加新评论