一、整体架构
qiankun 提供了三种 JS 沙箱,Proxy 相关的有两种:
| 类型 | 类名 | 模式 | 隔离强度 | 说明 |
|---|---|---|---|---|
SandBoxType.Proxy | ProxySandbox | strict(默认) | 强隔离 | 每个子应用一个独立的 fakeWindow,互不干扰 |
SandBoxType.LegacyProxy | LegacySandbox | loose(宽松) | 弱隔离 | 直接操作真实 window,通过记录/恢复实现隔离 |
沙箱的选择逻辑(src/sandbox/index.ts):
if (window.Proxy) {
sandbox = useLooseSandbox
? new LegacySandbox(appName, globalContext)
: new ProxySandbox(appName, globalContext, elementGetter);
} else {
sandbox = new SnapshotSandbox(appName); // 降级方案,非 Proxy
}
二、ProxySandbox(严格模式,默认)
2.1 核心思想
每个子应用拥有一个独立的
fakeWindow对象作为 Proxy 的 target,所有全局变量的读写都代理到这个fakeWindow上,从而实现多实例共存、互不污染。
2.2 fakeWindow 的创建
function createFakeWindow(globalContext: Window) {
const propertiesWithGetter = new Map<PropertyKey, boolean>();
const fakeWindow = {} as FakeWindow;
// 将 globalContext 上所有 不可配置(non-configurable) 的属性复制到 fakeWindow
Object.getOwnPropertyNames(globalContext)
.filter((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
return !descriptor?.configurable;
})
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
if (descriptor) {
// 特殊处理: top/parent/self/window 改为 configurable
// 这样 get trap 才能返回 proxy 自身而不违反 Proxy 不变量约束
if (p === 'top' || p === 'parent' || p === 'self' || p === 'window') {
descriptor.configurable = true;
if (!hasGetter) descriptor.writable = true;
}
rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
}
});
return { fakeWindow, propertiesWithGetter };
}
为什么要复制不可配置的属性?
根据 ECMAScript 规范,Proxy 的 getOwnPropertyDescriptor trap 不能将一个不存在于 target 上的属性报告为 non-configurable。因此必须把 window 上 non-configurable 的属性(如 window、top、NaN、undefined 等)同步到 fakeWindow。
2.3 Proxy Handler 详解
set trap — 写入拦截
proxy.xxx = value
执行逻辑:
沙箱运行中?
├── 是 → 目标属性在 globalContext 上存在但 fakeWindow 没有?
│ ├── 是 → 检查是否 writable/有 setter → 用 defineProperty 写入 fakeWindow
│ └── 否 → 直接写入 fakeWindow: target[p] = value
│
│ 属性在白名单中?(System, __cjsWrapper 等)
│ ├── 是 → 同时写入真实 globalContext(并记录原始值以便恢复)
│ └── 否 → 不影响真实 window
│
│ 记录到 updatedValueSet,更新 latestSetProp
│
└── 否 → 忽略写入,返回 true(避免 strict mode 抛 TypeError)
关键点:写操作默认只影响 fakeWindow,不污染真实 window,这是多实例隔离的核心。
get trap — 读取拦截
proxy.xxx
执行逻辑:
p === Symbol.unscopables → 返回 unscopables(配合 with 语句)
p === 'window' / 'self' / 'globalThis' → 返回 proxy 自身(防逃逸)
p === 'top' / 'parent' → 非 iframe 环境返回 proxy,iframe 中返回真实值
p === 'hasOwnProperty' → 返回自定义实现(同时检查 fakeWindow 和 globalContext)
p === 'document' → 返回真实 document
p === 'eval' → 返回真实 eval
其他属性:
propertiesWithGetter 中有 → 从 globalContext 读取
fakeWindow 上有 → 从 fakeWindow 读取
都没有 → 从 globalContext 读取
对函数类型的值调用 getTargetValue 做绑定处理
防逃逸设计:window.window、window.self、window.globalThis 都返回 proxy 本身,防止子应用通过这些属性拿到真实的 window 对象。
has trap — in 操作符拦截
has(target, p) {
return p in unscopables || p in target || p in globalContext;
}
配合 with(proxy) { ... } 语句使用。unscopables 包含 ES2015 全局变量(Array、Object、Promise 等),让这些变量能通过 in 检查,从而在 with 块中正确被访问。
getOwnPropertyDescriptor trap
属性在 fakeWindow 上 → 返回 fakeWindow 的描述符
属性在 globalContext 上 → 返回 globalContext 的描述符(强制 configurable: true)
都没有 → 返回 undefined
同时记录描述符来源到 descriptorTargetMap,供 defineProperty trap 使用。
defineProperty trap
defineProperty(target, p, attributes) {
const from = descriptorTargetMap.get(p);
switch (from) {
case 'globalContext':
return Reflect.defineProperty(globalContext, p, attributes);
default:
return Reflect.defineProperty(target, p, attributes);
}
}
如果属性描述符来源于 globalContext(通过 getOwnPropertyDescriptor 查询过),则 defineProperty 也写回 globalContext,否则写到 fakeWindow。
deleteProperty trap
只能删除 fakeWindow 上的属性,不影响真实 window。
ownKeys trap
ownKeys(target) {
return uniq(Reflect.ownKeys(globalContext).concat(Reflect.ownKeys(target)));
}
合并 globalContext 和 fakeWindow 的 keys 并去重。
getPrototypeOf trap
getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
}
确保 proxy instanceof Window 返回 true。
2.4 函数绑定处理 — getTargetValue
当从沙箱中获取到函数值时,需要特殊处理:
function getTargetValue(target: any, value: any): any {
// 仅绑定: 可调用 && 非已绑定函数 && 非构造函数
// 如 window.console、window.atob、window.fetch 这类原生方法
if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) {
const boundValue = Function.prototype.bind.call(value, target);
// 复制枚举属性(如 moment 函数上的静态方法)
// 复制 prototype
// 修正 toString 输出
return boundValue;
}
return value;
}
为什么需要 bind? 很多浏览器原生 API(如 fetch、console.log)内部有 this 检查,如果 this 不是 window 会抛出 Illegal invocation。bind 到 target(根据情况是 globalContext 或 nativeGlobal)保证调用正常。
2.5 unscopables 与 with 语句
qiankun 使用 import-html-entry 执行子应用脚本时,会将代码包裹在:
(function(window, self, globalThis) {
with(window) {
// 子应用代码
}
}).call(proxy, proxy, proxy, proxy)
Symbol.unscopables 返回的对象告诉 with 语句哪些属性不应该从 proxy 上查找,而是直接走作用域链。这里把 ES 内置全局变量(Array、Object、Math 等)放入 unscopables,避免在 proxy 上查找这些不可变的全局对象带来的性能开销。
2.6 白名单机制
const globalVariableWhiteList = ['System', '__cjsWrapper', ...variableWhiteListInDev];
白名单中的变量在 set 时同时写入真实 window,并在 inactive() 时恢复原始值。这是为了兼容 System.js 等工具的特殊需求。
2.7 currentRunningApp 注册机制
private registerRunningApp(name, proxy, elementGetter) {
if (this.sandboxRunning) {
setCurrentRunningApp({ name, window: proxy, elementGetter });
nextTask(() => setCurrentRunningApp(null)); // 下一个微任务清除
}
}
每次 get/set/delete 时注册当前运行的子应用,下一个微任务清除。这让 document.createElement 等被劫持的 DOM API 能知道当前操作来自哪个子应用,从而将动态创建的 script/style 归属到正确的沙箱。
三、LegacySandbox(宽松模式)
3.1 核心思想
直接在真实
window上读写,但通过三个 Map 记录所有变更,在inactive()时恢复原始状态,active()时重新应用变更。
3.2 三个核心 Map
// 沙箱期间新增的全局变量
private addedPropsMapInSandbox = new Map<PropertyKey, any>();
// 沙箱期间修改的全局变量的原始值
private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();
// 持续记录所有更新(新增+修改)的全局变量的当前值
private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();
3.3 set trap
const setTrap = (p, value, originalValue, sync2Window = true) => {
if (this.sandboxRunning) {
if (!rawWindow.hasOwnProperty(p)) {
// 新增属性 → 记录到 addedPropsMapInSandbox
addedPropsMapInSandbox.set(p, value);
} else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
// 修改已有属性(首次修改)→ 记录原始值
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
}
// 记录当前值
currentUpdatedPropsValueMap.set(p, value);
// 同步写入真实 window
if (sync2Window) rawWindow[p] = value;
return true;
}
return true;
};
3.4 get trap
get(_, p) {
if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
return proxy; // 防逃逸
}
const value = rawWindow[p];
return getTargetValue(rawWindow, value); // 直接从真实 window 读取
}
3.5 生命周期
active(激活):
active() {
if (!this.sandboxRunning) {
// 重新将 currentUpdatedPropsValueMap 的值设置到 window
this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
}
this.sandboxRunning = true;
}
inactive(卸载):
inactive() {
// 1. 恢复被修改的属性为原始值
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v));
// 2. 删除新增的属性
this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true));
this.sandboxRunning = false;
}
四、ProxySandbox vs LegacySandbox 对比
| 维度 | ProxySandbox (strict) | LegacySandbox (loose) |
|---|---|---|
| 全局变量存储 | 独立的 fakeWindow | 真实 window |
| 隔离方式 | 读写都代理到 fakeWindow | 写入 window 并记录变更 |
| 多实例支持 | 支持(每个实例独立 fakeWindow) | 不支持(共享 window,需要互斥) |
| 激活/卸载成本 | 低(只需切换 running 标志) | 高(需要遍历 Map 恢复/应用变更) |
| 对子应用兼容性 | 较好 | 更好(直接操作 window,兼容性更强) |
| 适用场景 | 默认推荐,支持多子应用并存 | 单实例场景,兼容老代码 |
五、沙箱生命周期与集成
5.1 创建流程
loadApp()
→ createSandboxContainer(appName, ...)
→ new ProxySandbox / LegacySandbox
→ patchAtBootstrapping() // 启动期补丁(动态 script/style 拦截等)
→ 返回 { instance, mount, unmount }
→ global = sandboxContainer.instance.proxy // 用 proxy 替代 window
→ execScripts(global, strictGlobal) // 在沙箱环境中执行子应用脚本
5.2 挂载流程
mount()
→ sandbox.active() // 激活沙箱
→ rebuild bootstrapping side effects // 重建启动期副作用
→ patchAtMounting() // 挂载期补丁
→ patchInterval() // 劫持 setInterval/clearInterval
→ patchWindowListener() // 劫持 addEventListener/removeEventListener
→ patchHistoryListener() // 劫持 history.listen
→ 动态 append 补丁 // 劫持 DOM 操作
→ rebuild mounting side effects // 重建挂载期副作用
5.3 卸载流程
unmount()
→ 收集所有 freers 的 rebuilders // 保存副作用重建函数供下次 mount 使用
→ sandbox.inactive() // 关闭沙箱
→ ProxySandbox: 恢复白名单变量,标记 sandboxRunning = false
→ LegacySandbox: 恢复所有修改,删除所有新增,标记 sandboxRunning = false
5.4 脚本执行
子应用的脚本通过 import-html-entry 的 execScripts 执行,核心原理是:
// strictGlobal 模式(ProxySandbox)
(function(window, self, globalThis) {
with(window) {
// 子应用代码
// 所有未声明的变量访问都通过 with → proxy 的 has/get trap
// 所有 window.xxx 的访问都通过 proxy 的 get trap
// 所有 window.xxx = yyy 都通过 proxy 的 set trap
}
}).call(proxy, proxy, proxy, proxy)
这样子应用代码中对全局变量的所有操作都被 Proxy 拦截,实现了完整的 JS 隔离。
六、关键设计决策总结
-
fakeWindow 作为 Proxy target:满足 ES 规范对 Proxy 不变量的要求,同时提供独立的存储空间。
-
防逃逸:
window/self/globalThis/top/parent都返回 proxy 自身,堵死子应用获取真实window的路径。 -
unscopables + with:利用
Symbol.unscopables让 ES 内置全局变量跳过 proxy 查找,提升性能;其余变量通过with语句走 proxy 的has/gettrap。 -
函数 bind:原生 API 必须绑定正确的
this,否则会抛Illegal invocation。 -
白名单同步:少量特殊变量(
System、__cjsWrapper)需要同步到真实window,并在卸载时恢复。 -
currentRunningApp 注册:通过微任务机制标记当前运行的子应用,让 DOM API 劫持能正确归属动态资源。