浅谈qiankun内部三种沙箱机制

1,409 阅读6分钟

前言

qiankun 为了实现应用和应用之间的变量隔离,做了三种沙箱机制,分别为快照沙箱(snapshotSandbox) , 单例沙箱(legacySandbox),代理沙箱(proxySandbox),这篇文章简单了解一下这三种沙箱机制的区别以及优缺点, 简单实现一下主要功能

使用规则

image.png

image.png

image.png

image.png

可以看到,当前环境有 Proxy 类的时候,会使用 proxy 做代理沙箱,否则使用的是快照沙箱,而且默认的 useLooseSandboxfalse ,也就是说如果当前 windowproxy ,则开启的是代理沙箱(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);

image.png

可以看到当激活沙箱后,对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);

image.png

但是当我们在没销毁的时候,在外部访问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);

image.png

当我们同时激活两个沙箱的时候可以发现他们的操作都是在同一个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);

image.png

可以看到当激活沙箱后,对 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);

image.png

当我们在沙箱状态下修改的变量,在沙箱没失活的状态下在外部访问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);


image.png

同是激活多个沙箱也是不支持的

  • 用的 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);

image.png

由于代理沙箱不会对外部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 );

image.png

可以看到,代理沙箱是真正做到环境隔离,而且可以支持多实例沙箱