前言
qiankun 为了实现应用和应用之间的变量隔离,做了三种沙箱机制,分别为快照沙箱(snapshotSandbox) , 单例沙箱(legacySandbox),代理沙箱(proxySandbox),这篇文章简单了解一下这三种沙箱机制的区别以及优缺点, 简单实现一下主要功能
使用规则
可以看到,当前环境有 Proxy 类的时候,会使用 proxy 做代理沙箱,否则使用的是快照沙箱,而且默认的 useLooseSandbox 为 false ,也就是说如果当前 window 有 proxy ,则开启的是代理沙箱(proxySandbox)
快照沙箱
代码
function iter(obj: typeof window, callbackFn: (prop: any) => void) {
for (const prop in obj) {
// ie10,11下默认hasOwnProperty不包括clearInterval等
// https://github.com/umijs/qiankun/pull/1490
if (obj.hasOwnProperty(prop) || prop === 'clearInterval') {
callbackFn(prop);
}
}
}
/**
* 记录当前进入沙箱时的window变量,当出沙箱还原window
*/
class SnapshotSandbox {
// 当前沙箱的状态
sandboxRunning = true;
// 激活时window的快照
private windowSnapshot!: Window;
// 在沙箱状态下修改的变量
private modifyPropsMap: Record<any, any> = {}
proxy = window;
constructor(globalContext = window) {
this.proxy = globalContext;
}
/**
* 激活的时候需要做的事
* 1. 把window当前的状态保存下来,记录到快照中,用于失活的时候还原window。
* 2. 如果不是第一次激活,则还原上次沙箱情况下的状态
*/
active() {
this.windowSnapshot = {} as any;
// 把window上的所有属性赋值到windowSnapshot上;
iter(this.proxy, (prop) => {
this.windowSnapshot[prop] = this.proxy[prop];
});
// 当是第二次激活的时候,需要把上次的内容赋值到这次激活的状态
Object.keys(this.modifyPropsMap).forEach((prop) => {
this.proxy[prop] = this.modifyPropsMap[prop];
});
this.sandboxRunning = true;
}
/**
* 失活的时候做的事
* 1. 把修改的状态记录到一个变量,用于下次激活还原当前状态
* 2. 还原window上的状态为激活的状态
*/
inactive() {
this.modifyPropsMap = {};
iter(this.proxy, (prop) => {
if (this.proxy[prop] !== this.windowSnapshot[prop]) {
// 表示修改过这个变量, 记录到modifyPropsMap
this.modifyPropsMap[prop] = this.proxy[prop];
// 还原window
this.proxy[prop] = this.windowSnapshot[prop];
}
});
this.sandboxRunning = false;
}
}
简单阐述一下: 当激活沙箱的时候,保留一个 window 的快照变量, 在这个阶段操作的window都会在失活的时候恢复回刚进入时候的状态,当下次进入沙箱的时候又会吧状态恢复到上次修改的状态。
案例
const snapShotSandbox1 = new SnapshotSandbox();
window.a = 1;
((window) => {
snapShotSandbox1.active();
window.b = 2;
window.a = 3;
console.log('第一个沙箱:', window.a, window.b);
snapShotSandbox1.inactive();
console.log(window.a, window.b);
})(snapShotSandbox1.proxy);
console.log('外部的widnow:', window.a, window.b);
可以看到当激活沙箱后,对window做出的改变都会在失活后消失
缺点
- 会影响到全局的
window
const snapShotSandbox1 = new SnapshotSandbox();
window.a = 1;
((window) => {
snapShotSandbox1.active();
window.b = 2;
window.a = 3;
console.log('第一个沙箱:', window.a, window.b);
// snapShotSandbox1.inactive();
// console.log('第一个沙箱失活后:'window.a, window.b);
})(snapShotSandbox1.proxy);
console.log('外部的widnow:', window.a, window.b);
但是当我们在没销毁的时候,在外部访问window ,可以看到他会污染window
- 不能做到同时激活多个子应用时的沙箱状态
const snapShotSandbox1 = new SnapshotSandbox();
const snapShotSandbox2 = new SnapshotSandbox();
window.a = 1;
((window) => {
snapShotSandbox1.active();
window.b = 2;
window.a = 3;
console.log('第一个沙箱:', window.a, window.b);
// snapShotSandbox1.inactive();
// console.log('第一个沙箱失活后:'window.a, window.b);
})(snapShotSandbox1.proxy);
((window) => {
snapShotSandbox2.active();
window.c = 3;
console.log('第二个沙箱:' window.a, window.b, window.c);
// snapShotSandbox2.inactive();
})(snapShotSandbox2.proxy);
console.log('外部的widnow:', window.a, window.b, window.c);
当我们同时激活两个沙箱的时候可以发现他们的操作都是在同一个window上操作的,做不到多沙箱隔离
- 遍历
window做的快照处理,性能低
优点
兼容性好
单例沙箱
代码
// 返回当前属性的 configurable 的值,如果没有 则为true
function isPropConfigurable(target: WindowProxy, prop: PropertyKey) {
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
return descriptor ? descriptor.configurable : true;
}
/**
* 基于 proxy 实现的单一沙箱
*/
class LegacySandbox {
sandboxRunning = true
globalContext: typeof window;
proxy: WindowProxy;
// 沙箱期间新增的全局变量
private addedPropsMapInSandBox = new Map<PropertyKey, any>();
// 沙箱期间更新的全局变量
private modifiedPropsOriginalValueMapInSandBox = new Map<PropertyKey, any>();
// 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot
private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();
//访问window变量的同时会给以上三个变量做记录
constructor(globalContext = window) {
this.globalContext = globalContext;
const rawWindow = globalContext;
const fakeWindow = Object.create(null);
const setTrap = (p: PropertyKey, value: any, originalValue: any, sync2Window: boolean = true) => {
if (this.sandboxRunning) {
if (!rawWindow.hasOwnProperty(p)) {
// 表示新增的 在失活的时候要删除
this.addedPropsMapInSandBox.set(p, value);
} else if (!this.modifiedPropsOriginalValueMapInSandBox.has(p)) {
// 如果从来没在modifiedPropsOriginalValueMapInSandBox记录过,则记录
this.modifiedPropsOriginalValueMapInSandBox.set(p, originalValue);
}
// 记录沙箱状态下修改过的所有状态
this.currentUpdatedPropsValueMap.set(p, value);
if (sync2Window) {
// 还是直接操作window
(rawWindow as any)[p] = value;
}
}
return true;
};
const proxy = new Proxy(fakeWindow, {
set(target: Window, prop: PropertyKey, value: any): boolean {
// 上次的值
const originalValue = (<any>rawWindow)[prop];
return setTrap(prop, value, originalValue);
},
get(target: Window, p: PropertyKey): any {
// 避免使用沙箱环境进行沙箱逃逸处理
if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
return proxy;
}
return (rawWindow as any)[p];
}
});
this.proxy = proxy;
}
private setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
if (value === undefined && toDelete) {
// 处理删除情况
delete (this.globalContext as any)[prop];
} else if (isPropConfigurable(this.globalContext, prop) && typeof prop !== 'symbol') {
// 当前属性描述可以被更改
(this.globalContext as any)[prop] = value;
}
}
/**
* 如果不是第一次激活,则恢复window上的值, currentUpdatedPropsValueMap
*/
active() {
if (!this.sandboxRunning) {
this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
};
this.sandboxRunning = true;
}
/**
* 失活状态下
* 1.将新增的变量删除
* 2.将修改的状态还原
*/
inactive() {
this.addedPropsMapInSandBox.forEach((v, p) => this.setWindowProp(p, undefined, true));
this.modifiedPropsOriginalValueMapInSandBox.forEach((v, p) => this.setWindowProp(p, v));
this.sandboxRunning = false;
}
}
简单阐述: 基于 proxy 实现的代理。再激活状态下,当给 window 上添加属性的时候,用addedPropsMapInSandBox 记录新增的,用 modifiedPropsOriginalValueMapInSandBox 记录修改的变量,当失活的时候恢复回去
案例
const legacySandbox1 = new LegacySandbox();
window.a = 1;
((window) => {
legacySandbox1.active();
window.b = 2;
window.a = 3;
console.log('第一个沙箱:', window.a, window.b);
legacySandbox1.inactive();
// console.log(window.a, window.b);
})(legacySandbox1.proxy);
console.log('外部的window:', window.a, window.b);
可以看到当激活沙箱后,对 window 做出的改变都会在失活后消失
缺点
- 会影响到全局的
window
const legacySandbox1 = new LegacySandbox();
window.a = 1;
((window) => {
legacySandbox1.active();
window.b = 2;
window.a = 3;
console.log('第一个沙箱:', window.a, window.b);
// legacySandbox1.inactive();
// console.log(window.a, window.b);
})(legacySandbox1.proxy);
console.log('外部的window:', window.a, window.b);
当我们在沙箱状态下修改的变量,在沙箱没失活的状态下在外部访问window 也会对window造成污染
- 不能做到同时激活多个子应用时的沙箱状态
const legacySandbox1 = new LegacySandbox();
const legacySandbox2 = new LegacySandbox();
window.a = 1;
((window) => {
legacySandbox1.active();
window.b = 2;
window.a = 3;
console.log('第一个沙箱:', window.a, window.b);
// legacySandbox1.inactive();
// console.log(window.a, window.b);
})(legacySandbox1.proxy);
((window) => {
legacySandbox2.active();
window.c = 3;
console.log('第二个沙箱:', window.a, window.b, window.c);
// legacySandbox2.inactive();
})(legacySandbox2.proxy);
console.log('外部的window:', window.a, window.b, window.c);
同是激活多个沙箱也是不支持的
- 用的
proxy兼容性没有快照沙箱好
优点
相比于快照沙箱省去了遍历 window ,性能比快照沙箱好
代理沙箱
代码
type FakeWindow = Window & Record<PropertyKey, any>
class ProxySandbox {
/** 记录沙箱状态下变更的变量 */
private updateValueMap = new Map<PropertyKey, any>();
sandboxRunning = true;
proxy: WindowProxy;
active() {
this.sandboxRunning = true;
}
inactive() {
this.sandboxRunning = false;
}
constructor() {
const rawWindow = window;
const fakeWindow = {} as FakeWindow;
const proxy: WindowProxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
this.updateValueMap.set(p, value);
}
return true;
},
get: (target: FakeWindow, p: PropertyKey) => {
/** 避免做沙箱逃逸 */
if (p === 'top' || p === 'window' || p === 'self') {
return proxy;
}
// 优先从updateValueMap取
return this.updateValueMap.get(p) || (rawWindow as any)[p];
}
});
this.proxy = proxy;
}
}
简单阐述:通过 updateValueMap 这个变量记录当前沙箱中的修改的变量(没有直接操作 window ),在 get 的时候优先从 updateValueMap 变量取,如果没有,从 window 上取。
案例
const proxySandbox1 = new ProxySandbox();
window.a = 1;
((window) => {
proxySandbox1.active();
window.b = 2;
window.a = 3;
console.log("第一个沙箱: ",window.a, window.b);
})(proxySandbox1.proxy);
console.log("外部的window: ", window.a, window.b);
由于代理沙箱不会对外部window做更改,所以说在沙箱内部是不会影响到外部的window的
测试一下多实例沙箱
const proxySandbox1 = new ProxySandbox();
const proxySandbox2 = new ProxySandbox();
window.a = 1;
((window) => {
proxySandbox1.active();
window.b = 2;
window.a = 3;
console.log("第一个沙箱: ",window.a, window.b);
})(proxySandbox1.proxy);
((window) => {
proxySandbox2.active();
window.c = 3;
console.log('第二个沙箱: ',window.a, window.b, window.c);
})(proxySandbox2.proxy);
console.log("外部的window: ", window.a, window.b, window.c );
可以看到,代理沙箱是真正做到环境隔离,而且可以支持多实例沙箱