沙箱
什么是沙箱?沙箱是一种隔离机制,它可以将不同的代码隔离开,防止相互污染,举例来说:我们知道,子应用是可以通过window
对象来访问主应用的全局变量,那么如果子应用修改了主应用的全局变量,那么主应用的全局变量就会被污染,这就是我们需要沙箱的原因。
众所周知,iframe是天然的沙箱,但是iframe也有缺点,比如通信问题、样式隔离、性能问题等,所以qiankun实现了自己的沙箱,qiankun沙箱有三种实现方式,分别是:
- SnapshotSandbox
- LegacySandbox
- ProxySandbox
SnapshotSandbox
SnapshotSandbox
是qiankun最早的沙箱实现,实现原理比较简单。
实现原理 :在子应用加载之前,先将主应用的window对象做一份快照,子应用卸载的时候,再将快照还原,在这个过程中会做diff比较并记录修改的数据,这样就实现了子应用与主应用的隔离,但是这种方式有一个缺点,就是性能问题,因为每次子应用加载的时候,都需要做一次快照,快照是遍历window,这样会影响性能,所以后来qiankun又实现了LegacySandbox
。
适用场景:使用与不支持Proxy的浏览器。
缺点: 会污染全局window,有性能问题
ts
function iter(obj: typeof window, callbackFn: (prop: any) => void) {
// eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const prop in obj) {
// patch for clearInterval for compatible reason, see #1490
if (obj.hasOwnProperty(prop) || prop === 'clearInterval') {
callbackFn(prop);
}
}
}
/**
* 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
*/
export default class SnapshotSandbox implements SandBox {
proxy: WindowProxy;
name: string;
type: SandBoxType;
sandboxRunning = true;
private windowSnapshot!: Window;
private modifyPropsMap: Record<any, any> = {};
constructor(name: string) {
this.name = name;
this.proxy = window;
this.type = SandBoxType.Snapshot;
}
active() {
// 记录当前快照
this.windowSnapshot = {} as Window;
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
});
// 恢复之前的变更
Object.keys(this.modifyPropsMap).forEach((p: any) => {
window[p] = this.modifyPropsMap[p];
});
this.sandboxRunning = true;
}
inactive() {
this.modifyPropsMap = {};
iter(window, (prop) => {
if (window[prop] !== this.windowSnapshot[prop]) {
// 记录变更,恢复环境
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
});
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap));
}
this.sandboxRunning = false;
}
patchDocument(): void {}
}
关键逻辑就是在active
和inactive
方法中,active方法会将window对象做一份快照,并应用变更,inactive方法会将快照还原,并记录变更,这样就实现了隔离。

