Home
Perfecto的头像

Perfecto

微前端架构技术选型,到底怎么选(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——领域驱动设计,也就是微服务化,在前端这里也是一样的,把大单体按照业务、功能模块拆分成一个个可以独立运行但是又可以相互调用的微应用,这就是微前端。

我们当时把市面上能找到的方案都拉出来“遛”了一遍,从 iframeqiankun,再到 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 在性能和用户体验上的问题也是无法接受,放弃
需要用一个新的基础方案将整个项目的代码重构,工作量上无法接受,放弃

总结

  1. 为什么放弃“明星方案” Qiankun 2?

一开始,我们是奔着 qiankun 去的,毕竟是阿里出品,社区最成熟。但深入调研后,我们退缩了。

核心卡点在 JS 沙箱。qiankunProxy 沙箱确实解决了大部分全局变量污染问题,但它不是没有代价的。首先是性能,所有 window 操作都加了一层 Proxy,在我们的 MSSP 这种重交互平台上,频繁的操作会不会导致卡顿?我们心里没底。

更让我们不安的是“沙箱逃逸”。我们查阅了社区(比如 qiankun/issues/1471),发现 Proxy 沙箱并非绝对安全,在某些场景下(如 Node.js 微应用)依然存在逃逸可能。我们的 MSSP 平台对安全要求极高,这种“理论上”的不确定性,对我们安服业务来说,就像个定时炸弹。我们宁愿问题暴露在明面上,也不想它藏在沙箱底层。

  1. 为什么没选“低成本” micro-app?

micro-app 的思路我们非常喜欢,类 Web Component 的封装,接入成本极低。它在沙箱性能和逃逸问题上,比 qiankun 处理得更巧妙。

但我们做选型的时候是 2024 年初,micro-app 还处于 0.x 版本,尚未发布 1.0 正式版。我们翻了它的 CHANGELOGissues,发现维护频率和社区活跃度都还不算太高。

说白了,我们不敢赌。MSSP 是我们的核心业务平台,我们承担不起一个“新兴”框架在生产环境出问题的风险。如果它是 1.0 稳定版,我们可能会做完全不同的选择。

  1. 为什么是 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() 一个本地异步组件一样,去拉取这个远程模块。

这种模式完美契合了我们的核心诉求:

  1. 性能原生:没有沙箱带来的额外性能损耗。
  2. 独立升级:微应用可以独立开发、独立部署,主应用只需要在运行时拉取最新的模块即可。

但它的短板也和优点一样突出:它默认什么隔离都不做。

  • JS 不隔离:大家共享一个 window
  • CSS 不隔离:class 名冲突了,样式就直接污染。

这也是组长拍板决定的方案,最后的结论是:这些短板,对于我们团队来说,是明确且可控的。

相比于 qiankun 沙箱“可能”的性能问题和“未知”的逃逸风险,MF 的“JS 污染”和“CSS 污染”是 100% 会发生的问题,但也正因如此,我们可以 100% 用工程化手段去规避它。

决定用 MF 后,我们花了 2 个月时间对 MSSP 平台进行改造。这里没有银弹,全是硬骨头,我分享几个我们踩得最深的坑。

坑一:共享依赖

MF 允许你定义 shared 依赖(比如 vue, element-ui),避免每个微应用都打包一份,导致冗余。

理想很丰满,但现实是:

  1. 版本冲突:我们主应用和微应用都强依赖 vue@2.6.12 和我们那个二改的 element-ui 库。A 应用想偷偷升个 vuex 版本,或者 B 应用用了个不一样的 vue-router,主应用 shared 了单例,当场报错。
  2. “幽灵依赖”:A 应用忘了 share 某个依赖(比如 axios),但因为主应用 share 了,本地跑得好好的;一上生产,主应用没加载 A(或者 A 先于主应用加载),直接白屏。

我们的解决方案:

  • 强制 singleton: true:所有共享依赖,特别是 vuevue-routervuexelement-ui 必须是单例。我们通过 package.jsonresolutions 字段,强制项目组所有这些基础库版本拉齐到一模一样。
  • 建立“依赖画像”:我们写了个脚本,扫描所有微应用的 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 报错。严禁、杜绝、禁止任何人写 bodyhtml* 通配符选择器。

坑三:应用的生命周期和通信

MF 只管模块加载,但它不管应用的“生老病死”(加载、挂载、卸载),也不管通信。

  • 生命周期:我们没用 single-spa(太重),而是自己封装了一层。主应用 import 微应用的 bootstrap.js,这个 bootstrap 会导出一个约定好的 mountunmount 方法(里面就是 new Vue(...)app.$destroy()),主应用在切换路由时,手动调用。
  • 通信:我们试过 propsEventBus,最后发现最简单的最好用。我们利用“主应用反向注入”。主应用在 mount 微应用时,会把一个 globalContext 对象(包含用户信息、路由跳转方法、全局 message 组件、还有一个用 new Vue() 创建的全局事件总线 Bus)通过 props 传进去。微应用只管调用,实现体验统一。

迁移效果

  1. CI/CD 时间:从平均 30-40 分钟(全量打包部署),降低到单个微应用(如“报告中鑫”)独立部署平均 5-8 分钟。
  2. 打包体积:主应用(基座)的初始加载体积,从近 200MB(解压后)降到了 99MB(主应用 + 公共依赖)。
  3. 开发效率:这是最爽的。各业务线的并行开发体验终于丝滑顺畅了,A 组上“报表”,B 组上“漏扫”,互不干扰,独立提测。
  4. 性能:由于没有了 JS 沙箱的性能损耗,应用跑起来和原生 SPA 几乎没区别。

结尾:我的思考与建议

最后,我想总结一下我们团队的选型思考。

回过头看,Module Federation 是最优解吗?不一定。 但它是在我们当时面临“巨石应用”、“CI/CD 奇慢”、“急需独立部署”等核心痛点下,最“合适”的解。

我们选择它,是因为:

  1. 我们团队技术栈高度统一(全是 Vue 2.6 + Webpack)。
  2. 我们愿意承担从 Webpack 4 升级到 Vue CLI 5 的巨大迁移成本。
  3. 我们对性能和原生体验的追求,高于对“强沙箱隔离”的追求。
  4. 我们愿意,也“有能力”投入工程化成本,去解决它暴露在外的(依赖和样式)问题。

给你的选型建议:

  • 如果你追求“开箱即用”,团队技术栈五花八门,或者对安全隔离要求极高(比如要接很多第三方应用),别犹豫,qiankunmicro-app 更省心。
  • 如果你的团队技术栈统一(特别是 Vue 或 React),对性能有洁癖,且愿意(像我们一样)投入成本去做升级迁移和工程化约束,那 Module Federation 会给你带来最原生的体验和最大的灵活性。

这只是我们在 MSSP 平台的一点浅薄实践,技术在发展,也许今天我们又会有新的答案。欢迎大家在评论区一起交流,使劲拍砖。

作者注:这是还原记录一下 2024 年初的时候经历的一次技术栈升级过程中的选型和思考,有一些数据版本已经过时。但是现在看,当时选型做的很对,MF 放在即将 2026 年的今天依旧很能打,甚至推出了 1.5、2.0 版本的 MF。

软件架构