Appearance
第13章 iframe 的复兴:Wujie 与新一代方案
"被判了死刑的技术,往往只是在等待一个正确的使用方式。"
本章要点
- 理解 iframe 方案"死而复生"的技术背景与根本原因
- 深入 Wujie 的三层架构:WebComponent(渲染层)+ iframe(JS 执行层)+ Proxy(桥接层)
- 掌握 iframe 通信的现代方案:从 postMessage 到 MessageChannel、BroadcastChannel 的进化
- 理解 Wujie 的 Proxy 劫持机制:location、history、document 的精确拦截
- 学会 iframe 场景下的性能优化:预加载、资源共享与降级策略
2019 年,乾坤横空出世,用 Proxy 沙箱取代 iframe 成为微前端的主流隔离方案。那时候,iframe 几乎被整个社区判了"死刑"——性能差、体验割裂、路由无法同步、弹窗不能居中。每一篇微前端选型文章都会在 iframe 方案旁边画一个大大的叉号。
然而,2022 年腾讯开源了 Wujie。它做了一件所有人都觉得不可能的事情:用 iframe 实现了比 Proxy 沙箱更完美的 JS 隔离,同时解决了 iframe 所有的传统痛点。
Wujie 的设计哲学是:把 iframe 当作一个隐藏的 JS 执行沙箱,而不是一个可见的渲染容器。子应用的 DOM 不渲染在 iframe 内部,而是通过 Web Components 投射到主应用的真实文档流中。这个看似简单的思路转换,彻底改变了 iframe 的工程价值。
本章将从源码层面深入剖析 Wujie 的架构设计。你会看到它如何将浏览器原生的 iframe 隔离能力、Web Components 的渲染能力和 Proxy 的劫持能力三者融合,打造出一个兼顾隔离性与用户体验的微前端方案。
13.1 为什么 iframe 又回来了
下图对比了 Proxy 沙箱和 iframe 沙箱在隔离能力上的根本差异:
13.1.1 Proxy 沙箱的天花板
第 4 章我们深入分析了乾坤的 Proxy 沙箱机制。它聪明、优雅,但有一个无法回避的根本性限制:Proxy 只能拦截通过代理对象访问的属性,无法拦截对原始 window 对象的直接访问。
typescript
// 乾坤 Proxy 沙箱的核心逻辑(简化版)
function createProxySandbox(appName: string) {
const fakeWindow = Object.create(null);
const proxy = new Proxy(fakeWindow, {
get(target, prop) {
if (prop in target) return target[prop];
const value = (window as any)[prop];
return typeof value === 'function' ? value.bind(window) : value;
},
set(target, prop, value) {
target[prop] = value;
return true;
},
});
return proxy;
}
// 问题场景一:eval 中的代码直接访问真实 window
eval('window.globalVar = 123'); // 沙箱无法拦截
// 问题场景二:第三方库内部直接写 window.xxx = yyy
// 沙箱只能通过改写 script 的执行上下文来"尽力"拦截
// 问题场景三:with + Proxy 的 Symbol.unscopables 逃逸这些并非理论上的边界情况。在实际生产环境中,大量第三方库(地图 SDK、富文本编辑器、监控 SDK)都会直接操作 window 对象。乾坤为此做了大量的补丁和兼容处理,但本质上是一场无尽的打地鼠游戏。
typescript
// Proxy 沙箱在实践中遇到的典型问题清单
interface ProxySandboxIssues {
// 逃逸问题
evalEscape: '通过 eval/new Function 执行的代码可能逃逸沙箱';
scriptTagEscape: '动态创建的 script 标签默认在全局作用域执行';
iframeEscape: '子应用如果自己创建 iframe,其中的代码完全不受沙箱管控';
// 兼容问题
thirdPartySDK: '高德地图、百度统计等 SDK 直接操作 window';
webWorkerContext: 'Worker 线程不受主线程 Proxy 沙箱影响';
cssVarLeak: 'CSS 变量通过 :root 设置,影响全局文档';
// 性能问题
frequentAccess: '高频属性访问(如动画帧中的 requestAnimationFrame)经过 Proxy 有可测量的开销';
memoryLeak: '沙箱卸载时如果清理不彻底,闭包引用导致内存泄漏';
}13.1.2 iframe 的天然优势被重新审视
与 Proxy 沙箱的"模拟隔离"相比,iframe 提供的是浏览器级别的原生隔离:
typescript
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeWindow = iframe.contentWindow!;
// 完全独立的全局对象
console.log(iframeWindow.window === window); // false
console.log(iframeWindow.document === document); // false
// 完全独立的 JS 执行上下文
iframeWindow.eval('var x = 1');
console.log(typeof x); // "undefined" —— 主应用完全不受影响
// 完全独立的原型链
console.log(iframeWindow.Array === Array); // false
console.log(iframeWindow.Object === Object); // falseiframe 的隔离不是"尽力而为",而是"铜墙铁壁"。这是 V8 引擎层面的上下文隔离——同一个浏览器进程中,不同的 iframe 拥有完全独立的 JavaScript 执行环境、独立的全局对象。无论子应用的代码多么"放肆",都不可能污染主应用。
13.1.3 传统 iframe 的四大缺陷与 Wujie 的破局
既然 iframe 隔离这么好,为什么之前被抛弃了?因为传统用法下的四个致命缺陷:路由状态丢失(刷新后 iframe src 回到初始值)、弹窗无法居中(Modal 只能在 iframe 可视区域内定位)、性能开销(每个 iframe 约 10-20MB 内存)、通信原始(只能通过 postMessage 传递可序列化数据)。
Wujie 的核心洞察可以用一句话概括:iframe 的问题不在于隔离——而在于渲染。把 iframe 的渲染职责剥离出来,只保留它的隔离能力,一切问题都迎刃而解。
typescript
// 传统 iframe:既负责 JS 隔离,也负责 DOM 渲染
// ┌─────────────────────────┐
// │ 主应用 │
// │ ┌───────────────────┐ │
// │ │ iframe │ │ ← 渲染被困在 iframe 内部
// │ │ 子应用 DOM + JS │ │
// │ └───────────────────┘ │
// └─────────────────────────┘
// Wujie:iframe 只负责 JS 隔离,DOM 通过 WebComponent 渲染
// ┌─────────────────────────┐
// │ 主应用 │
// │ ┌───────────────────┐ │
// │ │ WebComponent │ │ ← 子应用 DOM 在这里渲染
// │ │ (Shadow DOM) │ │
// │ └───────────────────┘ │
// │ [hidden iframe] │ ← JS 在这里执行(用户看不到)
// └─────────────────────────┘🔥 深度洞察:技术的"第二次机会"
iframe 的回归是技术演进中一个有趣的现象:被宣判死亡的技术,往往不是技术本身有问题,而是当时的使用方式有问题。 jQuery 的核心思想至今影响着所有 DOM 操作库。XML 在配置文件领域依然繁荣。iframe 也是如此——当我们换一种方式使用它,只取隔离之长、避渲染之短,它就从"最差方案"变成了"接近完美的方案"。这提醒我们:在评估一项技术时,要区分"技术的固有属性"和"使用方式的局限性"。
13.2 Wujie 的架构:WebComponent + iframe + Proxy
13.2.1 架构总览
下图展示了 Wujie 三层架构的数据流和协作方式:
Wujie 的架构由三个核心层组成,每一层都有明确的职责:
typescript
interface WujieArchitecture {
// 第一层:渲染层 —— Web Component + Shadow DOM
renderLayer: {
role: '承载子应用的 DOM 渲染';
technology: 'Custom Element + Shadow DOM';
benefit: '子应用 DOM 在主应用文档流中,弹窗/滚动行为正常';
};
// 第二层:执行层 —— 隐藏 iframe
executionLayer: {
role: '子应用 JS 代码的执行沙箱';
technology: '隐藏的 iframe (src = 同域空白页)';
benefit: '浏览器级别的完美 JS 隔离';
};
// 第三层:桥接层 —— Proxy 劫持
bridgeLayer: {
role: '将 iframe 中的 JS 操作桥接到主应用的 DOM';
technology: 'Proxy 劫持 iframe 的 document、location、history';
benefit: 'JS 在 iframe 中执行,但操作的是 Shadow DOM 里的 DOM 节点';
};
}下图用时序图展示了三层协作的核心数据流,从子应用发起 DOM 操作到用户最终看到渲染结果:
三层协作的核心数据流:子应用 JS 在 iframe 中调用 document.querySelector('#app') → Proxy 拦截 document 访问并重定向到 Shadow DOM → Shadow DOM 中的真实 DOM 被操作 → 浏览器渲染 → 用户在主应用页面中看到子应用内容。
13.2.2 隐藏 iframe 的创建
Wujie 创建 iframe 的方式非常讲究:
typescript
// Wujie 源码分析:创建隐藏的 iframe 沙箱
// 文件路径:src/iframe.ts
function createIframeSandbox(
appName: string,
url: string,
mainHostPath: string
): HTMLIFrameElement {
const iframe = document.createElement('iframe');
const attrsMaps: Record<string, string> = {
// 关键:src 指向主应用的同域空白页,而不是 about:blank
// 因为同域才能自由通信,才能访问 contentWindow
src: mainHostPath,
style: 'display:none',
name: appName,
};
Object.keys(attrsMaps).forEach((key) => {
iframe.setAttribute(key, attrsMaps[key]);
});
document.body.appendChild(iframe);
return iframe;
}两个关键设计决策:第一,iframe 是 display:none 的——它永远不可见,唯一作用是提供独立的 JS 执行上下文。第二,iframe 的 src 指向主应用的同域页面——如果 iframe 跨域,主应用将无法访问 contentWindow,整个 Proxy 桥接方案就崩塌了。
typescript
// 同域 vs 跨域的区别
iframe.src = '/empty.html'; // 同域
iframe.contentWindow; // ✅ 可以访问,可以注入代码
iframe.src = 'https://other-domain.com'; // 跨域
iframe.contentWindow; // ❌ SecurityError13.2.3 Web Component 渲染容器
Wujie 使用 Custom Element 和 Shadow DOM 作为子应用的渲染容器:
typescript
// Wujie 源码分析:Web Component 的定义
// 文件路径:src/shadow.ts
class WujieApp extends HTMLElement {
connectedCallback() {
// open 模式允许外部通过 element.shadowRoot 访问,调试友好
const shadowRoot = this.attachShadow({ mode: 'open' });
// Shadow DOM 内的 CSS 天然隔离
// 子应用的样式不泄漏到主应用,主应用的样式不侵入子应用
}
}
if (!customElements.get('wujie-app')) {
customElements.define('wujie-app', WujieApp);
}
// 最终的 DOM 结构
// <wujie-app data-app-name="order-app">
// #shadow-root (open)
// <html>
// <head><style>...子应用的样式...</style></head>
// <body>...子应用的 DOM...</body>
// </html>
// </wujie-app>