跳到主要内容

2 篇博文 含有标签「微前端」

查看所有标签

Module Federation 实战拆解:远程模块、依赖共享与 MF 2.0

· 阅读需 8 分钟

微前端绕不开 Module Federation(MF)。它和 qiankun 那类方案常被放在一起比,但两者其实抽象层级不同:qiankun 管的是「应用怎么隔离地拼在一起」,MF 管的是「模块怎么跨应用加载与共享」。

这篇不空谈:我搭了两个最小应用(host / remote),用 webpack 5.111.0 + @module-federation/enhanced 2.9.0 真跑了一遍构建,把产物、stats、manifest 全抓出来给你看——MF 这套东西「到底生成了什么」,看完就有底了。

声明:本文配置与构建产物均为本机真实运行结果(含 mf-manifest.json 原文)。运行时(浏览器里加载远程模块)未实测,涉及运行时的部分我标明了依据,不编造效果。

大厂自建沙箱实现微前端:从 Proxy 沙箱到 iframe 沙箱的完整拆解

· 阅读需 13 分钟

微前端把多个团队的应用塞进同一个页面,但浏览器只给了一个 window、一个 document、一条渲染管道。两个子应用如果都往全局上写东西、都动全局样式、都监听全局事件,必然互相踩脚。所以微前端框架的核心难点从来不是"怎么把代码加载进来",而是怎么给每个子应用一个互不干扰的"沙箱"

本文拆解各大厂自建沙箱的实现:先讲沙箱的本质(JS 全局隔离有哪几条路),再逐家拆 qiankun(蚂蚁)、micro-app(京东)、wujie(腾讯)、Garfish(字节)的方案与权衡,最后给出一套自建沙箱的工程要点。


1. 沙箱要解决什么问题

微前端里,沙箱至少承担四件事:

要隔离的风险沙箱手段
JS 全局变量子应用 A 写的 window.foo 污染 B伪造 window / iframe 原生隔离
全局方法劫持addEventListenersetTimeout 无处卸载代理 + 应用级缓存清理
样式子应用 CSS 互相影响、影响主应用Shadow DOM / 样式前缀
生命周期卸载后定时器、事件、DOM 残留统一挂载/卸载钩子 + 清理

一句话:沙箱 = 给每个子应用一个"看起来是全局、其实是私有"的运行环境,应用挂载时激活、卸载时清理。


2. 沙箱的本质:JS 全局隔离只有几条路

不管哪家大厂,全局隔离的实现手段就这几条,其余全是组合和优化:

手段隔离级别优点缺点
iframe原生 window 级隔离最彻底、浏览器保证资源重、事件/路由/登录态割裂
Proxy 伪造 window变量级多应用共存、性能好依赖 ES6 Proxy,不兼容旧浏览器
with + eval变量级灵活作用域链性能损耗
Shadow DOM样式/DOM 级原生样式隔离事件冒泡/焦点行为改变(React 事件代理失效)

大厂方案本质上是在"iframe 的原生隔离"和"Proxy 的灵活隔离"之间做选择

  • qiankun / micro-appProxy 路线(多应用灵活共存);
  • wujieiframe + 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 落到 fakeWindowget 沙箱内优先、沙箱内没有才读真实 window:

// qiankun ProxySandbox 的简化核心
function createProxySandbox() {
const fakeWindow = Object.create(null); // 每个子应用独立的"私有 window"
const running = true;

const proxy = new Proxy(fakeWindow, {
set(target, key, value) {
if (running) target[key] = value; // ① 写:只写 fakeWindow,不碰真实 window
return true;
},
get(target, key) {
if (key in target) return target[key]; // ② 读:沙箱内有的用沙箱的
return window[key]; // ③ 沙箱内没有的,才去真实 window 读
},
has(target, key) {
return key in target || key in window; // 让 in / hasOwnProperty 也按此规则
},
});
return proxy;
}

子应用的 JS 怎么"用"这个 fakeWindow? 把子应用代码包进一个立即执行函数,把 window 指向 proxy:

// qiankun 执行子应用 JS(简化):window/self/globalThis 全部换成 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 的拦截逻辑:

// micro-app 变量前置(简化)
for (const key of HIGH_FREQ_GLOBAL_KEYS) {
Object.defineProperty(fakeWindow, key, {
get() { return getRealGlobal(key); }, // 读真实全局,但不再走 proxy.get
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 互相污染"。三条路:

  1. Shadow DOM(wujie):浏览器原生边界,隔离最彻底——但会切断事件冒泡、改变焦点行为,对依赖事件委托的库(React 16 时代)不友好;
  2. 样式前缀 / 作用域化(micro-app、qiankun 也有):把子应用所有 CSS 选择器加一个前缀(如 micro-app[data-xxx] .app),不改变 DOM 结构,兼容最好;
  3. CSS Modules / BEM:工程层面约定,最轻但靠自觉。

没有银弹:要原生隔离选 Shadow DOM,要兼容性选前缀。这也是为什么大厂框架普遍"JS 隔离手段不同、但都会提供样式前缀方案作为降级"。


7. 大厂方案总对比与 2026 现状

框架所属JS 沙箱样式隔离多应用特点
qiankun蚂蚁ProxySandbox(快照→Legacy→Proxy 三代)前缀为主生态最成熟,但维护已放缓
micro-app京东proxy + with(变量前置 + 异步防抖);esm 走 iframe前缀 / Shadow DOM组件化思路,接入最简单
wujie腾讯同域 iframeShadow DOM隔离最强、保活/嵌套,React16 兼容差
Garfish字节路由 + 通信桥为主前缀一体化路由、@garfish/bridge 通信规范
Module FederationWebpack 生态不提供沙箱(靠打包隔离)共享依赖,不解决运行时污染

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) {
// 把 window 指向 proxy 执行子应用代码
return new Function('window', 'self', 'globalThis', code)
.bind(proxy)(proxy, proxy, proxy);
},
dispose() {
active.value = false; // 停止写入 fakeWindow
// 清掉子应用注册的定时器 / 事件 / DOM 引用(略)
},
};
}

样式隔离

  • 首选前缀方案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 原生边界"和"前缀兼容"之间取舍。自建沙箱时,把"执行 → 激活 → 清理"做成闭环,并记住:沙箱解决全局污染,但解决不了加载、路由和通信——它们是微前端里同样重要、却常常被"沙箱"这个名字掩盖的另外三座山。


参考