无界微前端源码解析:沙箱机制
深入分析 iframe 沙箱的创建、patch 和 JS 执行隔离原理。
为什么用 iframe
iframe 是浏览器原生的隔离方案,具备:
- 完整的 window 对象:独立的全局作用域
- 原生 JS 隔离:变量不会污染主应用
- 原生路由隔离:独立的 history 和 location
无界的创新在于:只用 iframe 运行 JS,DOM 渲染在 Shadow DOM 中。
iframe 创建
// packages/wujie-core/src/iframe.ts
export function iframeGenerator(
sandbox: WuJie,
attrs: { [key: string]: any },
mainHostPath: string,
appHostPath: string,
appRoutePath: string
): HTMLIFrameElement {
// 1. 创建 iframe
const iframe = document.createElement("iframe");
// 2. 设置属性
setAttrsToElement(iframe, {
src: mainHostPath, // 同域,避免跨域问题
style: "display: none",
...attrs,
[WUJIE_DATA_FLAG]: "",
});
// 3. 插入文档
document.body.appendChild(iframe);
// 4. 等待 iframe 加载
sandbox.iframeReady = stopIframeLoading(iframe).then(() => {
// 5. 初始化 iframe DOM
initIframeDom(iframe.contentWindow, sandbox, mainHostPath, appHostPath);
// 6. 注入变量
patchIframeVariable(iframe.contentWindow, sandbox, appHostPath);
});
return iframe;
}
关键点:
src设为主应用域名,保证同域display: none隐藏 iframe- 通过
stopIframeLoading阻止 iframe 加载主应用内容
阻止 iframe 加载
// packages/wujie-core/src/iframe.ts
function stopIframeLoading(iframe: HTMLIFrameElement) {
const iframeWindow = iframe.contentWindow;
const oldDoc = iframeWindow.document;
return new Promise<void>((resolve) => {
function loop() {
setTimeout(() => {
let newDoc;
try {
newDoc = iframeWindow.document;
} catch (err) {
newDoc = null;
}
// 等待 document 就绪
if (!newDoc || newDoc == oldDoc) {
loop();
return;
}
// 停止加载
iframeWindow.stop ? iframeWindow.stop() : newDoc.execCommand("Stop");
resolve();
}, 1);
}
loop();
});
}
初始化 iframe DOM
// packages/wujie-core/src/iframe.ts
function initIframeDom(iframeWindow: Window, wujie: WuJie, mainHostPath: string, appHostPath: string): void {
const iframeDocument = iframeWindow.document;
// 1. 创建空白文档
const newDoc = window.document.implementation.createHTMLDocument("");
const newDocumentElement = iframeDocument.importNode(newDoc.documentElement, true);
iframeDocument.replaceChild(newDocumentElement, iframeDocument.documentElement);
// 2. 保存原生方法
iframeWindow.__WUJIE_RAW_DOCUMENT_HEAD__ = iframeDocument.head;
iframeWindow.__WUJIE_RAW_DOCUMENT_QUERY_SELECTOR__ = iframeWindow.Document.prototype.querySelector;
iframeWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__ = iframeWindow.Document.prototype.createElement;
// 3. 初始化 base 标签
initBase(iframeWindow, wujie.url);
// 4. patch history
patchIframeHistory(iframeWindow, appHostPath, mainHostPath);
// 5. patch 事件
patchIframeEvents(iframeWindow);
// 6. patch window
patchWindowEffect(iframeWindow);
// 7. patch document
patchDocumentEffect(iframeWindow);
// 8. patch Node
patchNodeEffect(iframeWindow);
// 9. patch 相对路径
patchRelativeUrlEffect(iframeWindow);
}
patch history
// packages/wujie-core/src/iframe.ts
function patchIframeHistory(iframeWindow: Window, appHostPath: string, mainHostPath: string): void {
const history = iframeWindow.history;
const rawHistoryPushState = history.pushState;
const rawHistoryReplaceState = history.replaceState;
// 劫持 pushState
history.pushState = function (data: any, title: string, url?: string): void {
// 将子应用路径转换为主应用路径
const baseUrl = mainHostPath + iframeWindow.location.pathname +
iframeWindow.location.search + iframeWindow.location.hash;
const mainUrl = getAbsolutePath(url?.replace(appHostPath, ""), baseUrl);
rawHistoryPushState.call(history, data, title, url === undefined ? undefined : mainUrl);
if (url === undefined) return;
// 更新 base 标签
updateBase(iframeWindow, appHostPath, mainHostPath);
// 同步路由到主应用
syncUrlToWindow(iframeWindow);
};
// replaceState 同理
history.replaceState = function (data: any, title: string, url?: string): void {
// ...类似逻辑
};
}
patch 事件监听
// packages/wujie-core/src/iframe.ts
function patchIframeEvents(iframeWindow: Window) {
iframeWindow.__WUJIE_EVENTLISTENER__ = new Set();
iframeWindow.addEventListener = function addEventListener(type, listener, options) {
// 运行插件钩子
execHooks(iframeWindow.__WUJIE.plugins, "windowAddEventListenerHook", iframeWindow, type, listener, options);
// 记录事件
iframeWindow.__WUJIE_EVENTLISTENER__.add({ type, listener, options });
// 路由相关事件保留在 iframe
if (appWindowAddEventListenerEvents.includes(type)) {
return rawWindowAddEventListener.call(iframeWindow, type, listener, options);
}
// 其他事件代理到主应用 window
rawWindowAddEventListener.call(window.__WUJIE_RAW_WINDOW__ || window, type, listener, options);
};
iframeWindow.removeEventListener = function removeEventListener(type, listener, options) {
// 运行插件钩子
execHooks(iframeWindow.__WUJIE.plugins, "windowRemoveEventListenerHook", iframeWindow, type, listener, options);
// 移除记录
iframeWindow.__WUJIE_EVENTLISTENER__.forEach((o) => {
if (o.listener === listener && o.type === type && options == o.options) {
iframeWindow.__WUJIE_EVENTLISTENER__.delete(o);
}
});
// 对应移除
if (appWindowAddEventListenerEvents.includes(type)) {
return rawWindowRemoveEventListener.call(iframeWindow, type, listener, options);
}
rawWindowRemoveEventListener.call(window.__WUJIE_RAW_WINDOW__ || window, type, listener, options);
};
}
事件分类:
| 事件类型 | 监听位置 | 示例 |
|---|---|---|
| 路由事件 | iframe | popstate, hashchange |
| DOM 事件 | 主应用 window | click, scroll, resize |
patch window 属性
// packages/wujie-core/src/iframe.ts
function patchWindowEffect(iframeWindow: Window): void {
Object.getOwnPropertyNames(iframeWindow).forEach((key) => {
// 特殊处理 getSelection
if (key === "getSelection") {
Object.defineProperty(iframeWindow, key, {
get: () => iframeWindow.document[key],
});
return;
}
// 代理到主应用 window
if (windowProxyProperties.includes(key)) {
processWindowProperty(key);
return;
}
// 正则匹配
windowRegWhiteList.some((reg) => {
if (reg.test(key) && key in iframeWindow.parent) {
return processWindowProperty(key);
}
return false;
});
});
// 处理 onEvent
const windowOnEvents = Object.getOwnPropertyNames(window)
.filter((p) => /^on/.test(p))
.filter((e) => !appWindowOnEvent.includes(e));
windowOnEvents.forEach((e) => {
Object.defineProperty(iframeWindow, e, {
get: () => window[e],
set: (handler) => {
window[e] = typeof handler === "function" ? handler.bind(iframeWindow) : handler;
},
});
});
// 运行插件钩子
execHooks(iframeWindow.__WUJIE.plugins, "windowPropertyOverride", iframeWindow);
}
脚本执行
// packages/wujie-core/src/iframe.ts
export function insertScriptToIframe(scriptResult, iframeWindow, rawElement?) {
const { src, module, content, crossorigin, async, attrs, callback, onload } = scriptResult;
const scriptElement = iframeWindow.document.createElement("script");
const { replace, plugins, proxyLocation } = iframeWindow.__WUJIE;
// 1. 通过 jsLoader 处理代码
const jsLoader = getJsLoader({ plugins, replace });
let code = jsLoader(content, src, getCurUrl(proxyLocation));
// 2. 内联脚本包装
if (content && !module) {
// 关键:将 window/self/global/location 替换为代理对象
code = `(function(window, self, global, location) {
${code}
}).bind(window.__WUJIE.proxy)(
window.__WUJIE.proxy,
window.__WUJIE.proxy,
window.__WUJIE.proxy,
window.__WUJIE.proxyLocation,
);`;
}
// 3. 设置脚本内容
scriptElement.textContent = code || "";
// 4. 插入执行
const container = rawDocumentQuerySelector.call(iframeWindow.document, "head");
container.appendChild(scriptElement);
// 5. 执行下一个脚本
const execNextScript = () => !async && container.appendChild(nextScriptElement);
}
核心技巧:通过 IIFE 包装,将全局变量替换为代理对象:
// 原始代码
window.foo = 'bar';
document.getElementById('app');
// 包装后
(function(window, self, global, location) {
window.foo = 'bar'; // 实际操作 proxy
document.getElementById('app'); // 实际操作 proxyDocument
}).bind(window.__WUJIE.proxy)(
window.__WUJIE.proxy,
window.__WUJIE.proxy,
window.__WUJIE.proxy,
window.__WUJIE.proxyLocation,
);
注入变量
// packages/wujie-core/src/iframe.ts
function patchIframeVariable(iframeWindow: Window, wujie: WuJie, appHostPath: string): void {
// 沙箱实例
iframeWindow.__WUJIE = wujie;
// 公共路径
iframeWindow.__WUJIE_PUBLIC_PATH__ = appHostPath + "/";
// 子应用接口
iframeWindow.$wujie = wujie.provide;
// 原始 window
iframeWindow.__WUJIE_RAW_WINDOW__ = iframeWindow;
}
子应用可通过 window.$wujie 访问:
// 子应用中
window.$wujie.bus.$emit('event', data); // 事件通信
window.$wujie.props; // 获取 props
window.$wujie.location; // 代理的 location
销毁清理
// packages/wujie-core/src/sandbox.ts
public async destroy() {
await this.unmount();
// 清理事件
this.bus.$clear();
// 清理代理
this.proxy = null;
this.proxyDocument = null;
this.proxyLocation = null;
// 清理 DOM
if (this.el) {
clearChild(this.el);
this.el = null;
}
// 清理 iframe
if (this.iframe) {
const iframeWindow = this.iframe.contentWindow;
// 移除所有事件监听
if (iframeWindow?.__WUJIE_EVENTLISTENER__) {
iframeWindow.__WUJIE_EVENTLISTENER__.forEach((o) => {
iframeWindow.removeEventListener(o.type, o.listener, o.options);
});
}
// 移除 iframe
this.iframe.parentNode?.removeChild(this.iframe);
this.iframe = null;
}
// 从缓存中删除
deleteWujieById(this.id);
}
小结
无界的 iframe 沙箱机制:
- 同域 iframe:避免跨域问题,保证 JS 隔离
- 阻止加载:创建空白 iframe,不加载主应用内容
- patch 劫持:history、事件、window 属性全面劫持
- IIFE 包装:脚本执行时替换全局变量为代理对象
- 事件分流:路由事件留在 iframe,DOM 事件代理到主应用
下一篇我们将分析 CSS 隔离的实现。
📦 源码版本:wujie v1.0.22
上一篇:架构总览
下一篇:CSS 隔离