微前端之背景与实践
一个 React 后台管理系统堆了两百多个页面,每次 CI 跑 45 分钟。产品经理想把隔壁团队用 Vue 写的客服系统嵌进来——“用户不要两个入口,要一个。”
这就是微前端要解决的问题。不是”多个团队怎么协作”这种管理学命题,而是你真实会遇到的:构建太慢、技术栈不统一、不同团队互相阻塞发布。
单体前端的尽头
微前端的思路其实不新。2016 年 Micro-Frontends 网站上线时,这个概念已经在前端社区酝酿了一段时间。它的命名和核心思想跟微服务一脉相承:把一个单体应用拆成多个可以独立运行、独立开发、独立部署的单元,然后组合成一个用户感知不到拼缝的整体。
康威定律说的就是这个——你设计出来的系统架构,最终会复刻你团队的组织结构。如果你的团队被拆成了三个业务小组,但代码还堆在一个仓库里,那每次合并冲突都是组织结构在代码层面的投射。
换句话说,微前端不只是在拆代码,是在让代码结构适配你的团队结构。
现代 Web 应用的两难
如果你做过中大型前端项目,大概率经历过这个选择题:
方案 A:一个大仓库,SPA 模式。 所有子应用在一个项目里,开发和调试方便,但构建越来越慢,代码耦合越来越深。改一个公共组件,你不知道会影响多少个页面。
方案 B:拆成多个仓库,MPA 模式。 每个子应用独立部署,但用户在不同子应用之间跳转时,要重新加载整页资源。白屏、状态丢失、用户体验割裂。
这两个方案分别牺牲了开发体验和用户体验。微前端的野心是:既要独立部署的开发体验,又要 SPA 般无缝的用户体验。
微前端到底解决了什么
拆开来看,微前端想达成的是这几个目标:
技术栈无关。 这点太重要了。不是每个团队都会用 React,也不是每个老系统都值得用新框架重写。如果你的后台管理系统里有一个七年前用 Angular.js 1 写的报表模块,微前端让你可以在不碰它源码的情况下,把它嵌进新的 React 主应用里并排运行。
独立部署。 每个子应用有自己的 CI/CD 流水线,上线不用等别的团队。你改你的用户模块,隔壁改他们的订单模块,互不阻塞。
增量升级。 不用一次性重写整个系统。你可以一个模块一个模块地从 jQuery 迁移到 Vue 3,用户根本察觉不到。
但我得说一句实话:这些好处不是免费的。微前端引入了新的复杂度——子应用之间的通信、公共依赖的提取、样式的隔离、路由的分发——每一项都需要额外的工作量。如果你的团队只有三五个人,一个应用还没大到难以维护的程度,引入微前端可能是在给自己找麻烦。
几条技术路线
市面上实现微前端的路子有好几种,它们之间的差异不在”能不能做”,而在”做了之后谁疼”。
iframe:最简单的方案,最深的坑
iframe 天然提供了隔离的运行环境——独立的 window、独立的 CSS、独立的 JS 执行上下文。听起来很完美。
问题在于:iframe 和主页面之间的通信很笨拙(postMessage),URL 状态不同步(子页面里的路由变化不会反映到浏览器地址栏),而且每次加载子页面都要重建整个运行时环境,性能开销不小。
如果你的场景是嵌一个完全独立的、不需要频繁跟主应用交互的页面,iframe 是可以接受的。但如果你想做的是 SPA 般流畅的多模块切换,iframe 会让你的用户体验往后退五年。
Web Components:标准化的方向,但工具链还不成熟
Web Components 提供了浏览器原生支持的组件封装能力,Shadow DOM 天然隔离样式。用纯 Web Components 构建微前端在理论上是干净的——没有任何第三方框架依赖。
但现实中,Web Components 在复杂业务场景下的开发体验还比不上 React 或 Vue。表单处理、状态管理、路由——这些在主流框架里有成熟的生态,到了 Web Components 这边你得自己搭或者找还不够成熟的库。
stencil 这类编译时工具试图弥合这个差距,但至今没成为主流选择。
Module Federation:构建时的魔法
Webpack 5 的 Module Federation 走的是另一条路:在构建时就把不同应用的模块”联邦”到一起,运行时共享依赖。
EMP 是基于 Module Federation 的方案。这条路的好处是共享依赖不需要运行时协商——webpack 在构建阶段就处理好了。但代价是你要把所有子应用绑在同一个构建工具链上,技术栈的自由度实际上受到限制。
框架方案:single-spa 和它的衍生们
single-spa 是社区公认的元老级方案。它提供了一个微前端的生命周期管理框架,让你在主应用中注册子应用,根据路由切换来挂载和卸载。
qiankun 是蚂蚁团队在 single-spa 基础上封装的方案,加了 JS 沙箱和样式隔离,并且对 umi 生态友好。
icestark 是阿里飞冰团队的方案,设计思路跟 single-spa 类似,对 React 技术栈更友好。
这三个方案里,qiankun 在国内的社区活跃度和文档完善度是目前最高的。如果你的团队想在两周内把微前端跑起来,qiankun 是我会优先推荐试试的方案。
用 qiankun 把微前端跑起来
下面是一个最小可用的 qiankun 接入流程。我们用 React 做主应用,接入两个 Vue 子应用。
主应用:注册子应用
先创建主应用并安装 qiankun:
1 | npx create-react-app main |
然后在入口文件里注册子应用并启动:
1 | import { registerMicroApps, start } from 'qiankun'; |
registerMicroApps 的三个关键参数:
entry:子应用的入口 HTML 地址,qiankun 会用import-html-entry去 fetch 它container:子应用要渲染到主应用里的哪个 DOM 节点activeRule:当浏览器 URL 匹配这个规则时激活该子应用
子应用:导出生命周期
qiankun 通过 import-html-entry 加载子应用,要求子应用在自己的入口文件里导出三个生命周期函数:
1 | /** |
注意那个 props.container ? ... : ... 的判断——子应用在独立开发时(没有主应用包裹),props.container 是 undefined,需要 fallback 到 document.getElementById('root')。这个细节让你的子应用既能独立运行调试,也能被 qiankun 加载。
打包配置
子应用的 webpack 输出必须是 UMD 格式,qiankun 才能在运行时拿到子应用导出的生命周期函数:
1 | const packageName = require('./package.json').name; |
jsonpFunction 的命名很重要——如果多个子应用都用了默认的 webpackJsonp,它们的 webpack chunk 加载会互相冲突。
Vue 和 React 子应用的具体接入细节,qiankun 官方文档已经写得很清楚:
拆开看:自己实现一个微前端框架
用框架是一回事,理解框架内部工作原理是另一回事。如果你需要在面试里深入聊微前端,或者你的团队有特殊需求想要一个定制的轻量方案,下面这些核心模块值得了解。
应用注册与路由劫持
所有微前端框架做的第一件事,就是监听路由变化,然后决定激活哪个子应用。
浏览器提供了两种路由模式:history(pushState / replaceState)和 hash(hashchange 事件)。你需要同时劫持这两者:
- 对
pushState和replaceState,包装原生方法,在调用后触发子应用匹配逻辑 - 对
popstate和hashchange,监听事件,同样触发匹配逻辑
匹配逻辑本身很简单:遍历注册的子应用列表,找到 activeRule 与当前路径匹配的应用,卸载当前应用,挂载新应用。
生命周期管理
微前端框架通常定义两套生命周期:
主应用侧:
beforeLoad:挂载子应用前,适合做权限校验或 loading 状态mounted:子应用挂载完成后,适合做埋点或全局状态更新unmounted:子应用卸载后,适合做清理工作
子应用侧(由子应用导出):
bootstrap:首次加载时调用一次,初始化子应用全局配置mount:每次激活时调用,渲染子应用unmount:每次卸载时调用,销毁子应用实例
资源加载:不只是 fetch 一个 HTML
qiankun 的 import-html-entry 做的事比看起来多:
- fetch 子应用的入口 HTML
- 从中提取所有的
<script>和<link>标签 - 依次执行 JS 代码(注意是依次执行,因为可能有依赖顺序)
- 收集子应用导出的生命周期函数
有一个容易忽略的点:子应用的 JS 里可能有 document.createElement('script') 动态插入的脚本,这些也需要被拦截和处理。
JS 沙箱:每个子应用活在自己的世界里
如果多个子应用都在 window 上挂全局变量,冲突不可避免。JS 沙箱解决的就是这个问题。
核心思路是用 Proxy 为每个子应用创建一个假的 window 对象:
1 | export class ProxySandbox { |
几个设计决策值得注意:
running开关:子应用被卸载后,不能再往 fakeWindow 写数据。这防止了一个已卸载的子应用(比如还有未清理的定时器回调)污染全局状态。has()永远返回true:这让子应用里'document' in window这种检查不会报错,同时配合get中的 fallback 逻辑,让子应用感知不到自己运行在沙箱里。函数类型的属性
bind到真实window:比如addEventListener,如果不 bind,函数内部的this会指向 fakeWindow,导致事件监听失效。
样式隔离
除了 JS 沙箱,样式隔离是另一个大课题。常见方案有:
- Shadow DOM:天然的样式隔离,但子应用里的第三方组件(比如 antd 的弹窗挂载到 body)会逃逸
- CSS Modules / CSS-in-JS:在构建阶段加前缀,但要求子应用配合
- 运行时前缀:qiankun 的实验性功能,动态给所有样式选择器加前缀
老实说,样式隔离是微前端里最不优雅的部分,至今没有一个方案能同时兼顾隔离性和兼容性。
预加载
在用户浏览当前子应用时,提前加载下一个可能访问的子应用的资源。这个策略可以显著减少切换子应用时的白屏时间——尤其是那些打包体积比较大的子应用。
什么时候不该用微前端
微前端解决的是组织层面的问题,但它引入的技术复杂度是实实在在的。以下情况我建议多想想:
- 团队不到 10 人,应用还在快速迭代阶段。 微前端带来的额外配置和调试成本可能比它解决的问题还多。
- 子应用之间有大量共享状态。 微前端的理想场景是业务领域边界清晰,如果两个”子应用”实际上是紧密耦合的,强行拆开只会增加通信开销。
- 对首屏性能要求极高。 微前端通常需要在运行时动态加载子应用,首屏会比单体 SPA 多一次额外的资源请求。
我的判断标准很简单:如果你没有因为多团队协作而痛苦,就不要引入为多团队协作而设计的架构。
从哪里开始
如果你想在项目里尝试微前端,我的建议是:
- 从一个低风险的子应用开始。 别一上来就动核心业务。找一个相对独立的功能模块——比如后台的”系统设置”或”操作日志”——用微前端的方式接入。
- 先把 CI/CD 流水线跑通。 独立部署是微前端的核心价值,如果上线还要手动协调,那你只是把代码拆开了,没得到真正的好处。
- 读 qiankun 源码。 代码量不大,核心逻辑就几千行。理解它怎么处理路由劫持和沙箱,会让你在遇到诡异 bug 时少走很多弯路。
微前端不是什么高深的技术,它是一种工程上的取舍:用运行时的额外复杂度,换组织层面的并行效率。理解了这一点,你就知道什么时候该拿起它,什么时候该放下。