微前端沙箱机制详解
前言
在微前端架构中,多个子应用共享同一个浏览器运行环境。如果不加隔离,子应用之间以及子应用与主应用之间很容易产生全局变量污染、样式冲突、事件监听泄漏等问题。沙箱(Sandbox) 正是为了解决这些隔离问题而诞生的核心机制。
本文将从沙箱的基本概念出发,逐步深入 qiankun 和 wujie 两大主流微前端框架的沙箱实现原理。
为什么需要沙箱?
先来看一个没有沙箱的场景:
// 子应用 A
window.globalConfig = { theme: 'dark' };
document.addEventListener('click', handleClickA);
// 子应用 B
window.globalConfig = { theme: 'light' }; // 覆盖了 A 的配置!
document.addEventListener('click', handleClickB);
当子应用 A 和 B 同时运行,或者 A 卸载后 B 加载时,就会出现:
| 问题类型 | 具体表现 |
|---|---|
| 全局变量污染 | window.globalConfig 被后加载的应用覆盖 |
| 事件监听泄漏 | 子应用卸载后,click 事件监听未清除 |
| 样式冲突 | 子应用 A 的 .btn { color: red } 影响子应用 B |
| 定时器泄漏 | setInterval 未清除,导致已卸载的应用代码仍在执行 |
沙箱的目标就是为每个子应用营造一个相对独立的运行环境,确保子应用之间互不干扰。
JS 沙箱的三种方案
快照沙箱(Snapshot Sandbox)
快照沙箱是最早期、最简单的方案。核心思想:在子应用挂载前拍摄 window 的快照,在卸载时恢复快照。
实现原理
代码实现
class SnapshotSandbox {
constructor() {
this.snapshot = {}; // window 快照
this.modifyMap = {}; // 子应用对 window 的修改记录
}
// 激活沙箱
active() {
// 1. 拍摄当前 window 快照
this.snapshot = {};
for (const key in window) {
this.snapshot[key] = window[key];
}
// 2. 恢复上次子应用的修改
Object.keys(this.modifyMap).forEach(key => {
window[key] = this.modifyMap[key];
});
}
// 失活沙箱
inactive() {
this.modifyMap = {};
for (const key in window) {
if (window[key] !== this.snapshot[key]) {
// 记录变更
this.modifyMap[key] = window[key];
// 还原 window
window[key] = this.snapshot[key];
}
}
}
}
使用示例
const sandbox = new SnapshotSandbox();
// 激活沙箱,子应用 A 运行
sandbox.active();
window.city = 'Beijing';
window.language = 'zh';
console.log(window.city); // 'Beijing'
// 切换到子应用 B,失活沙箱
sandbox.inactive();
console.log(window.city); // undefined(已还原)
// 再切回子应用 A
sandbox.active();
console.log(window.city); // 'Beijing'(从 modifyMap 恢复)
优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,兼容性好 | 遍历 window 性能开销大 |
不依赖 Proxy | 不支持多实例(同时运行多个子应用) |
| 支持 IE 等老浏览器 | 每次激活/失活都需要全量对比 |
代理沙箱——单实例(Legacy Proxy Sandbox)
利用 ES6 的 Proxy 拦截对 window 的读写操作,将修改记录到一个独立的变更池中。
实现原理
代码实现
class LegacyProxySandbox {
constructor() {
this.addedMap = new Map(); // 新增的属性
this.modifiedMap = new Map(); // 修改的属性(记录原始值)
this.currentMap = new Map(); // 当前所有变更
const self = this;
const fakeWindow = Object.create(null);
this.proxy = new Proxy(fakeWindow, {
set(target, key, value) {
if (!window.hasOwnProperty(key)) {
// 新增属性
self.addedMap.set(key, value);
} else if (!self.modifiedMap.has(key)) {
// 首次修改,记录原始值
self.modifiedMap.set(key, window[key]);
}
self.currentMap.set(key, value);
// 同步修改真实 window
window[key] = value;
return true;
},
get(target, key) {
return window[key];
}
});
}
// 激活:恢复子应用之前的修改
active() {
this.currentMap.forEach((value, key) => {
window[key] = value;
});
}
// 失活:还原 window
inactive() {
// 还原被修改的属性
this.modifiedMap.forEach((value, key) => {
window[key] = value;
});
// 删除新增的属性
this.addedMap.forEach((_, key) => {
delete window[key];
});
}
}
相比快照沙箱,代理沙箱不需要遍历整个 window,性能更好。但它仍然直接修改了真实的 window,所以依然不支持多实例。
代理沙箱——多实例(Proxy Sandbox)
这是 qiankun 目前默认使用的沙箱方案。核心改进:子应用的所有修改都记录在一个 fakeWindow 上,不污染真实 window。
实现原理
代码实现
class ProxySandbox {
constructor() {
this.isRunning = false;
const rawWindow = window;
// 创建一个 fakeWindow,拷贝部分不可配置属性
const fakeWindow = Object.create(null);
this.proxy = new Proxy(fakeWindow, {
set(target, key, value) {
if (!this.isRunning) return false;
// 所有修改都写入 fakeWindow,不影响真实 window
target[key] = value;
return true;
},
get(target, key) {
// 优先从 fakeWindow 获取
if (key in target) {
return target[key];
}
// 否则从真实 window 获取
const value = rawWindow[key];
// 如果是函数,需要绑定 this 到真实 window
if (typeof value === 'function' && !value.prototype) {
return value.bind(rawWindow);
}
return value;
},
has(target, key) {
return key in target || key in rawWindow;
}
});
}
active() {
this.isRunning = true;
}
inactive() {
this.isRunning = false;
}
}
使用示例
const sandboxA = new ProxySandbox();
const sandboxB = new ProxySandbox();
sandboxA.active();
sandboxB.active();
// 子应用 A 和 B 同时修改 "window"
sandboxA.proxy.city = 'Beijing';
sandboxB.proxy.city = 'Shanghai';
console.log(sandboxA.proxy.city); // 'Beijing'
console.log(sandboxB.proxy.city); // 'Shanghai'
console.log(window.city); // undefined(真实 window 未被污染)
三种 JS 沙箱对比
| 特性 | 快照沙箱 | 单实例代理沙箱 | 多实例代理沙箱 |
|---|---|---|---|
| 实现方式 | 遍历 window 快照 | Proxy + 直接修改 window | Proxy + fakeWindow |
| 多实例支持 | 不支持 | 不支持 | 支持 |
| 性能 | 较差(全量遍历) | 较好 | 最好 |
| 浏览器兼容 | IE9+ | 不支持 IE | 不支持 IE |
| 是否污染 window | 是(需还原) | 是(需还原) | 否 |
| qiankun 中的使用 | 降级方案 | 单实例场景 | 默认方案 |
qiankun 的沙箱实现
qiankun 是蚂蚁金服开源的微前端框架,基于 single-spa 封装,提供了开箱即用的沙箱能力。
整体架构
JS 沙箱
qiankun 会根据环境自动选择沙箱类型:
// qiankun 源码简化
function createSandbox(appName, useLooseSandbox) {
if (window.Proxy) {
// 支持 Proxy
return useLooseSandbox
? new LegacySandbox(appName) // 单实例
: new ProxySandbox(appName); // 多实例(默认)
}
// 不支持 Proxy,降级到快照沙箱
return new SnapshotSandbox(appName);
}
副作用收集与清理
qiankun 不仅隔离 window 属性,还会劫持常见的副作用 API,在子应用卸载时自动清理:
// qiankun 劫持的副作用 API
const rawSetInterval = window.setInterval;
const rawSetTimeout = window.setTimeout;
const rawAddEventListener = window.addEventListener;
// 重写 setInterval,记录定时器 ID
window.setInterval = (...args) => {
const intervalId = rawSetInterval(...args);
// 记录到沙箱的副作用列表
sandbox.collectEffect('interval', intervalId);
return intervalId;
};
// 子应用卸载时,统一清理
function unmountSandbox(sandbox) {
sandbox.effects.intervals.forEach(id => clearInterval(id));
sandbox.effects.timeouts.forEach(id => clearTimeout(id));
sandbox.effects.listeners.forEach(([event, handler]) => {
window.removeEventListener(event, handler);
});
}
qiankun 劫持的副作用包括:
setInterval/setTimeoutaddEventListener/removeEventListener- 动态创建的
<style>/<link>/<script>标签 MutationObserver
CSS 沙箱
qiankun 提供两种样式隔离方案:
Scoped CSS(默认)
通过给子应用的所有样式规则添加属性选择器前缀实现隔离:
// qiankun 配置
registerMicroApps([{
name: 'app-a',
sandbox: { experimentalStyleIsolation: true }
}]);
效果:
/* 子应用原始样式 */
.btn { color: red; }
h1 { font-size: 24px; }
/* 处理后 */
div[data-qiankun="app-a"] .btn { color: red; }
div[data-qiankun="app-a"] h1 { font-size: 24px; }
qiankun 实现的核心逻辑:
function scopedCSS(styleNode, appName) {
const prefix = `div[data-qiankun="${appName}"]`;
const sheet = styleNode.sheet;
for (let i = 0; i < sheet.cssRules.length; i++) {
const rule = sheet.cssRules[i];
if (rule.type === CSSRule.STYLE_RULE) {
// 给选择器添加前缀
const newSelector = rule.selectorText
.split(',')
.map(s => `${prefix} ${s.trim()}`)
.join(', ');
// 替换规则
const newRule = `${newSelector} { ${rule.style.cssText} }`;
sheet.deleteRule(i);
sheet.insertRule(newRule, i);
}
}
}
局限性:无法隔离通过 document.body.style 等方式设置的内联样式,也无法阻止子应用影响 body、html 等全局元素。
Shadow DOM
使用浏览器原生的 Shadow DOM 实现强隔离:
registerMicroApps([{
name: 'app-a',
sandbox: { strictStyleIsolation: true }
}]);
原理:
function createShadowDOM(container, appContent) {
// 创建 Shadow Root
const shadow = container.attachShadow({ mode: 'open' });
// 子应用内容放入 Shadow DOM 中
shadow.innerHTML = appContent;
return shadow;
}
优点:样式完全隔离,互不影响。
缺点:
- 一些组件库(如 Ant Design)的弹窗会挂载到
document.body,无法被 Shadow DOM 包裹 React事件代理可能失效(React 将事件委托到document或 root 上)- 全局样式(字体、主题变量)无法穿透 Shadow DOM
qiankun 沙箱完整生命周期
wujie 的沙箱实现
wujie(无界) 是腾讯开源的微前端框架,采用了与 qiankun 完全不同的技术方案——WebComponent + iframe 的组合。
核心设计思想
wujie 的核心理念是:用 iframe 做 JS 隔离,用 WebComponent(Shadow DOM)做 DOM 隔离,取两者的长处。
为什么选择 iframe?
iframe 是浏览器原生提供的最强隔离方案:
// 每个 iframe 都有独立的:
iframe.contentWindow // 独立的 window 对象
iframe.contentDocument // 独立的 document 对象
iframe.contentWindow.location // 独立的 location
iframe.contentWindow.history // 独立的 history
| 能力 | Proxy 沙箱 | iframe 沙箱 |
|---|---|---|
| window 隔离 | 部分(需要大量 Proxy 拦截) | 完全隔离 |
| 原型链隔离 | 不隔离 | 完全隔离 |
| location / history | 需要额外处理 | 天然独立 |
| 定时器隔离 | 需要手动劫持收集 | 天然隔离 |
| 全局事件隔离 | 需要手动劫持收集 | 天然隔离 |
但传统的 iframe 有一些明显的缺陷:
| 缺陷 | 说明 |
|---|---|
| DOM 无法共享 | iframe 内的 DOM 和主应用完全割裂,弹窗不能突破 iframe 边界 |
| 路由状态不同步 | iframe 的 URL 变化不反映在浏览器地址栏 |
| 白屏问题 | iframe 每次创建都需要重新加载资源 |
| 通信复杂 | 只能通过 postMessage 进行跨域通信 |
wujie 的巧妙之处正是解决了这些缺陷:只用 iframe 的 JS 执行环境,把 DOM 渲染到主应用的 Shadow DOM 中。
iframe + WebComponent 的协作机制
wujie 的核心工作原理可以概括为以下步骤:
第一步:创建隐藏的 iframe
function createIframe(url) {
const iframe = document.createElement('iframe');
// 设置 src 为子应用的同域地址(或 about:blank)
iframe.setAttribute('src', url);
iframe.style.display = 'none'; // 隐藏 iframe
document.body.appendChild(iframe);
return iframe;
}
第二步:创建 WebComponent 容器
class WujieApp extends HTMLElement {
connectedCallback() {
// 创建 Shadow DOM
this.shadow = this.attachShadow({ mode: 'open' });
}
}
customElements.define('wujie-app', WujieApp);
主应用中使用:
<!-- 子应用渲染容器 -->
<wujie-app id="sub-app"></wujie-app>
第三步:代理 iframe 的 document 到 Shadow DOM
这是 wujie 最核心的一步——子应用的 JS 在 iframe 中执行,但所有 DOM 操作被代理到主应用的 Shadow DOM 中:
function patchIframeDocument(iframeWindow, shadowRoot) {
const rawDocument = iframeWindow.document;
// 代理 document.querySelector
Object.defineProperty(iframeWindow.document, 'querySelector', {
get() {
return function(selector) {
// 从 Shadow DOM 中查找,而不是 iframe 的 document
return shadowRoot.querySelector(selector);
};
}
});
// 代理 document.createElement
// 创建的元素最终会被插入到 Shadow DOM
Object.defineProperty(iframeWindow.document, 'body', {
get() {
return shadowRoot.body; // 指向 Shadow DOM 中的 body
}
});
// 代理 document.head
Object.defineProperty(iframeWindow.document, 'head', {
get() {
return shadowRoot.head;
}
});
}
第四步:在 iframe 中执行子应用的 JS
function execScriptInIframe(iframeWindow, scriptText) {
// 通过 iframe 的 window 来执行脚本
// 脚本中访问的 window 是 iframe 的 window(天然隔离)
// 脚本中的 DOM 操作被代理到 Shadow DOM
const scriptElement = iframeWindow.document.createElement('script');
scriptElement.textContent = scriptText;
iframeWindow.document.head.appendChild(scriptElement);
}
wujie 的路由同步
wujie 会将 iframe 的路由变化同步到主应用的 URL 上:
function syncUrlToWindow(iframeWindow, appName) {
// 监听 iframe 的 popstate 事件
iframeWindow.addEventListener('popstate', () => {
const currentPath = iframeWindow.location.pathname;
// 将子应用路由同步到主应用 URL
const mainUrl = new URL(window.location.href);
mainUrl.searchParams.set(appName, currentPath);
window.history.replaceState(null, '', mainUrl.toString());
});
}
wujie 的预加载与保活
wujie 还提供了两个重要特性来优化体验:
预加载(Preload)
import { preloadApp } from 'wujie';
// 在空闲时预加载子应用资源
preloadApp({
name: 'sub-app',
url: 'https://sub.example.com',
exec: true // 预执行 JS
});
预加载会提前创建 iframe 和 WebComponent 容器,用户切换时几乎无延迟。
保活模式(Alive)
import { startApp } from 'wujie';
startApp({
name: 'sub-app',
url: 'https://sub.example.com',
alive: true // 开启保活模式
});
保活模式下,子应用切走时 iframe 和 Shadow DOM 都不销毁,只是从 DOM 树中移除。再次切回时直接恢复,无需重新加载。
qiankun vs wujie 全面对比
| 对比维度 | qiankun | wujie |
|---|---|---|
| JS 隔离方案 | Proxy 代理 fakeWindow | iframe 天然隔离 |
| CSS 隔离方案 | Scoped CSS / Shadow DOM | WebComponent Shadow DOM |
| 隔离完整性 | 一般(需要大量 Patch) | 高(浏览器原生隔离) |
| 多实例支持 | 支持(ProxySandbox) | 天然支持 |
| 子应用保活 | 不支持(需重新渲染) | 支持 |
| 预加载 | 支持(资源预请求) | 支持(可预执行) |
| 子应用通信 | props 传递 / GlobalState | props / EventBus / window.parent |
| 路由同步 | 基于 single-spa 路由劫持 | iframe 路由 + URL 同步 |
| 弹窗问题 | Shadow DOM 下弹窗可能逃逸到 body | 弹窗在 Shadow DOM 中正常渲染 |
| 接入成本 | 子应用需改造(导出生命周期) | 较低(无需改造生命周期) |
| 社区生态 | 成熟(Star 15k+) | 较新(持续发展中) |
| 适用场景 | 通用微前端方案 | 对隔离要求高、需要保活的场景 |
与 Vite 的兼容性(主应用/子应用)
先说结论
- Vite 主应用(基座):可以使用 qiankun,也可以使用 wujie。它们本质都是运行时框架/库,和主应用使用 Webpack 还是 Vite 没有强绑定。
- 容易产生误会的是:很多人遇到的“不能用”,其实是 Vite 子应用接入 qiankun 时踩了坑(尤其是开发环境);而 wujie 因为 iframe 天然隔离,通常对 Vite 子应用更友好。
如果你遇到“不能用”,通常是这些原因
1)qiankun + Vite 子应用(尤其 dev 环境)
qiankun 经典的加载链路是:拉取子应用 HTML → 解析脚本与样式 → 在沙箱里执行脚本。这套机制对「非模块脚本(UMD/IIFE)+ 全局暴露生命周期」的子应用最友好。
但 Vite 的开发环境默认走 原生 ESM(type="module"、大量 import、@vite/client、HMR 模块图)。如果子应用脚本被当作普通脚本执行,通常会出现:
Cannot use import statement outside a module- 子应用静态资源路径(
base/publicPath)不正确导致 404
本质原因:一个偏“脚本注入/执行”的运行时加载模型,遇到了一个偏“浏览器原生模块加载”的开发产物。
常见解决思路(只给方向,不展开到具体框架配置):
- 让 Vite 子应用产物更符合 qiankun 的消费方式(例如构建为可全局访问的生命周期入口,或使用社区适配插件)
- 正确处理子应用资源的
base/publicPath(避免相对路径错乱) - dev 场景下处理跨域拉取资源(CORS / origin 等)
2)wujie(iframe)被安全策略拦截
wujie 依赖 iframe。如果子应用(或其 CDN / 网关 / Nginx)设置了以下安全策略,浏览器会直接禁止被嵌入:
X-Frame-Options: DENY/SAMEORIGINContent-Security-Policy: frame-ancestors 'none'(或未允许主应用域名)
本质原因:不是 Vite 或 wujie 的问题,而是“页面不允许被 iframe 嵌套”。
手写一个迷你微前端沙箱
把核心原理串起来,实现一个最简化版本的沙箱系统:
class MiniSandbox {
constructor(name) {
this.name = name;
this.active = false;
this.fakeWindow = Object.create(null);
this.effects = {
intervals: [],
timeouts: [],
listeners: []
};
this.proxy = this._createProxy();
}
_createProxy() {
const self = this;
const rawWindow = window;
return new Proxy(this.fakeWindow, {
get(target, key) {
// 特殊属性直接返回 proxy 自身
if (key === 'window' || key === 'self' || key === 'globalThis') {
return self.proxy;
}
// 劫持定时器
if (key === 'setInterval') {
return (...args) => {
const id = rawWindow.setInterval(...args);
self.effects.intervals.push(id);
return id;
};
}
if (key === 'setTimeout') {
return (...args) => {
const id = rawWindow.setTimeout(...args);
self.effects.timeouts.push(id);
return id;
};
}
// 劫持事件监听
if (key === 'addEventListener') {
return (event, handler, ...rest) => {
self.effects.listeners.push([event, handler]);
return rawWindow.addEventListener(event, handler, ...rest);
};
}
// 优先从 fakeWindow 获取
if (key in target) return target[key];
// 兜底到真实 window
const value = rawWindow[key];
if (typeof value === 'function' && !value.prototype) {
return value.bind(rawWindow);
}
return value;
},
set(target, key, value) {
if (!self.active) return false;
target[key] = value;
return true;
},
has(target, key) {
return key in target || key in rawWindow;
}
});
}
// 在沙箱环境下执行代码
execScript(code) {
this.active = true;
// 利用 with 语句将作用域绑定到 proxy
const execFunc = new Function('window', `
with(window) {
${code}
}
`);
execFunc.call(this.proxy, this.proxy);
}
// 销毁沙箱
destroy() {
this.active = false;
// 清理所有副作用
this.effects.intervals.forEach(id => clearInterval(id));
this.effects.timeouts.forEach(id => clearTimeout(id));
this.effects.listeners.forEach(([event, handler]) => {
window.removeEventListener(event, handler);
});
console.log(`[${this.name}] 沙箱已销毁,所有副作用已清理`);
}
}
// 使用示例
const sandbox1 = new MiniSandbox('子应用A');
const sandbox2 = new MiniSandbox('子应用B');
sandbox1.execScript(`
window.appName = '子应用A';
window.setInterval(() => console.log(window.appName), 3000);
`);
sandbox2.execScript(`
window.appName = '子应用B';
console.log(window.appName); // '子应用B'
`);
console.log(window.appName); // undefined(真实 window 未被污染)
// 卸载子应用
sandbox1.destroy(); // 定时器被自动清理
面试高频问答
Q1:微前端沙箱是什么?为什么需要沙箱?
答:沙箱是一种隔离机制,用于保证微前端架构中各子应用之间的运行环境互不干扰。需要沙箱是因为多个子应用共享同一个浏览器上下文(window、DOM、CSS),如果不隔离会导致全局变量污染、样式冲突、事件监听泄漏、定时器泄漏等问题。
Q2:qiankun 的三种沙箱有什么区别?
答:
- 快照沙箱(SnapshotSandbox):激活时遍历
window保存快照,失活时恢复。兼容 IE,但性能差,不支持多实例。 - 单实例代理沙箱(LegacySandbox):用
Proxy拦截window操作,只记录变更而非全量快照,性能更好,但仍直接修改window,不支持多实例。 - 多实例代理沙箱(ProxySandbox):qiankun 默认方案。所有修改写入
fakeWindow,不污染真实window,天然支持多实例。
Q3:qiankun 的 CSS 隔离方案有哪些?各有什么优缺点?
答:
- Scoped CSS(
experimentalStyleIsolation):给子应用样式加上div[data-qiankun="appName"]前缀。优点是兼容性好;缺点是无法隔离body/html级别的样式,内联样式也无法处理。 - Shadow DOM(
strictStyleIsolation):利用浏览器原生的 Shadow DOM 完全隔离样式。优点是隔离彻底;缺点是弹窗类组件可能逃逸到document.body,React 事件委托可能失效。
Q4:wujie 为什么选择 iframe + WebComponent 的方案?
答:iframe 提供了浏览器最完整的 JS 隔离能力(独立的 window、document、location、history、原型链等),WebComponent 的 Shadow DOM 提供了天然的 CSS 隔离。wujie 结合两者的优势——用 iframe 做 JS 执行环境,用 Shadow DOM 做 DOM 渲染容器,通过 Proxy 将 iframe 中的 DOM 操作代理到 Shadow DOM,巧妙地解决了传统 iframe 的白屏、DOM 割裂、路由不同步等问题。
Q5:wujie 如何将 iframe 中的 DOM 操作代理到 Shadow DOM?
答:wujie 通过 Object.defineProperty 或 Proxy 重写了 iframe 中 document 的关键属性和方法,如 document.body、document.head、document.querySelector、document.getElementById 等,将它们指向主应用中 Shadow DOM 的对应节点。这样子应用的 JS 在 iframe 中执行时,所有 DOM 查询和操作实际上都作用于 Shadow DOM。
Q6:qiankun 和 wujie 怎么选?
答:
- 选 qiankun:项目已经基于 single-spa 体系、团队对 qiankun 生态熟悉、不需要子应用保活功能、IE 兼容需求(可降级到快照沙箱)。
- 选 wujie:对隔离要求极高(原型链级别)、需要子应用保活以提升切换性能、子应用接入改造成本要求低(不需要导出生命周期钩子)、不需要兼容 IE。
Q7:Proxy 沙箱有哪些边界情况需要处理?
答:
- 原生方法的
this绑定:如window.addEventListener、window.fetch等方法必须绑定到真实window上调用,否则会报Illegal invocation错误。 Symbol类型的属性:需要正确处理Symbol.toStringTag、Symbol.toPrimitive等。- 不可配置的属性:
window上的undefined、NaN、Infinity等不可配置属性需要从真实window返回。 eval和with:eval默认在全局作用域执行,需要特殊处理才能在沙箱作用域中运行。document.createElement等 DOM API:这些操作需要额外劫持,否则创建的元素会挂载到真实文档。
Q8:除了 qiankun 和 wujie,还有哪些微前端沙箱方案?
答:
- micro-app(京东):基于 WebComponent,使用 Proxy 做 JS 沙箱,类似 qiankun 但无需 single-spa,接入更简单。
- EMP(欢聚时代):基于 Webpack 5 Module Federation,运行时模块共享,严格来说不算传统沙箱,而是通过模块隔离实现。
- Garfish(字节):和 qiankun 类似使用 Proxy 沙箱,支持 VM 沙箱(快照 + Proxy 混合),隔离能力更强。
- iframe 方案:直接使用 iframe 嵌套,隔离最强但体验最差(白屏、割裂)。wujie 本质上是优化后的 iframe 方案。
Q9:qiankun / wujie 能在 Vite 主应用中使用吗?如果遇到“不能用”通常是什么原因?
答:可以。Vite 是否能作为主应用,通常不是问题。更常见的“不能用”来自两类原因:
- qiankun + Vite 子应用(dev):Vite dev 默认是原生 ESM + HMR,和 qiankun 更偏“脚本注入执行”的加载模型不完全匹配,容易出现
import相关报错或资源路径 404,需要做产物/配置适配。 - wujie(iframe):如果子应用被配置了
X-Frame-Options或 CSPframe-ancestors等策略,浏览器会禁止 iframe 嵌入,表现为白屏或直接报错。