跳到主要内容

微前端沙箱机制详解

前言

在微前端架构中,多个子应用共享同一个浏览器运行环境。如果不加隔离,子应用之间以及子应用与主应用之间很容易产生全局变量污染样式冲突事件监听泄漏等问题。沙箱(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 + 直接修改 windowProxy + 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 / setTimeout
  • addEventListener / 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 等方式设置的内联样式,也无法阻止子应用影响 bodyhtml 等全局元素。

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 全面对比

对比维度qiankunwujie
JS 隔离方案Proxy 代理 fakeWindowiframe 天然隔离
CSS 隔离方案Scoped CSS / Shadow DOMWebComponent Shadow DOM
隔离完整性一般(需要大量 Patch)(浏览器原生隔离)
多实例支持支持(ProxySandbox)天然支持
子应用保活不支持(需重新渲染)支持
预加载支持(资源预请求)支持(可预执行)
子应用通信props 传递 / GlobalStateprops / 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 的开发环境默认走 原生 ESMtype="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 / SAMEORIGIN
  • Content-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 CSSexperimentalStyleIsolation):给子应用样式加上 div[data-qiankun="appName"] 前缀。优点是兼容性好;缺点是无法隔离 body/html 级别的样式,内联样式也无法处理。
  • Shadow DOMstrictStyleIsolation):利用浏览器原生的 Shadow DOM 完全隔离样式。优点是隔离彻底;缺点是弹窗类组件可能逃逸到 document.body,React 事件委托可能失效。

Q4:wujie 为什么选择 iframe + WebComponent 的方案?

:iframe 提供了浏览器最完整的 JS 隔离能力(独立的 windowdocumentlocationhistory、原型链等),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.definePropertyProxy 重写了 iframe 中 document 的关键属性和方法,如 document.bodydocument.headdocument.querySelectordocument.getElementById 等,将它们指向主应用中 Shadow DOM 的对应节点。这样子应用的 JS 在 iframe 中执行时,所有 DOM 查询和操作实际上都作用于 Shadow DOM。

Q6:qiankun 和 wujie 怎么选?

  • 选 qiankun:项目已经基于 single-spa 体系、团队对 qiankun 生态熟悉、不需要子应用保活功能、IE 兼容需求(可降级到快照沙箱)。
  • 选 wujie:对隔离要求极高(原型链级别)、需要子应用保活以提升切换性能、子应用接入改造成本要求低(不需要导出生命周期钩子)、不需要兼容 IE。

Q7:Proxy 沙箱有哪些边界情况需要处理?

  • 原生方法的 this 绑定:如 window.addEventListenerwindow.fetch 等方法必须绑定到真实 window 上调用,否则会报 Illegal invocation 错误。
  • Symbol 类型的属性:需要正确处理 Symbol.toStringTagSymbol.toPrimitive 等。
  • 不可配置的属性window 上的 undefinedNaNInfinity 等不可配置属性需要从真实 window 返回。
  • evalwitheval 默认在全局作用域执行,需要特殊处理才能在沙箱作用域中运行。
  • 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 或 CSP frame-ancestors 等策略,浏览器会禁止 iframe 嵌入,表现为白屏或直接报错。