微前端架构技术选型,到底怎么选(2024版)
大概在 2023 年底,我们团队快被我们开发的 MSSP 平台逼疯了。作为一个 ToB 的大型平台,基于 Vue 2.6.12 和我们自己基于 Element UI 二次封装的业务组件库 搭建,功能堆得越来越多,最后变成了一个打包体积超过 200MB 的“巨无霸”单体应用。
意味着我们本地 dev 冷启动要等 3-5 分钟;意味着 CI/CD 流水线跑一次长达 30-40 分钟。说实话,那段时间的开发体验幸福感极低,下午 5 点提测,大概率要等到晚饭后才能部署到测试环境。更要命的是,十几个模块耦合在一起,改 A 模块的一个小 bug,你都不知道会不会影响到 B 模块,每次上线都心惊胆战,测试团队也苦不堪言。
2024 年过年前吧,隐约听说有一个业务需求需要复用另一个叫 SDSP 的平台的一些功能和组件,如果只是复用某个完整的页面可以直接使用 iframe 做,隔离性好,简单快速。但是只是局部复用呢?复用某个带有业务特色的组件?复用某个抽屉?这时候我们把目光瞄向了微前端。
微前端是当时很火的一个技术,后端开发中有个概念叫 DDD——领域驱动设计,也就是微服务化,在前端这里也是一样的,把大单体按照业务、功能模块拆分成一个个可以独立运行但是又可以相互调用的微应用,这就是微前端。
我们当时把市面上能找到的方案都拉出来“遛”了一遍,从 iframe 到 qiankun,再到 Module Federation。我先放一个当时的对比草稿(红色和绿色表示负面影响较大/决定性因素):
| 对比项/方案 | qiankun2 | module-federation | micro-app | MPA+路由分发 | MPA+iframe | 纯web component |
| js隔离 | 1.沙箱存在性能问题2.涉及沙箱逃逸问题比较难解决 | 无沙箱,通过规范或工具限制 | 1.proxy沙箱性能言方有优化2.涉及沙箱逃逸问题比较难解决 | 跳转页面,不存在隔离 | 天然且完美的js和样式隔离机制 | 原生的隔离机制问题 |
| 性能 | 1.沙箱仍是with+ Function,由此产生的性能问题依旧没有解决2.proxy代理导致对比VNode时,proxy代理返回的VNode和原本的VNode是不等的,实际上VNode无变化 [https://github.com/napp/discussions/259](https://github.com/napp/discussions/259)3.内存问题依旧存在 | 接近于原生的性能 | 1.沙箱也是with+ Function,但是会缓存部分全局变量提高with查找变量时的效率 | 1.基础库无法复用2.切换App无法复用资源3.每次切换都需要刷新页面 | 1.iframe内部重新创建新的document,初始化时白屏时间比较久2.构建包无法共享基础库,包比较大3.切换App无法复用资源页面 | 原生性能 |
| 技术栈升级 | 允许技术栈渐进升级 | 需尽量保持一致 | 允许技术栈渐进升级 | 允许技术栈渐进升级 | ||
| 改造成本 | 1.将原本的应用进行业务拆分 | 1.升级脚手架2.将原本的应用进行业务拆分 | 1.将原本的应用进行业务拆分 | 1.将原本的应用进行业务拆分 | 1.将原本的应用进行业务拆分 | 1.成本很高,需要把整个项目的代码重新编写2.将原本的应用进行业务拆分 |
| 构建包 | 增量升级 | 增量升级 | 增量升级 | 各个应用各自独立全量升级 | 各个应用各自独立全量升级 | 全量升级 |
| 样式隔离 | 通过scopedCss隔离样式,隔离不彻底还需要人为补充 | 无 | 通过scopedCss隔离样式,对于scopedCss的配置粒度更细,隔离不彻底还需要人为补充 | 无 | 天然且完美的js和样式隔离机制 | 完美支持 |
| 用户体验 | 1.dom操作时较为卡顿2.开发过程子应用问题调试麻烦 | 原生体验 | 每个系统间需要进行一个重新加载,界面体验感割裂严重 | 1.需要对遮置和居中做处理2.需要处理url同步3.需要解决初始化白屏体验问题 | 原生体验 | |
| 第三方应用 | 接入vue、react等主流框架应用的成本很低,但是有第三方库不兼容的风险 | 需要第三方应用也遵循同样的基础库和构建方式才支持接入 | 支持 | 支持 | 支持程度最佳 | 不支持接入 |
| 生态完整性 | 维护停滞,社区暴露问题较多 | webpack5插件,webpack团队背书 | 目前仍是 0.8.x,未有1.0稳走版本 | google推动的标准 | ||
| 通信机制 | 通过第三方的事件通知机制 | 通信数据格式比较受限 | 原生的 J5 事件通知机制 | |||
| 总结 | 存在明显的性能问题,且沙箱逃逸问题难解决,放弃 | 在接入第三方应用上存在的问题比较大,但是对于我们想要达到【独立升级】的效果来说,此短板可以接受 | 在处理沙箱的性能问题和逃逸问题上虽然比方案1更优秀,但这两个问题出现还是难解决,且目前官方维护的频率及未有稳定版本,放弃 | 相当将原本的一个系统拆成多个系统,体验上无法接受,放弃 | iframe 在性能和用户体验上的问题也是无法接受,放弃 | 需要用一个新的基础方案将整个项目的代码重构,工作量上无法接受,放弃 |
总结
- 为什么放弃“明星方案” Qiankun 2?
一开始,我们是奔着 qiankun 去的,毕竟是阿里出品,社区最成熟。但深入调研后,我们退缩了。
核心卡点在 JS 沙箱。qiankun 的 Proxy 沙箱确实解决了大部分全局变量污染问题,但它不是没有代价的。首先是性能,所有 window 操作都加了一层 Proxy,在我们的 MSSP 这种重交互平台上,频繁的操作会不会导致卡顿?我们心里没底。
更让我们不安的是“沙箱逃逸”。我们查阅了社区(比如 qiankun/issues/1471),发现 Proxy 沙箱并非绝对安全,在某些场景下(如 Node.js 微应用)依然存在逃逸可能。我们的 MSSP 平台对安全要求极高,这种“理论上”的不确定性,对我们安服业务来说,就像个定时炸弹。我们宁愿问题暴露在明面上,也不想它藏在沙箱底层。
- 为什么没选“低成本” micro-app?
micro-app 的思路我们非常喜欢,类 Web Component 的封装,接入成本极低。它在沙箱性能和逃逸问题上,比 qiankun 处理得更巧妙。
但我们做选型的时候是 2024 年初,micro-app 还处于 0.x 版本,尚未发布 1.0 正式版。我们翻了它的 CHANGELOG 和 issues,发现维护频率和社区活跃度都还不算太高。
说白了,我们不敢赌。MSSP 是我们的核心业务平台,我们承担不起一个“新兴”框架在生产环境出问题的风险。如果它是 1.0 稳定版,我们可能会做完全不同的选择。
- 为什么是 Module Federation (MF)?
最后,我们把目光锁定在了 Webpack 5 的原生特性:Module Federation。
但这里有个天坑:MF 是 Webpack 5 的功能,而我们庞大的 MSSP 平台,跑的还是 Webpack 4(藏在旧版的 vue-cli 里)。
这意味着,我们根本没得选。想用 MF,就必须先把这个巨无霸工程从 Webpack 4 升级到 Webpack 5。我们评估了一下,决定切换到 Vue CLI 5(它内置了 Webpack 5),这本身就是一次伤筋动骨的迁移。
既然横竖都要“大动干戈”,为什么不一步到位?
MF 最打动我们的是什么?是“原生”。
它不搞沙箱,也不搞 HTML Entry,它就是在编译时告诉你:“我(微应用)这里有几个模块(如 ./Button 或 ./App)是可以导出的”;同时在运行时,主应用可以像 import() 一个本地异步组件一样,去拉取这个远程模块。
这种模式完美契合了我们的核心诉求:
- 性能原生:没有沙箱带来的额外性能损耗。
- 独立升级:微应用可以独立开发、独立部署,主应用只需要在运行时拉取最新的模块即可。
但它的短板也和优点一样突出:它默认什么隔离都不做。
- JS 不隔离:大家共享一个
window。 - CSS 不隔离:
class名冲突了,样式就直接污染。
这也是组长拍板决定的方案,最后的结论是:这些短板,对于我们团队来说,是明确且可控的。
相比于 qiankun 沙箱“可能”的性能问题和“未知”的逃逸风险,MF 的“JS 污染”和“CSS 污染”是 100% 会发生的问题,但也正因如此,我们可以 100% 用工程化手段去规避它。
决定用 MF 后,我们花了 2 个月时间对 MSSP 平台进行改造。这里没有银弹,全是硬骨头,我分享几个我们踩得最深的坑。
坑一:共享依赖
MF 允许你定义 shared 依赖(比如 vue, element-ui),避免每个微应用都打包一份,导致冗余。
理想很丰满,但现实是:
- 版本冲突:我们主应用和微应用都强依赖
vue@2.6.12和我们那个二改的element-ui库。A 应用想偷偷升个vuex版本,或者 B 应用用了个不一样的vue-router,主应用shared了单例,当场报错。 - “幽灵依赖”:A 应用忘了
share某个依赖(比如axios),但因为主应用share了,本地跑得好好的;一上生产,主应用没加载 A(或者 A 先于主应用加载),直接白屏。
我们的解决方案:
- 强制
singleton: true:所有共享依赖,特别是vue、vue-router、vuex、element-ui必须是单例。我们通过package.json的resolutions字段,强制项目组所有这些基础库版本拉齐到一模一样。 - 建立“依赖画像”:我们写了个脚本,扫描所有微应用的
package.json,生成一个全局的共享依赖清单(shared-manifest.json),由主应用统一share,微应用只管用,不允许私自share。
坑二:CSS 隔离
前面说了 MF 不带 CSS 隔离。刚开始,我们天真地以为:“没事,我们是 Vue,人手一个 <style scoped>,怕啥?”
现实是,scoped 只能管住你组件内部的样式,它管不住“全局样式”!
A 应用的同学为了方便,reset.css 里写了个 body { font-size: 16px; },B 应用(设计稿是 14px)的同学直接“炸”了,跑过来质问:“谁动了我的 body!”
我们的解决方案:
- 我们没有用
Shadow DOM(太重了,怕有别的坑)。 - 强制
<style scoped>:所有业务组件,必须使用scoped。 - 全局样式“特批”:对于必须用的全局样式(比如
element-ui的主题覆盖),我们约定必须以mssp-global-开头,并用 PostCSS 插件在编译时加lint,不符合规范的直接 CI 报错。严禁、杜绝、禁止任何人写body、html或*通配符选择器。
坑三:应用的生命周期和通信
MF 只管模块加载,但它不管应用的“生老病死”(加载、挂载、卸载),也不管通信。
- 生命周期:我们没用
single-spa(太重),而是自己封装了一层。主应用import微应用的bootstrap.js,这个bootstrap会导出一个约定好的mount和unmount方法(里面就是new Vue(...)和app.$destroy()),主应用在切换路由时,手动调用。 - 通信:我们试过
props、EventBus,最后发现最简单的最好用。我们利用“主应用反向注入”。主应用在mount微应用时,会把一个globalContext对象(包含用户信息、路由跳转方法、全局message组件、还有一个用new Vue()创建的全局事件总线Bus)通过props传进去。微应用只管调用,实现体验统一。
迁移效果
- CI/CD 时间:从平均 30-40 分钟(全量打包部署),降低到单个微应用(如“报告中鑫”)独立部署平均 5-8 分钟。
- 打包体积:主应用(基座)的初始加载体积,从近 200MB(解压后)降到了 99MB(主应用 + 公共依赖)。
- 开发效率:这是最爽的。各业务线的并行开发体验终于丝滑顺畅了,A 组上“报表”,B 组上“漏扫”,互不干扰,独立提测。
- 性能:由于没有了 JS 沙箱的性能损耗,应用跑起来和原生 SPA 几乎没区别。
结尾:我的思考与建议
最后,我想总结一下我们团队的选型思考。
回过头看,Module Federation 是最优解吗?不一定。 但它是在我们当时面临“巨石应用”、“CI/CD 奇慢”、“急需独立部署”等核心痛点下,最“合适”的解。
我们选择它,是因为:
- 我们团队技术栈高度统一(全是 Vue 2.6 + Webpack)。
- 我们愿意承担从 Webpack 4 升级到 Vue CLI 5 的巨大迁移成本。
- 我们对性能和原生体验的追求,高于对“强沙箱隔离”的追求。
- 我们愿意,也“有能力”投入工程化成本,去解决它暴露在外的(依赖和样式)问题。
给你的选型建议:
- 如果你追求“开箱即用”,团队技术栈五花八门,或者对安全隔离要求极高(比如要接很多第三方应用),别犹豫,
qiankun或micro-app更省心。 - 如果你的团队技术栈统一(特别是 Vue 或 React),对性能有洁癖,且愿意(像我们一样)投入成本去做升级迁移和工程化约束,那
Module Federation会给你带来最原生的体验和最大的灵活性。
这只是我们在 MSSP 平台的一点浅薄实践,技术在发展,也许今天我们又会有新的答案。欢迎大家在评论区一起交流,使劲拍砖。
作者注:这是还原记录一下 2024 年初的时候经历的一次技术栈升级过程中的选型和思考,有一些数据版本已经过时。但是现在看,当时选型做的很对,MF 放在即将 2026 年的今天依旧很能打,甚至推出了 1.5、2.0 版本的 MF。