手撸沙箱
我们单独把沙箱拿出来,稍微修改下,实现思路和SnapshotSandbox
一样,通过一个例子加深下我们的理解:
js
function iter(obj, callbackFn) {
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
callbackFn(prop);
}
}
}
class Sandbox {
windowSnapshot;
modifyPropsMap = {};
constructor() {
this.windowSnapshot = {};
this.proxy = myWindow;
this.sandboxRunning = true;
}
active() {
this.windowSnapshot = {};
iter(myWindow, (prop) => {
this.windowSnapshot[prop] = myWindow[prop];
});
// 恢复之前的变更
Object.keys(this.modifyPropsMap).forEach((p) => {
myWindow[p] = this.modifyPropsMap[p];
});
this.sandboxRunning = true;
}
inactive() {
iter(myWindow, (prop) => {
if (myWindow[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = myWindow[prop];
myWindow[prop] = this.windowSnapshot[prop];
}
});
this.sandboxRunning = false;
}
}
// 这是我们模拟的全局window对象, 在上面有三个属性, name, desc, data,sayYes
const myWindow = {
name: '这是模拟的全局window对象',
desc: '这是主项目的描述',
data: {
age: 12,
}
};
// 加载子应用的沙箱
const sandbox = new Sandbox();
((myWindow) => {
// 子应用完成加载
// 在子应用中修改全局window对象(也就是我们的myWidow)
sandbox.active();
myWindow.name = '这是子应用修改后的全局window对象';
myWindow.desc = '这是子应用修改后的描述';
myWindow.newData = {
age: 13,
};
console.log(myWindow);
// 输出结果为下面这样
// {"name": "这是子应用修改后的全局window对象", "desc": "这是子应用修改后的描述","data": { "age": 12},"newData": {"age": 13}}
// 可以看到在子应用中,我们的全局window对象已经被修改了
// 子应用卸载
// 失活沙箱
sandbox.inactive();
console.log(myWindow);
// 输出结果为下面这样
// { "name": "这是模拟的全局window对象", "desc": "这是主项目的描述", "data": { "age": 12 }, "newData": undefined}
// 从上面的结果可以看出, 子应用修改的全局window对象并没有影响原来的window, 这就是沙箱的作用
sandbox.active();
console.log(myWindow);
// 输出结果为下面这样
// {"name": "这是子应用修改后的全局window对象", "desc": "这是子应用修改后的描述","data": { "age": 12},"newData": {"age": 13}}
})(sandbox.proxy);
通过上面的的小栗子,我们更直观的看到了SnapshotSandbox
的工作原理,上面的代码,我用了一个全局变量myWindow
来模拟window对象,然后通过Sandbox
类来实现沙箱,active
方法会将window对象做一份快照,并应用变更,inactive
方法会将快照还原,并记录变更,这样就实现了隔离。
LegacySandbox
qiankun基于Proxy实现了两种不同应用场景的沙箱,分别是LegacySandbox
(单例)和ProxySandbox
(多例),LegacySandbox
虽然使用了代理,但仍然是对window的读写,所以这种沙箱只支持单例模式。

在沙箱激活和失活的时候他们的逻辑如下:

手撸沙箱
核心思想还是要把变更存储起来,方便在激活和失活的时候恢复环境,下面我们来把核心代码摘出来,逐行解析下
js
// 假设这是我们的全局window对象
const myWindow = {
name: 'myWindow',
age: 18
};
class LegacySandBox {
constructor(name, globalContext = myWindow) {
// 沙箱运行时新增的全局变量
this.addedPropsMapInSandbox = new Map();
// 沙箱运行时修改的全局变量
this.modifiedPropsOriginalValueMapInSandbox = new Map();
// 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot
this.currentUpdatedPropsValueMap = new Map();
// 沙箱是否在运行中
this.sandboxRunning = true;
// 记录最后修改的属性
this.latestSetProp = '';
this.name = name;
this.globalContext = globalContext;
// 全局window对象
const rawWindow = globalContext;
// 影子window对象,用来代理全局window对象
const fakeWindow = Object.create(null);
// 设置value,并记录数据变更
const setTrap = (p, newValue, originValue, sync2Window) => {
// 1. 记录数据变更
if (this.sandboxRunning) {
// 如果原始window上没有该属性,说明是新增的全局变量,记录到addedPropsMapInSandbox
if(!rawWindow.hasOwnProperty(p)) {
this.addedPropsMapInSandbox.set(p, newValue);
} else if (!this.modifiedPropsOriginalValueMapInSandbox.has(p)) {
// 如果原始window上有该属性,说明是修改的全局变量,记录到modifiedPropsOriginalValueMapInSandbox
this.modifiedPropsOriginalValueMapInSandbox.set(p, originValue);
}
// 记录到currentUpdatedPropsValueMap
this.currentUpdatedPropsValueMap.set(p, newValue);
// 2. 通过原始window对象设置值
if (sync2Window) {
rawWindow[p] = newValue;
}
}
return true;
};
const proxy = new Proxy(fakeWindow, {
get(target, p) {
const value = rawWindow[p];
return value;
},
set(target, p, newValue) {
const originalValue = rawWindow[p];
return setTrap(p, newValue, originalValue, true);
}
});
this.proxy = proxy;
}
// 给window对象设置属性, 1. 激活的时候恢复记录的值 2. 失活的时候删除记录的值
setWindowProp(prop, value, toDelete) {
if (value === undefined && toDelete) {
// 删除值
delete this.globalContext[prop];
} else {
this.globalContext[prop] = value;
}
}
active() {
// 1. 恢复沙箱运行时的全局变量
if (!this.sandboxRunning) {
this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
}
// 2. 设置运行状态
this.sandboxRunning = true;
}
inactive() {
// 1. 重置沙箱运行时的全局变量
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v));
this.addedPropsMapInSandbox.forEach((v, p) => this.setWindowProp(p, undefined, true));
// 2. 设置运行状态
this.sandboxRunning = false;
}
}
// 创建沙箱
const sandbox = new LegacySandBox('test');
((myWindow) => {
// 新增全局变量
myWindow.subName = 'subname from sandbox';
// 修改全局变量
myWindow.age = 20;
// 输出全局变量
console.log(myWindow.subName, myWindow.age); // subName: 'subname from sandbox', age: 20
// 失活沙箱
sandbox.inactive();
// 输出全局变量
console.log(myWindow.name, myWindow.age); // myWindow 18
// 激活沙箱
sandbox.active();
console.log(myWindow.subName, myWindow.age); // subName: 'subname from sandbox', age: 20
})(sandbox.proxy);
ProxySandbox
LegacySandbox
虽然使用了代理,但仍然是对window的读写,在多个实例运行时肯定会冲突,当我们需要同时启动多个实例时,就需要用到ProxySandbox
,ProxySandbox
也是基于Proxy实现的,支持多例模式,ProxySandbox
同样是对get和set做了拦截,对fakewindow进行代理,并将window 的 document、location、top、window 等属性拷贝一份,当我们修改window对象的时候,会将修改同步到fakewindow,当我们获取属性时,优先从fakewindow中获取,如果fakewindow中没有,再从window中获取,这样就实现了隔离。 由于我们的数据都存储在了当前沙箱对应的fakeWindow上,所以运行多个子应用时,他们的沙箱环境也是独立的,互不影响。

