先证明需要,再谈方案
微前端是少数选错代价远大于选对收益的架构。这份把四条路线、三种沙箱、样式隔离的四代方案摊开讲,每一节都给出"什么时候不该用"。
01先问组织几个团队、发布节奏是否真的冲突。答不上来就先别上。
02认准隔离强度iframe 最强体验最差,MF 最弱但能共享代码,中间是 qiankun 和 wujie。
03能说清逃逸点Proxy 沙箱拦不住什么、样式隔离治不了什么,这才是深度。
1
微前端解决的不是技术问题,是组织问题★
先说清代价再谈方案
微前端真正要解决的是多个团队要独立发布同一个前端这件事。如果只有一个团队、一个发布节奏,引入微前端只会凭空多出三层复杂度:沙箱、隔离、通信。所以面试里第一句应该是"先确认是不是真的需要"。
没有微前端时,多团队的典型冲突
- 两个团队共用一个仓库,发布要互相等
- 升级一个公共依赖(比如 React 大版本)要全量回归
- 一个团队的代码风格和目录结构扩散到全局
- 构建时间随项目线性增长,改一行要等十分钟
微前端换来的能力
- 每个子应用独立仓库、独立构建、独立发布
- 技术栈可以不同(老系统 Vue2 不用立刻升级)
- 故障隔离,一个子应用崩了不影响主框架
- 代价:多了一层运行时,调试和排查变复杂
- 最容易被忽略的代价:本地开发调试变难(要同时起好几个应用)、依赖重复加载导致体积膨胀、跨应用的样式和全局变量互相污染、错误堆栈被截断难以定位。
- "技术栈无关"是个双刃剑:能让老系统继续用 Vue2,但也意味着团队之间没法互相改代码,公共组件要维护多份。
- 替代方案要先排除:monorepo 能解决"独立发布"里的一部分问题(独立构建、独立版本),只是共用一个仓库。如果团队的痛点只是构建慢和互相等待,monorepo 加 CI 缓存可能就够了,不需要微前端。
面试怎么答微前端解决的是多个团队独立发布同一个前端的问题,不是纯技术选型。它的收益是独立仓库独立构建独立发布、技术栈可以不同、故障隔离;代价是多一层运行时,本地调试、依赖复用、样式隔离、错误定位都会变复杂。所以在技术方案之前先确认是不是真的需要,如果痛点只是构建慢和互相等待,monorepo 加 CI 缓存可能是更轻的解法。
2
四条路线:iframe / qiankun / wujie / Module Federation★
隔离强度和体验是反比
四条路线的核心差别是隔离靠什么:iframe 靠浏览器原生、qiankun 靠 Proxy 沙箱、wujie 靠"iframe 跑 JS + shadow DOM 渲染"、Module Federation 干脆不做隔离只管共享。隔离越强,体验和改造成本越差。
四条路线的取舍【iframe】隔离 100%,体验 60%
JS / CSS / 全局变量全隔离,靠浏览器原生
问题:弹窗无法覆盖父页面、路由和前进后退割裂、
父子通信只能 postMessage、首屏多一次文档加载
适合:嵌入第三方不可信页面、老系统改造预算极低
【qiankun】隔离 80%,体验 85%
基于 single-spa,JS 用 Proxy 沙箱,CSS 用 scoped 或 shadow DOM
子应用按约定的生命周期导出 bootstrap / mount / unmount
问题:沙箱不是真隔离(有些全局逃逸)、样式隔离靠约定
适合:大多数中大型企业的后台系统
【wujie】隔离 95%,体验 85%
每个子应用一个隐藏 iframe 跑 JS(拿到原生隔离)
渲染到主应用的 shadow DOM 里(布局和弹窗正常)
问题:社区和生态比 qiankun 小
适合:既想要强隔离又不想牺牲体验
【Module Federation】不做隔离,只做共享
webpack 5 / Vite 的运行时模块共享
优点:依赖能共享一份,不重复打包
缺点:要求所有子应用用同一套构建工具和框架版本
适合:同一技术栈、想共享依赖的场景
- 为什么 iframe 的体验问题绕不过去:iframe 是一个独立的文档,它的 fixed 定位相对于 iframe 视口、z-index 出不去、路由历史是独立的。这些是浏览器安全模型的限制,不是实现问题。
- wujie 的思路为什么巧妙:把"执行环境"和"渲染位置"拆开。JS 在 iframe 里跑所以拿到原生隔离,DOM 渲染到主文档的 shadow DOM 里所以布局正常。代价是 iframe 和 shadow DOM 之间的同步要处理很多细节。
- qiankun 的沙箱不是真隔离:它用 Proxy 拦
window上的读写,但有些场景拦不住(比如直接操作document.body、定时器、事件监听)。所以它还有一套"快照沙箱"兜底。 - Module Federation 不是微前端的替代品:它解决"代码复用",不解决"独立发布和隔离"。两者经常一起用:qiankun 管隔离,MF 管共享依赖。
面试怎么答四条路线的核心差别是隔离靠什么。iframe 靠浏览器原生,隔离最强但体验最差,弹窗盖不住父页面、路由割裂、通信只能 postMessage。qiankun 基于 single-spa,用 Proxy 做 JS 沙箱、scoped 或 shadow DOM 做样式隔离,是大多数后台系统的选择,但沙箱不是真隔离。wujie 把执行和渲染拆开,JS 跑在隐藏 iframe 里拿到原生隔离,DOM 渲染到 shadow DOM 里保证布局正常。Module Federation 不做隔离,只解决运行时依赖共享,要求技术栈统一。
3
沙箱的三种实现★
这是微前端最常被追问的技术点
JS 沙箱要解决一件事:子应用改
window 不影响别人,切走时能还原。三种实现:快照沙箱(记录前后 diff,只能单实例)、Proxy 沙箱(虚拟 window,可多实例)、iframe 沙箱(真 window,最彻底)。◐ 可交互:开不开沙箱,子应用之间会不会互相污染
每个子应用有自己的假 window,写进去的东西出不来。
子应用 A
A 读到 theme:undefined
子应用 B
B 读到 theme:undefined
真实 window 上多出来的 key
(干净)
沙箱开启时,A 写的东西只在 A 的假 window 上,B 读不到,真实 window 也是干净的。
三种沙箱的对比【快照沙箱】snapshot
激活时给 window 拍快照,退出时 diff 出改了什么、还原
优点:实现简单
缺点:只能有一个子应用在跑;window 属性多时 diff 很慢
用于:qiankun 的单实例模式
【Proxy 沙箱】proxy
每个子应用一个 Proxy 假 window
优点:支持多实例同时运行,不用遍历 window
缺点:逃逸情况多(document.body 直接改、事件监听、定时器)
用于:qiankun 的多实例模式、大部分自研方案
【iframe 沙箱】
子应用在 iframe 里执行,window 是真的、原生隔离
优点:隔离彻底,不用维护 Proxy 的边界
缺点:iframe 和主文档通信有成本
用于:wujie
- Proxy 沙箱的逃逸点在哪:代码里写
document.body.appendChild、document.addEventListener、setInterval、localStorage都是直接操作真对象,Proxy 拦不到。所以还要给 document、定时器、storage 也套一层代理,或者靠卸载时统一清理。 - 为什么用
with:with能把一个对象放在作用域链最前面,这样子应用里裸写foo = 1就落在假 window 上,而不是真 window。with在业务代码里被禁用,在这里是唯一合理的用法。 - 定时器和事件监听必须记录:卸载时要 clear 掉,否则子应用切走后还在跑,内存泄漏且逻辑错乱。这是"沙箱"之外必须单独做的一套清理机制。
面试怎么答JS 沙箱要保证子应用改 window 不影响别人、切走能还原。三种实现:快照沙箱在激活时给 window 拍快照,退出时 diff 还原,实现简单但只支持单实例;Proxy 沙箱给每个子应用一个假 window,支持多实例,但 document、定时器、事件监听这些直接操作真对象的会逃逸,要额外代理和清理;iframe 沙箱在真 window 里执行,隔离最彻底。实现 Proxy 沙箱的关键是用 with 把假 window 放进作用域链最前面。
4
样式隔离:从约定到真隔离★
四代方案,越往后越狠
样式隔离的难度在于CSS 是全局的,而子应用的样式表被插入到主文档里。四代方案:命名约定 → 运行时改选择器加前缀 → shadow DOM → iframe。qiankun 默认给的是第二种。
四代方案与它们的破绽【一、命名约定】BEM 加应用前缀
.app-order__title { }
破绽:靠人守,第三方库和 UI 框架的全局样式照样污染
【二、运行时改选择器】qiankun 的 experimentalStyleIsolation
.title { } → div[data-qiankun="order"] .title { }
优点:不用改子应用代码
破绽:@keyframes、@font-face、:root 变量不会被改写;
动态插入的 style 标签要监听;!important 仍然穿透
【三、shadow DOM】真隔离
shadowRoot 里的样式出不去,外面的进不来
破绽:弹窗渲染到 body 时会脱离 shadow,
第三方库大量依赖 document 查询会失效
【四、iframe】最彻底
style 完全独立,连 CSS 变量都不继承
- 最常见的真实问题不是样式泄漏,而是全局变量泄漏:子应用改了
:root上的 CSS 变量、或者引入了某个 UI 库的全局 reset,把主应用的按钮全改了。这类问题运行时改选择器治不了。 - z-index 打架:子应用的弹窗 z-index 是 1000,主应用的导航是 9999,结果弹窗被盖住。解法是在沙箱里维护一个独立的 z-index 基准,或者约定各应用的分段。
- 弹窗挂载点:子应用的 Modal 通常
appendChild到document.body,这会脱离子应用的样式作用域。要提供主应用统一的挂载容器,或者让子应用可配置挂载点。
面试怎么答样式隔离难在 CSS 是全局的,而子应用的样式表插在主文档里。方案有四代:命名约定靠自觉、运行时给选择器加前缀(qiankun 默认做法,但 @keyframes 和 :root 变量改不到)、shadow DOM 真隔离但弹窗会脱离、iframe 最彻底但体验差。实际项目里更麻烦的往往是 CSS 变量和 UI 库的全局 reset 泄漏,以及子应用弹窗挂到 document.body 后 z-index 打架,这些要靠统一挂载点和 z-index 分段约定解决。
5
路由分发:谁管 URL
主应用统一管,还是子应用自管
两种模式:主应用统一管路由(子应用作为某个路径前缀下的内容,URL 由主应用解析)和子应用自管路由(主应用只负责加载,子应用内部有自己的 router)。前者简单,后者适合子应用本身是个完整应用的情况。
- 为什么子应用要设 base:子应用的资源路径和路由都相对于自己的部署位置。不设 base,子应用里的
/detail会跳到主应用的根路径,看起来像"跳出去了"。 - 刷新问题:直接访问
/order/detail/42时,服务端必须回退到主应用的 index.html,主应用启动后按 activeRule 判断该加载哪个子应用,子应用再按自己的路由渲染。/order 这个前缀的 Nginx 配置要和 activeRule 对齐。 - 前进后退:single-spa 会监听 popstate 统一调度,子应用内部的路由跳转也会反映到 URL 上。但两个 router 同时监听 history 容易打架,通常让子应用用 memory history 或设 base 隔离。
面试怎么答路由分发有两种模式。一是主应用统一管,用 activeRule 按路径前缀决定激活哪个子应用,子应用作为该前缀下的内容,这种方式简单,子应用的 router 要设 base 前缀。二是子应用自管路由,主应用只管加载和传 base。不管哪种,服务端都要把子应用路径回退到主应用入口,且 Nginx 的前缀配置要和 activeRule 对齐,否则刷新会 404。
6
公共依赖:共享还是各带一份★
体积膨胀的主要来源
默认情况下每个子应用都会打包一份 React / Vue,主应用再带一份,就是三份。解法有三:externals 外置由主应用提供(简单但版本必须一致)、Module Federation 运行时共享(灵活)、不管(子应用按需懒加载时影响有限)。
三种共享方式的取舍【externals】主应用提供,子应用不打包
// webpack
externals: { react: 'React', 'react-dom': 'ReactDOM' }
// 子应用里 import React from 'react' 会被替换成 window.React
优点:立竿见影,体积直接减掉
缺点:版本强绑,子应用不能用不同的 React 版本
【Module Federation】运行时按需共享
shared: { react: { singleton: true, requiredVersion: '^18' } }
优点:可以指定版本范围,singleton 保证全局一份
缺点:要求都用 MF 体系构建
【各带一份】
优点:子应用完全自治,升级互不影响
代价:体积翻倍(React + ReactDOM 约 130KB gzip)
什么时候可接受:子应用是懒加载的,首屏不受影响
- singleton 是什么:MF 的
singleton: true保证同一个依赖在所有应用里只实例化一次。React 必须 singleton——两份 React 会导致 hooks 报错("Invalid hook call"),因为它们各自维护自己的 dispatcher。 - 版本不一致怎么办:MF 会在版本不满足时降级加载自己的副本,同时打 warning。如果那个依赖必须单例(React、Vue),就会出现运行时错误。所以要么统一版本,要么别共享。
- 公共组件库怎么处理:这是最容易出问题的地方。每个子应用带一份组件库,样式会重复插入,导致主题色被后加载的覆盖。解法是组件库也走共享,或者用 CSS 变量做主题让多份样式等价。
面试怎么答默认每个子应用都打包一份框架,主应用再带一份,体积就翻倍。三种解法:externals 把 React 交给主应用提供,最简单但版本强绑;Module Federation 做运行时共享,可以指定版本范围,singleton 保证全局一份;或者干脆各带一份,代价是体积翻倍,但子应用懒加载时首屏不受影响。要注意 React 这类必须 singleton,两份会导致 Invalid hook call 错误。
7
通信:别让子应用互相知道
中心化的消息总线
原则是主应用做总线,子应用之间不直接通信。做法是主应用提供一个事件总线(甚至是状态 store)通过 props 注入,子应用只发事件不关心谁听。这样加一个新子应用不用改任何已有应用。
- 为什么不用全局 window 挂变量:那是隐式依赖,谁改了什么查不出来。而且沙箱会拦 window,行为不确定。
- 事件 vs 共享状态:事件适合"发生了某件事"(登出、切换主题);共享状态适合"某个值变了"(当前用户、权限列表)。后者用 store 更好,因为新加载的子应用能直接读到当前值,而事件是过去时,错过了就没了。
- 卸载要注销监听:子应用 unmount 时如果没清掉 bus 上的监听,下次再加载会重复注册,一个事件触发两次。这是很常见的 bug。
面试怎么答通信的原则是主应用做总线,子应用之间不直接通信,这样新增子应用不用改已有的。实现上是主应用提供一个事件总线或共享 store,通过 props 注入给子应用。事件适合"发生了某件事",共享状态适合"某个值变了",后者更好,因为后加载的子应用能直接读到当前值,不会错过。要注意子应用卸载时必须注销监听,否则重复注册会导致一次事件触发多次。
8
生命周期:那三个函数到底做什么★
bootstrap 只跑一次,这条最要命
single-spa 规定子应用导出三个函数:bootstrap(只执行一次)、mount(每次进入)、unmount(每次离开)。关键差别是bootstrap 不会重复执行,所以全局初始化必须放 mount,不能放 bootstrap——这是最常见的"第二次进入就白屏"的原因。
- 为什么"第二次进入白屏"总是 bootstrap 的锅:很多模板把
ReactDOM.render写在 bootstrap 里。第一次进入正常,切走 unmount 清了 DOM,再切回来 bootstrap 不再执行,就什么都没渲染。 - Vue 应用要重建实例:unmount 时
app.unmount(),下次 mount 要重新createApp并mount到容器。复用旧实例会报错。 - 老应用没有导出这三个函数:single-spa 提供了
single-spa-html、single-spa-vue、single-spa-react这些适配器,帮你把普通应用包成符合约定的形式。改造老系统时就是用这些。
面试怎么答子应用要导出 bootstrap、mount、unmount 三个函数,其中 bootstrap 整个生命周期只执行一次,mount 和 unmount 每次进出都会执行。所以全局初始化和事件绑定要放 mount 而不是 bootstrap,否则第二次进入就不会执行,表现为白屏。unmount 里必须清理定时器和注销事件监听,否则会泄漏和重复触发。老应用改造可以用 single-spa 提供的框架适配器包一层。
9
什么时候不该上微前端★
三个明确的"不要"
三种情况应该明确拒绝:只有一两个团队、需要频繁共享组件、以及性能是硬指标。微前端的成本是持续的运维成本,不是一次性的改造成本。
先评估这三个问题1. 有几个团队?发布节奏真的冲突吗?
一个团队 → 不需要。用 monorepo 解决构建和复用问题
两个团队但节奏一致 → 大概率也不需要
2. 公共组件多不多?
多 → 微前端会让每个应用各维护一份,反而更痛
替代:内部组件库 + 版本管理
3. 首屏性能是硬指标吗?
是 → 微前端会多一层运行时和重复依赖
替代:单个应用做路由级分包
仍然需要的情况:
多个团队、独立发布是刚需
技术栈暂时无法统一(收购来的系统、老系统)
单仓构建时间已经影响研发效率
- "先做微前端"最常见的后果:项目只有两个页面,却引入了一整套沙箱、注册、通信机制。新人上手成本高,出问题没人能排查。
- 包大小和时间的关系:微前端框架本身(qiankun 约 40KB gzip)加上重复的框架依赖,首屏多出 100-200KB 很常见。首屏指标紧的产品要慎重。
- 组织能力是前提:微前端要求每个子应用真的能独立发布、独立运维、独立负责。如果 CI 还共用一条流水线,边界就是形式上的。
- 替代方案清单:monorepo 加 pnpm workspace(复用和独立构建)、路由级拆包(首屏)、组件库加版本管理(复用)、BFF 层聚合(接口)。这四样能解决大部分"想上微前端"的动机。
面试怎么答三种情况不该上微前端:只有一个团队、公共组件很多、首屏性能是硬指标。因为微前端带来的是持续的运维成本,沙箱、注册、通信、依赖重复都是长期负担。如果动机只是构建慢和想复用,monorepo 加 pnpm workspace、路由级拆包、组件库版本管理这几样更轻。真正需要的是多团队独立发布、技术栈暂时无法统一、单仓构建已成瓶颈这几种情况。
10
Module Federation 到底共享了什么
运行时去别人的产物里拿模块
Module Federation 的核心是运行时:A 应用不需要在构建时知道 B 的存在,而是在运行到某个时刻去 B 的产物里取模块。这让"独立部署 + 共享代码"第一次能同时成立,代价是运行时依赖网络和版本协商。
- 必须先加载 remoteEntry:它是"清单文件",里面记录了有哪些模块、各自在哪个 chunk。host 启动时要先加载它,否则 import 会失败。常见的做法是在入口处 await 一个
init。 - 版本协商是运行时做的:host 要求 ^18,remote 提供 17,MF 会加载 remote 自己的副本并警告。如果那个依赖是 singleton(比如 React),就会出现两份实例的灾难。
- 和微前端的定位差别:MF 解决代码共享,不解决沙箱和隔离——因为它假定大家是同一个技术栈、能互相信任。所以"用 MF 做微前端"其实是在没有隔离的前提下做模块共享。
- Vite 也能用:通过
@originjs/vite-plugin-federation或 Module Federation 2.0,但生态和 webpack 版有细微差异,跨构建工具混用时要验证。
面试怎么答Module Federation 的核心是运行时共享:应用启动时去 remote 的 remoteEntry 清单里取模块,构建时不需要知道对方存在。这让独立部署和代码共享能同时成立。代价是运行时依赖网络,版本协商也在运行时做,如果某个依赖要求 singleton 而版本对不上,就会出现两份实例。它解决的是模块复用,不解决沙箱和隔离,所以它和 qiankun 这类方案是互补而不是替代关系。
11
速答清单
考前十分钟只看这一页
01微前端真正解决什么问题?
多个团队独立发布同一个前端。不是技术选型,是组织问题。只有一个团队时引入只会多出沙箱、隔离、通信三层复杂度。
02iframe 做微前端最大的问题?
体验:弹窗盖不住父页面、路由历史独立、fixed 定位相对于 iframe、父子只能 postMessage。这是浏览器安全模型的限制,改不了。
03qiankun 的 JS 沙箱怎么实现?
Proxy 给每个子应用一个假 window,用 with 把假 window 放进作用域链最前面,读写都落在假 window 上。
04Proxy 沙箱有哪些逃逸点?
document.body 直接操作、事件监听、定时器、localStorage 这些直接操作真对象的拦不住,要额外代理并在卸载时统一清理。
05快照沙箱和 Proxy 沙箱的区别?
快照在激活时给 window 拍照、退出时 diff 还原,实现简单但只支持单实例;Proxy 用假 window,支持多实例同时运行。
06wujie 的思路是什么?
把执行和渲染拆开:JS 在隐藏 iframe 里跑拿到原生隔离,DOM 渲染到主文档的 shadow DOM 里保证布局和弹窗正常。
07样式隔离有哪几代方案?
命名约定、运行时给选择器加前缀、shadow DOM、iframe。qiankun 默认是加前缀,治不了 @keyframes、:root 变量和 !important。
08子应用第二次进入白屏,通常是什么原因?
把渲染逻辑写在了 bootstrap 里。bootstrap 只执行一次,unmount 清掉 DOM 后不会再跑,所以什么都渲染不出来。
09unmount 里必须做什么?
卸载框架实例、清理定时器、注销事件总线监听、移除全局事件绑定。不做就会泄漏,且事件会重复触发。
10公共依赖怎么共享?
externals 交给主应用提供最简单但版本强绑;Module Federation 运行时共享可指定版本范围;或者各带一份,代价是体积翻倍。
11为什么 React 必须 singleton?
两份 React 各自维护自己的 hooks dispatcher,会导致 Invalid hook call 错误。
12子应用之间怎么通信?
主应用做总线,通过 props 注入。子应用只发事件不关心谁听。共享状态优于事件,因为后加载的应用能直接读到当前值。
13Module Federation 和微前端的关系?
MF 解决运行时模块共享,不解决沙箱和隔离。两者互补:qiankun 管隔离,MF 管共享依赖。
14什么情况不该上微前端?
只有一个团队、公共组件很多、首屏性能是硬指标。这三种情况用 monorepo、路由拆包、组件库版本管理更轻。
15微前端后端的路由怎么配?
所有子应用路径都回退到主应用入口,主应用按 activeRule 判断加载哪个子应用。Nginx 的前缀配置要和 activeRule 对齐。