微前端沙箱隔离技术
在微前端架构中,多个子应用共享同一运行时环境,容易因全局变量污染和样式冲突引发不可预见的错误。例如:
- 样式污染:某子应用定义的
.ant-btn
样式意外覆盖主应用按钮样式 - 变量冲突:子应用 A 通过
window.token = 'A'
篡改主应用认证令牌 - 原型污染:子应用重写
Array.prototype.includes
引发其他应用异常
而沙箱隔离技术通过为每个子应用创建独立的环境,解决上述问题。下面以 qiankun 为例,探讨其沙箱隔离机制的技术实现。
一、样式隔离
1. 动态样式隔离(默认)
qiankun 默认开启,可以确保单实例场景子应用间的样式隔离,但无法确保主应用跟子应用、或多实例场景的子应用样式隔离,其原理是通过监听子应用的生命周期,动态注入或移除其样式表。当子应用挂载时,样式被插入页面;卸载时,样式自动移除。
适用场景:单页面切换子应用的简单场景
技术实现:
// 样式注入核心逻辑
const mountStyles = (app, styles) => {
const styleStore = new Map();
app.mount(() => {
styles.forEach(style => {
const styleElement = document.createElement('style');
styleElement.textContent = style;
document.head.appendChild(styleElement);
styleStore.set(style, styleElement);
});
});
app.unmount(() => {
styleStore.forEach(element => element.remove());
});
};
深度剖析:
- 运行时开销:每次挂载/卸载需操作 DOM,存在重排风险(可通过
requestAnimationFrame
优化) - 典型缺陷案例:主应用使用
!important
的全局样式仍可能穿透子应用样式
2. 影子 DOM 沙箱(Shadow DOM)
手动开启后,qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
开启方式:
start({ sandbox: { strictStyleIsolation: true } });
高级配置方案:
start({
sandbox: {
strictStyleIsolation: true,
// 允许主应用样式穿透
styleSheetTransform: (cssText, appName) =>
`:host { ${cssText} }` // 将全局样式限定在 Shadow DOM 内部
}
});
实现原理:
- 为子应用容器创建 Shadow Root。
- 子应用的 DOM 结构和样式仅作用于 Shadow Tree 内部。
- 示例:若主应用和子应用均有
.App
类名,Shadow DOM 可确保两者互不干扰。
局限性:
- 第三方 UI 库(如 Element UI)的全局样式可能失效。
- 子应用无法直接操作主文档 DOM(如全局弹窗)。
3. 作用域沙箱(Scoped CSS)
手动开启后,qiankun 会改写子应用所添加的样式,为子应用的样式规则添加唯一前缀选择器(如 div[data-qiankun="appName"]
),限制其作用范围。例如,.app-main
会被重写为 div[data-qiankun="appName"] .app-main
。
开启方式:
start({
sandbox: { experimentalStyleIsolation: true }
});
实现原理:
- 解析子应用的
<style>
和<link>
样式资源。 - 为所有选择器添加父级属性选择器前缀。
- 将外部样式表转换为内联样式以支持重写。
优缺点:
- 优点:兼容性强,无需修改子应用代码。
- 缺点:解析和重写样式规则会增加运行时开销,且无法处理动态插入的样式。
二、JS 隔离
1. SnapshotSandbox 快照沙箱
基于 diff 实现的沙箱,用于兼容不支持 Proxy 的低版本浏览器。
实现原理:
把主应用的 window
对象做浅拷贝,将 window
的键值对存成一个 Hash Map
。
之后无论子应用对 window
做任何改动,在恢复环境时,把这个 Hash Map
又应用回 window
即可。

激活时:保存当前 window
对象的快照,并恢复子应用上次卸载前的状态。
卸载时:对比当前 window
与快照的差异,记录修改并还原初始状态。
class SnapshotSandbox {
active() {
this.windowSnapshot = {...window};
Object.keys(this.modifyPropsMap).forEach(p => window[p] = this.modifyPropsMap[p]);
}
inactive() {
Object.keys(window).forEach(p => {
if (window[p] !== this.windowSnapshot[p]) {
this.modifyPropsMap[p] = window[p];
window[p] = this.windowSnapshot[p];
}
});
}
}
2. LegacySandbox 单例代理沙箱
上面的 SnapshotSandbox
有一个问题:每次子应用 unmount 时都要对每个属性值做一次 Diff,类似这样:
for (const prop in window) {
if (window[prop] !== this.windowSnapshot[prop]) {
// 记录子应用的变更
this.modifyPropsMap[prop] = window[prop];
// 恢复主应用的环境
window[prop] = this.windowSnapshot[prop];
}
}
如果有 1000 个属性那就得对比 1000 次。
LegacySandbox
的想法就是通过监听对 window
的修改来直接记录 Diff 内容,因为只要对 window
属性进行设置,那么就会有两种情况:
- 如果是新增属性,则存到
addedMap
里 - 如果是更新属性,则把原来的键值存到
prevMap
,把新的键值存到newMap
通过 addedMap
, prevMap
和 newMap
这三个变量就能反推出子应用以及原来环境的变化,qiankun 也能以此作为恢复环境的依据。
-(1).3uuzlsw69i.webp)
3. ProxySandbox 多例代理沙箱
前面两种沙箱都是 单例模式 下使用的沙箱。即一个页面中只能同时展示一个子应用,无论是 set
还是 get
都是直接操作 window
对象。
在这种单例模式下,子应用修改全局变量时会在原来的 window
上做修改,如果在同个路由页面下展示多个子应用,则会有环境变量污染的问题。
为了避免真实的 window
被污染,qiankun 实现了 ProxySandbox
。它的想法是:
- 把当前
window
的一些原生属性(document
、location
等)拷贝出来,单独放在一个对象上,这个对象也称为fakeWindow
- 之后对每个子应用分配一个
fakeWindow
- 当子应用修改全局变量时:
- 如果是原生属性,则修改全局的
window
- 如果不是原生属性,则修改
fakeWindow
里的内容
- 如果是原生属性,则修改全局的
- 子应用获取全局变量时:
- 如果是原生属性,则从
window
里拿 - 如果不是原生属性,则优先从
fakeWindow
里获取
- 如果是原生属性,则从
这样一来连恢复环境都不需要了,因为每个子应用都有自己一个环境,当在 active
时就给这个子应用分配一个 fakeWindow
,当 inactive
时就把这个 fakeWindow
存起来,以便之后再利用。
-(1).3nrrqda0tz.webp)
类似这样:
class ProxySandbox {
constructor() {
const fakeWindow = {};
this.proxy = new Proxy(fakeWindow, {
get: (target, p) => target[p] || window[p],
set: (target, p, value) => (target[p] = value)
});
}
}
优势:
- 完全隔离子应用的全局变量,支持多实例并行。
- 高性能,无需遍历
window
对象。