手撸沙箱
我们忽略掉所有的边界处理,实现一个最简单的ProxySandbox
,来加深下我们的理解:
js
let activeSandboxCount = 0;
class ProxySandbox {
constructor(name, globalContext = myWindow) {
this.name = name;
this.sandboxRunning = true;
this.globalContext = globalContext;
const fakeWindow = {};
const proxy = new Proxy(fakeWindow, {
get: (target, key) => {
const actualTarget = key in target ? target : this.globalContext;
return actualTarget[key];
},
set: (target, p, newValue) => {
if (this.sandboxRunning) {
target[p] = newValue;
}
}
});
this.proxy = proxy;
}
active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
inactive() {
this.sandboxRunning = false;
}
}
const myWindow = {
name: 'myWindow',
age: 18,
};
const sandbox1 = new ProxySandbox('box1');
const sandbox2 = new ProxySandbox('box2');
((myWindow) => {
myWindow.subName = 'subname from sandbox1';
myWindow.age = 20;
// 输出全局变量
console.log(myWindow.subName, myWindow.age); // subname from sandbox1 20
// 失活沙箱
sandbox1.inactive();
// 激活沙箱
sandbox1.active();
console.log(myWindow.subName, myWindow.age); // subname from sandbox1 20
})(sandbox1.proxy);
((myWindow) => {
myWindow.subName = 'subname from sandbox2';
myWindow.age = 30;
// 输出全局变量
console.log(myWindow.subName, myWindow.age); // subname from sandbox2 30
// 失活沙箱
sandbox2.inactive();
// 激活沙箱
sandbox2.active();
console.log(myWindow.subName, myWindow.age); // subname from sandbox2 30
})(sandbox2.proxy);
console.log(myWindow.name, myWindow.age); // myWindow 18
总结
qiankun的三种沙箱我们都已经介绍完了,可以分为两大类,一种是快照模式,一种是代理模式,代理模式又有两种适用场景,单例模式和多例模式
- 快照模式:
SnapshotSandbox
,单例,适用于不支持Proxy的浏览器 - 代理模式:
LegacySandbox
,单例,适用于支持Proxy的浏览器 - 代理模式:
ProxySandbox
,单例、多例,适用于支持Proxy的浏览器