微前端把多个团队的应用塞进同一个页面,但浏览器只给了一个 window、一个 document、一条渲染管道。两个子应用如果都往全局上写东西、都动全局样式、都监听全局事件,必然互相踩脚。所以微前端框架的核心难点从来不是"怎么把代码加载进来",而是怎么给每个子应用一个互不干扰的"沙箱"。
本文拆解各大厂自建沙箱的实现:先讲沙箱的本质(JS 全局隔离有哪几条路),再逐家拆 qiankun(蚂蚁)、micro-app(京东)、wujie(腾讯)、Garfish(字节)的方案与权衡,最后给出一套自建沙箱的工程要点。
1. 沙箱要解决什么问题
微前端里,沙箱至少承担四件事:
| 要隔离的 | 风险 | 沙箱手段 |
|---|
| JS 全局变量 | 子应用 A 写的 window.foo 污染 B | 伪造 window / iframe 原生隔离 |
| 全局方法劫持 | addEventListener、setTimeout 无处卸载 | 代理 + 应用级缓存清理 |
| 样式 | 子应用 CSS 互相影响、影响主应用 | Shadow DOM / 样式前缀 |
| 生命周期 | 卸载后定时器、事件、DOM 残留 | 统一挂载/卸载钩子 + 清理 |
一句话:沙箱 = 给每个子应用一个"看起来是全局、其实是私有"的运行环境,应用挂载时激活、卸载时清理。
2. 沙箱的本质:JS 全局隔离只有几条路
不管哪家大厂,全局隔离的实现手段就这几条,其余全是组合和优化:
| 手段 | 隔离级别 | 优点 | 缺点 |
|---|
| iframe | 原生 window 级 | 隔离最彻底、浏览器保证 | 资源重、事件/路由/登录态割裂 |
| Proxy 伪造 window | 变量级 | 多应用共存、性能好 | 依赖 ES6 Proxy,不兼容旧浏览器 |
| with + eval | 变量级 | 灵活 | 作用域链性能损耗 |
| Shadow DOM | 样式/DOM 级 | 原生样式隔离 | 事件冒泡/焦点行为改变(React 事件代理失效) |
大厂方案本质上是在"iframe 的原生隔离"和"Proxy 的灵活隔离"之间做选择:
- qiankun / micro-app 走 Proxy 路线(多应用灵活共存);
- wujie 走 iframe + WebComponent 路线(牺牲一部分灵活性,换最强的原生隔离);
- micro-app 的 vite/esm 场景 也兜底回 iframe。
3. 方案一:Proxy 沙箱(qiankun 的 ProxySandbox)
qiankun 在 single-spa 之上提供了三代沙箱,演进史本身就是"性能与隔离度"的权衡史:
| 沙箱 | 原理 | 多应用 | 是否污染真实 window | 性能 |
|---|
| SnapshotSandbox(早期) | 激活时快照整个 window,失活时 diff 还原 | ❌ 单例 | 短暂污染(靠还原) | 差(每次遍历整个 window) |
| LegacySandbox(单应用) | Proxy 记录 diff,用三个 Map 存"新增/修改原值/当前值" | ❌ 单例 | 部分污染(仍读写真实 window) | 中 |
| ProxySandbox(主流) | 每个应用一个 fakeWindow,Proxy 拦截读写 | ✅ 多应用并存 | 不污染 | 高 |
ProxySandbox 的核心就十几行——关键在 set 落到 fakeWindow、get 沙箱内优先、沙箱内没有才读真实 window:
function createProxySandbox() {
const fakeWindow = Object.create(null);
const running = true;
const proxy = new Proxy(fakeWindow, {
set(target, key, value) {
if (running) target[key] = value;
return true;
},
get(target, key) {
if (key in target) return target[key];
return window[key];
},
has(target, key) {
return key in target || key in window;
},
});
return proxy;
}
子应用的 JS 怎么"用"这个 fakeWindow? 把子应用代码包进一个立即执行函数,把 window 指向 proxy:
new Function('window', 'self', 'globalThis', `
${childCode} // 子应用打包产物
`)
.bind(windowProxy)(windowProxy, windowProxy, windowProxy);
于是子应用里写 window.xxx = 1,实际写进的是它自己的 fakeWindow;两个子应用各写各的,互不污染。失活时不用做任何还原——因为真实 window 从头到尾就没被动过,只需要把 running 置 false 停止拦截。
4. 方案二:proxy + with(micro-app 及其性能优化)
micro-app(京东零售)把微前端封装成"类 WebComponent 组件"(<micro-app> 标签),JS 沙箱同样是 proxy + with,但它重点解决了两个 Proxy 沙箱没解决的工程问题:
① 变量前置:别在 proxy 的 get 里干重活
频繁的 window.xxx 读会反复走进 proxy 的 get。micro-app 的做法是用 Object.defineProperty 预先把高频全局变量定义到 fakeWindow 上,让常规读写走原生的属性访问而不是 proxy 的拦截逻辑:
for (const key of HIGH_FREQ_GLOBAL_KEYS) {
Object.defineProperty(fakeWindow, key, {
get() { return getRealGlobal(key); },
set(value) { setRealGlobal(key, value); },
});
}
② 异步防抖:避免并行 promise 的渲染风暴
子应用运行时常有大量 promise 回调,micro-app 给 promise 打标记、保证"上一个 promise 执行完成后再进下一个",避免并发触发导致主线程被瞬间塞满。
但 with 有代价:with 改变了作用域链,每次变量访问都可能触发多级查找,性能比裸全局访问差。所以 micro-app 一方面用"变量前置"把热点变量从 with 作用域里摘出来,一方面承认它的 JS 沙箱仍是代理式方案的性能上限所在。
vite / esm 的兜底:iframe 沙箱
with 环境跑不了 ESM(import 无法在 with 块里执行)。所以 micro-app 给 vite 项目准备了 iframe 沙箱:把 esm 产物放进 iframe 执行,再通过重写子应用的原型链实现对 JS 和 DOM 的拦截——"with 沙箱灵活、iframe 沙箱隔离更严",按需二选一。
5. 方案三:iframe 沙箱(wujie 的 JS + Shadow DOM 组合)
5.1 为什么"纯 iframe"不行
iframe 是浏览器原生的最强隔离,但它有一长串坑,这也是微前端界对 iframe 又爱又恨的原因:
| iframe 的问题 | 后果 |
|---|
| 资源消耗大 | 每次都要全新文档环境,内存/计算翻倍 |
| 事件冒泡不穿透 | 子应用里的按键、快捷键主应用统一劫持不了 |
| 路由不同步 | 子应用内跳转,刷新后主应用不知道在哪一页 |
| 登录态不共享 | 子应用要重新登录,三方 cookie 禁用时更糟 |
| 加载失败无感知 | 子应用崩了主应用不知道 |
| 不能预加载缓存 | 也无法共享基础库 |
5.2 wujie:用 iframe 做 JS 沙箱,用 WebComponent 补样式和 DOM
wujie(腾讯)的思路是各取所长:
- JS 沙箱 = 同域 iframe:把子应用 JS 注入主应用同域的 iframe 里执行——iframe 内部是完整的原生 window 隔离,隔离度拉满,还自带
history / location,路由天然独立;
- 样式与 DOM = WebComponent(Shadow DOM):在主应用里创建一个 wujie 自定义元素,子应用的完整 DOM 渲染在它的 Shadow DOM 内,CSS 原生隔离(Shadow DOM 边界外的样式进不去、里面的样式出不来);
- 三套代理打通"iframe 里的 JS"和"主应用里的 DOM":
- Window Proxy:拦截子应用对
window 的访问,路由类事件(popstate/hashchange)路由到 iframe,其他事件委派给主应用;
- Document Proxy:子应用的
document.createElement 等操作落到 Shadow DOM 容器;<script> 注入 iframe 执行、<style> 经处理后加入 Shadow DOM;
- Location Proxy:维护"主应用路径"和"子应用路径"两个上下文并自动互译,实现双向路由同步。
性能与代价:wujie 的通信延迟实测 <50ms(对比 qiankun 100~300ms),DOM 代理可避免 iframe 整体重渲染,内存占用降约 60%;但强制 Shadow DOM 会让 React 的事件代理失效(React 16 兼容差),这是它"隔离强"换来的代价。
6. 样式隔离:Shadow DOM 与前缀之争
JS 沙箱解决"全局变量",样式隔离解决"CSS 互相污染"。三条路:
- Shadow DOM(wujie):浏览器原生边界,隔离最彻底——但会切断事件冒泡、改变焦点行为,对依赖事件委托的库(React 16 时代)不友好;
- 样式前缀 / 作用域化(micro-app、qiankun 也有):把子应用所有 CSS 选择器加一个前缀(如
micro-app[data-xxx] .app),不改变 DOM 结构,兼容最好;
- CSS Modules / BEM:工程层面约定,最轻但靠自觉。
没有银弹:要原生隔离选 Shadow DOM,要兼容性选前缀。这也是为什么大厂框架普遍"JS 隔离手段不同、但都会提供样式前缀方案作为降级"。
7. 大厂方案总对比与 2026 现状
| 框架 | 所属 | JS 沙箱 | 样式隔离 | 多应用 | 特点 |
|---|
| qiankun | 蚂蚁 | ProxySandbox(快照→Legacy→Proxy 三代) | 前缀为主 | ✅ | 生态最成熟,但维护已放缓 |
| micro-app | 京东 | proxy + with(变量前置 + 异步防抖);esm 走 iframe | 前缀 / Shadow DOM | ✅ | 组件化思路,接入最简单 |
| wujie | 腾讯 | 同域 iframe | Shadow DOM | ✅ | 隔离最强、保活/嵌套,React16 兼容差 |
| Garfish | 字节 | 路由 + 通信桥为主 | 前缀 | ✅ | 一体化路由、@garfish/bridge 通信规范 |
| Module Federation | Webpack 生态 | 不提供沙箱(靠打包隔离) | 无 | — | 共享依赖,不解决运行时污染 |
2026 年值得注意的现状:
- qiankun 维护放缓:Proxy 沙箱仍是"多应用灵活共存"的标杆实现,但蚂蚁已基本停止大版本演进,存量项目向"vite + 模块联邦"或"iframe 兜底"迁移;
- iframe 路线抬头:wujie 持续迭代,强隔离、安全敏感(金融/政务)场景偏爱"原生隔离";对 React 16 兼容问题的社区修补也在跟进;
- "还需要沙箱吗"的讨论:Module Federation 时代,不少人质疑运行时沙箱的必要性——但 MF 只解决"构建期共享与加载",运行时全局污染问题它不解决。沙箱是否必要,取决于你的子应用是否"不可信/不可控":内部团队 + 可控代码,可以靠工程约束(模块化 + 命名规范)替代;集成第三方老系统,沙箱就是刚需。
8. 自建沙箱的工程要点
如果要在团队内部自建一个最小可用沙箱,把上面所有方案浓缩成一张 checklist:
JS 隔离(二选一)
- 轻量:
fakeWindow + Proxy(写进私有对象、读时回退真实 window),多应用并存;
- 强隔离:同域 iframe + Document/Window 代理。
执行与恢复
- 子应用代码包进
new Function('window', 'self', 'globalThis', code),bind 上 proxy 再执行;
- 挂载时激活沙箱、卸载时清理——定时器、事件监听、全局变量、DOM 引用一个都不能漏。
一个极简可用的 Proxy 沙箱骨架(约 30 行):
function createMicroSandbox() {
const fakeWindow = Object.create(null);
const active = { value: true };
const proxy = new Proxy(fakeWindow, {
set: (t, k, v) => (active.value ? ((t[k] = v), true) : true),
get: (t, k) => (k in t ? t[k] : window[k]),
});
return {
proxy,
run(code) {
return new Function('window', 'self', 'globalThis', code)
.bind(proxy)(proxy, proxy, proxy);
},
dispose() {
active.value = false;
},
};
}
样式隔离
- 首选前缀方案(
data-app-xxx 属性 + 选择器前缀),兼容性最好;
- 需要原生隔离再上 Shadow DOM,但要评估对事件委托的影响。
别忘了沙箱之外:真正的微前端是"加载 + 沙箱 + 路由 + 通信 + 生命周期"五件套,沙箱只是其中一环——加载(html entry / 模块联邦)、路由互译、父子通信,每一件都是独立的大工程。
9. 一句话总结
沙箱是微前端的核心难点,本质是"给每个子应用一个看起来全局、实际私有的运行环境",实现手段只有几条路:Proxy 伪造 window(qiankun 的 ProxySandbox:写进 fakeWindow、读时回退真实 window,多应用共存不污染)、proxy + with(micro-app:变量前置 + 异步防抖优化,esm 场景兜底 iframe)、iframe 原生隔离(wujie:JS 进同域 iframe、DOM/样式进 Shadow DOM,用三套代理打通,隔离最强但牺牲兼容)。样式隔离在"Shadow DOM 原生边界"和"前缀兼容"之间取舍。自建沙箱时,把"执行 → 激活 → 清理"做成闭环,并记住:沙箱解决全局污染,但解决不了加载、路由和通信——它们是微前端里同样重要、却常常被"沙箱"这个名字掩盖的另外三座山。