如何实现一个js沙箱

738 阅读3分钟

一. 如何应用沙箱

所有沙箱都是一个类,且都有 proxy 属性, inactive,active 两个方法,分别控制沙箱的打开和关闭。在qiankun中,每一个子应用都会有独属于自己的沙箱实例:

  • active:启动当前沙箱环境
  • inactive:关闭当前沙箱环境
  • proxy属性:代入 execScript 的 proxy,即子应用的window属性是谁。

沙箱的使用可以理解就是一个IIFE,类似于webpack的运行时对模块化的实现,在乾坤中,它实际上也是 execScript 的原理:

eval(
    '(function(window) {
      // ... 子应用脚本
    })(proxy)'  // 代入的proxy,就是对应沙箱实例的proxy属性
)

二. 快照沙箱(SnapshotSandbox)

应用特点:

  1. 不支持 proxy 时选用的沙箱,只支持单例,即只能同时存在一个子应用。
  2. 子应用上下文直接对window进行修改。

实现思路:

  1. active时,去给window拍照。遍历window上的所有自有属性,this.windowSnapshot[key] = window[key]
  2. 遍历this.modifyPropsMap,对window所有属性进行子应用定制化修改,window[key] = this.modifyPropsMap[key]
  3. 子应用的js逻辑修改的就是货真价实的window,在inactive时,向this.modifyPropsMap登记变化的属性,随后将window还原

所存在的问题:

  1. 这样实现没有深度clone,如果有修改window.obj = {} 中的某些属性,inactive时候会认为没有发生改变,我认为应该采取深层clone,并递归对比。
class SnapshotSandbox {
  constructor () {
    this.windowSnapshot = {}
    this.modifyPropsMap = {}
  }

  active() {
     // 1. 给window拍照
    Object.keys(window).forEach(key => {
      this.windowSnapshot[key] = window[key]
    })
     // 2. 把window还原至沙箱需要的状态 
    Object.keys(this.modifyPropsMap).forEach(key => {
      window[key] = this.modifyPropsMap[key]
    })
  }

  inactive() {
     // 1. 遍历当前window,根据windowSnapshot还原window
    Object.keys(window).forEach(key => {
      if (this.windowSnapshot[key] !== window[key]) {
         // 如果当前window[key]和this.windowSnapshot[key]不相等,说明key修改过
        this.modifyPropsMap[key] = window[key]  // 更新key至modify
        this.window[key] = this.windowSnapshot[key]  // 将照片属性还原至window
      }
    })
  }
}

三. Proxy单例沙箱(LegacySandBox)

应用特点:

  1. 支持proxy,且只支持单例,即同时渲染一个子应用
  2. 子应用上下文,给window一个proxy,通过proxy修改window,子应用运行期间会切实修改window

实现思路:

  1. constructor创建proxy,

    1. set:增加的属性收录在 addedPropsMapInSandbox 中,修改的属性收录在 modifiedPropsOriginalValueMapInSandbox,无论增加还是修改,都收录在 currentUpdatedPropsValueMap 中。然后切实修改window对应属性。
    2. get:直接从window中取值
  2. active:遍历 currentUpdatePropsValueMap ,然后将对应的k-v设置到window上。

  3. inactive:

    1. 遍历 addedPropsMapInSandbox,删除 window上对应的k-v
    2. 遍历 modifiedPropsOriginalValueMapInSandbox,将window还原至对应k-v
class LegacySandbox {
  constructor () {
    this.addPropsMap = new Map([])
    this.modifiedProps = new Map([])
    this.updatedProps = new Map([]) // 感觉没啥必要,addPropsMap和modifiedProps的总和不就是updatedProps么

    const fakeWindow = Object.create(null)

    this.proxy = new Proxy(fakeWindow, {
      set(key, value) {
        // 1. 增加的属性,添加到 addPropsMap
        if (!window.hasOwnProperty(key)) {
          this.addPropsMap.set(key, value)
        } else if (!this.modifiedProps.has(key)) {
          // 2. 修改的属性,记录在 modifiedProps
          this.modifiedProps.set(key, window[key])
        }
        // 3. 增改的属性,统统记录在 updatedProps 
        this.updatedProps.set(key, value)
        // 4. 增改的属性,切实修改到 window
        window[key] = value
      },
      get (key) {
        const value = window[key]
        if (typeof value === 'function') {
          return value.bind(window)
        }
        return window[key]
      }
    })
  }

  active() {
    // 将 updatedProps 上的属性统统还原至子应用上下文
    updatedProps.forEach(key => {
      window[key] = updatedProps.get(key)
    })
  }

  inactive() {
    // 将所有增加的属性设置为undefined
    this.addPropsMap.forEach(key => {
      window[key] = undefined
    })
    // 将所有修改的属性,设置为原值
    this.modifiedProps.forEach(key => {
      window[key] = this.modifiedProps[key]
    })
  }
}

四. Proxy多实例沙箱(ProxySandbox)

应用特点:

  1. 不会切实改变window
  2. 支持多子应用实例,每个子应用重新实例化一个沙箱
  3. 实际上完全没必要active和inactive
  4. 最万能理想,简单的沙箱。

实现思路:

  1. 维护一个 updateProps Map,用这个对象代替window实体

  2. 创建 proxy

    1. set:所有 k-v 都修改到 updateProps 上,不实际修改window上的任何属性
    2. get:所有 key 都优先从 updateProps 上拿,拿不到再上 window 上拿
class ProxySandbox {
    constructor() {
        const rawWindow = window;
        const fakeWindow = {}
        const proxy = new Proxy(fakeWindow, {
            set(target, p, value) {
                target[p] = value;
                return true
            },
            get(target, p) {
                return target[p] || rawWindow[p];
            }
        });
        this.proxy = proxy
    }
}

let sandbox1 = new ProxySandbox();
let sandbox2 = new ProxySandbox();
window.a = 1;
((window) => {
    window.a = 'hello';
    console.log(window.a)
})(sandbox1.proxy);
((window) => {
    window.a = 'world';
    console.log(window.a)
})(sandbox2.proxy);