神族九帝's blog 神族九帝's blog
首页
网盘 (opens new window)
线报 (opens new window)
商城 (opens new window)
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 基础篇
  • 进阶篇
  • 高级篇
  • 计算机基础
  • 高频考点
  • 精简题
  • 综合问题
  • 复习题
  • vue
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • 更多
  • 网站
  • 资源
  • Vue资源
  • 收藏的一些API
  • 未来要做的事
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 导航
GitHub (opens new window)

神族九帝,永不言弃

首页
网盘 (opens new window)
线报 (opens new window)
商城 (opens new window)
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 基础篇
  • 进阶篇
  • 高级篇
  • 计算机基础
  • 高频考点
  • 精简题
  • 综合问题
  • 复习题
  • vue
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • 更多
  • 网站
  • 资源
  • Vue资源
  • 收藏的一些API
  • 未来要做的事
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 导航
GitHub (opens new window)
  • 复习指导

  • HTML

  • CSS

  • JavaScript

    • 思维导图
    • 数据类型
    • 隐式类型转换
    • 闭包
    • 原型和原型链
    • 继承
    • 数组扁平化flat
    • new操作符做了什么
    • 数组去重性能
    • 防抖与节流函数
    • 深浅拷贝
    • Event Loop
    • 数组方法和类数组
    • 数组排序
    • 异步编程
    • 获取URL参数
    • 模块规范
    • tree-shaking的原理
    • 隔离沙箱
      • iframe 实现沙箱
        • 代理实现共享
      • diff 实现沙箱
      • 基于代理(proxy)实现单实例沙箱
      • 基于代理(proxy)实现多实例沙箱
      • qiankun 沙箱源码解读
        • LegacySandbox 单实例沙箱
        • ProxySandbox 多实例沙箱
        • SapshotSandbox 快照沙箱
      • 参考链接
  • 设计模式

  • 浏览器

  • 手写系列

  • Vue

  • Webpack

  • Http

  • 前端优化

  • 项目

  • 面试真题

  • 算法

  • 精选文章

  • 八股文

  • 前端工程化

  • 面试题
  • JavaScript
wu529778790
2022-05-31

隔离沙箱

73a061edff0b4f17aabed63d6664ec4d

在微前端领域当中,沙箱是很重要的一件事情。像微前端框架 single-spa 没有实现 js 沙箱,我们在构建大型微前端应用的时候,很容易造成一些变量的冲突,对应用的可靠性面临巨大的风险。在微前端当中,有一些全局对象在所有的应用中需要共享,如 document,location,等对象。子应用开发的过程中可能是多个团队在做,很难约束他们使用全局变量。有些页面可能会有多个不同的子应用,需要我们支持多沙箱,每个沙箱需要有加载,卸载,在恢复的能力。

# iframe 实现沙箱

let iframe = document.createElement("iframe", { src: "about:blank" });
document.body.appendChild(iframe);
const sandboxGlobal = iframe.contentWindow;

注意:只有同域的 ifame 才能取出对应的 contentWindow, iframe 的 src 设置为 about:blank,可以保证一定是同域的,也不会发生资源加载,参考 iframe src (opens new window)

# 代理实现共享

微前端除了有一个隔离的 window 环境外,其实还需要共享一些全局对象,这时候我们可以用代理去实现

class SandboxWindow {
  /**
   * 构造函数
   * @param {*} context 需要共享的对象
   * @param {*} frameWindow iframe的window
   */
  constructor(context, frameWindow) {
    return new Proxy(frameWindow, {
      get(target, name) {
        if (name in context) {
          // 优先使用共享对象
          return context[name];
        }
        return target[name];
      },
      set(target, name, value) {
        if (name in context) {
          // 修改共享对象的值
          return (context[name] = value);
        }
        target[name] = value;
      },
    });
  }
}

// 需要全局共享的变量
const context = { document: window.document, history: window.history };

// 创建沙箱
const newSandboxWindow = new SandboxWindow(context, sandboxGlobal);

// 判断沙箱上的对象和全局对象是否相等
console.log("equal", newSandboxWindow.document === window.document); // equal true

newSandboxWindow.abc = "1"; //在沙箱上添加属性
console.log(window.abc); // 在全局上查看属性, undefined
console.log(newSandboxWindow.abc); //在沙箱上查看属性, 1

以上我们利用 iframe 沙箱可以实现以下特性:

  • 全局变量隔离,如 setTimeout、location、react 不同版本隔离
  • 路由隔离,应用可以实现独立路由,也可以共享全局路由
  • 多实例,可以同时存在多个独立的微应用同时运行

# diff 实现沙箱

在不支持代理的浏览器中,我们可以通过 diff 的方式实习沙箱。在应用运行的时候保存一个快照 window 对象,将当前 window 对象的全部属性都复制到快照对象上,子应用卸载的时候将 window 对象修改做个 diff,将不同的属性用个 modifyMap 保存起来,再次挂载的时候再加上这些修改的属性。

class DiffSandbox {
  constructor(name) {
    this.name = name;
    this.modifyMap = {}; // 存放修改的属性
    this.windowSnapshot = {};
  }
  active() {
    // 缓存active状态的沙箱
    this.windowSnapshot = {};
    for (const item in window) {
      this.windowSnapshot[item] = window[item];
    }

    Object.keys(this.modifyMap).forEach((p) => {
      window[p] = this.modifyMap[p];
    });
  }

  inactive() {
    for (const item in window) {
      if (this.windowSnapshot[item] !== window[item]) {
        // 记录变更
        this.modifyMap[item] = window[item];
        // 还原window
        window[item] = this.windowSnapshot[item];
      }
    }
  }
}

const diffSandbox = new DiffSandbox("diff沙箱");
diffSandbox.active(); // 激活沙箱
window.a = "1";
console.log("开启沙箱:", window.a); // 开启沙箱: 1
diffSandbox.inactive(); //失活沙箱
console.log("失活沙箱:", window.a); // 实货沙箱: undefined
diffSandbox.active(); // 重新激活
console.log("再次激活", window.a); // 再次激活: 1

# 基于代理(proxy)实现单实例沙箱

在 ES6 当中,我们可以通过代理(Proxy)实现对象的劫持。基本实录也是通过 window 对象的修改进行记录,在卸载时删除这些记录,在应用再次激活时恢复这些记录,来达到模拟沙箱环境的目的。代码如下

// 修改window属性的公共方法
const updateWindowProp = (prop, value, isDel) => {
  if (value === undefined || isDel) {
    delete window[prop];
  } else {
    window[prop] = value;
  }
};

class ProxySandbox {
  active() {
    // 根据记录还原沙箱
    this.currentUpdatedPropsValueMap.forEach((v, p) => updateWindowProp(p, v));
  }
  inactive() {
    // 1 将沙箱期间修改的属性还原为原先的属性
    this.modifiedPropsMap.forEach((v, p) => updateWindowProp(p, v));
    // 2 将沙箱期间新增的全局变量消除
    this.addedPropsMap.forEach((_, p) => updateWindowProp(p, undefined, true));
  }

  constructor(name) {
    this.name = name;
    this.proxy = null;
    // 存放新增的全局变量
    this.addedPropsMap = new Map();
    // 存放沙箱期间更新的全局变量
    this.modifiedPropsMap = new Map();
    // 存在新增和修改的全局变量,在沙箱激活的时候使用
    this.currentUpdatedPropsValueMap = new Map();

    const { addedPropsMap, currentUpdatedPropsValueMap, modifiedPropsMap } =
      this;
    const fakeWindow = Object.create(null);
    const proxy = new Proxy(fakeWindow, {
      set(target, prop, value) {
        if (!window.hasOwnProperty(prop)) {
          // 如果window上没有的属性,记录到新增属性里
          // debugger;
          addedPropsMap.set(prop, value);
        } else if (!modifiedPropsMap.has(prop)) {
          // 如果当前window对象有该属性,且未更新过,则记录该属性在window上的初始值
          const originalValue = window[prop];
          modifiedPropsMap.set(prop, originalValue);
        }
        // 记录修改属性以及修改后的值
        currentUpdatedPropsValueMap.set(prop, value);
        // 设置值到全局window上
        updateWindowProp(prop, value);
        return true;
      },
      get(target, prop) {
        return window[prop];
      },
    });
    this.proxy = proxy;
  }
}

const newSandBox = new ProxySandbox("代理沙箱");
const proxyWindow = newSandBox.proxy;
proxyWindow.a = "1";
console.log("开启沙箱:", proxyWindow.a, window.a); // 开启沙箱: 1 1
newSandBox.inactive(); //失活沙箱
console.log("失活沙箱:", proxyWindow.a, window.a); // 失活沙箱: undefined undefined
newSandBox.active(); //失活沙箱
console.log("重新激活沙箱:", proxyWindow.a, window.a); // 1 1

这种方式同一时刻只能有一个激活的沙箱,否则全局对象上的变量会有两个以上的沙箱更新,造成全局变量冲突。

# 基于代理(proxy)实现多实例沙箱

在单实例的场景总,我们的 fakeWindow 是一个空的对象,其没有任何储存变量的功能,微应用创建的变量最终实际都是挂载在 window 上的,这就限制了同一时刻不能有两个激活的微应用。

class MultipleProxySandbox {
  active() {
    this.sandboxRunning = true;
  }
  inactive() {
    this.sandboxRunning = false;
  }

  /**
   * 构造函数
   * @param {*} name 沙箱名称
   * @param {*} context 共享的上下文
   * @returns
   */
  constructor(name, context = {}) {
    this.name = name;
    this.proxy = null;
    const fakeWindow = Object.create({});
    const proxy = new Proxy(fakeWindow, {
      set: (target, name, value) => {
        if (this.sandboxRunning) {
          if (Object.keys(context).includes(name)) {
            context[name] = value;
          }
          target[name] = value;
        }
      },
      get: (target, name) => {
        // 优先使用共享对象
        if (Object.keys(context).includes(name)) {
          return context[name];
        }
        return target[name];
      },
    });
    this.proxy = proxy;
  }
}

const context = { document: window.document };

const newSandBox1 = new MultipleProxySandbox("代理沙箱1", context);
newSandBox1.active();
const proxyWindow1 = newSandBox1.proxy;

const newSandBox2 = new MultipleProxySandbox("代理沙箱2", context);
newSandBox2.active();
const proxyWindow2 = newSandBox2.proxy;
console.log(
  "共享对象是否相等",
  window.document === proxyWindow1.document,
  window.document === proxyWindow2.document
); // true true

proxyWindow1.a = "1"; // 设置代理1的值
proxyWindow2.a = "2"; // 设置代理2的值
window.a = "3"; // 设置window的值
console.log("打印输出的值", proxyWindow1.a, proxyWindow2.a, window.a); // 1 2 3

newSandBox1.inactive();
newSandBox2.inactive(); // 两个沙箱都失活

proxyWindow1.a = "4"; // 设置代理1的值
proxyWindow2.a = "4"; // 设置代理2的值
window.a = "4"; // 设置window的值
console.log("失活后打印输出的值", proxyWindow1.a, proxyWindow2.a, window.a); // 1 2 4

newSandBox1.active();
newSandBox2.active(); // 再次激活

proxyWindow1.a = "4"; // 设置代理1的值
proxyWindow2.a = "4"; // 设置代理2的值
window.a = "4"; // 设置window的值
console.log("失活后打印输出的值", proxyWindow1.a, proxyWindow2.a, window.a); // 4 4 4

这种方式同一时刻只能有一个激活的多个沙箱,从而实现多实例沙箱。

以上是微前端比较常用的沙箱实现方式,想要在生产中使用,需要我们做很多的判断和约束。

# qiankun 沙箱源码解读

  • snapshotSandbox 快照沙箱
  • legacySandbox 单实例代理沙箱
  • proxySandbox 多实例代理沙箱

如果浏览器支持 Proxy 就用 LegacySandbox 或 ProxySandbox 沙箱,比较老的浏览器用 SnapshotSandbox 沙箱,现在在支持 proxy 的浏览器 qiankun 里主要用 ProxySandbox

https://juejin.cn/post/6981756262304186405 (opens new window)

# LegacySandbox 单实例沙箱

/**
 * 判断该属性也能从对应的对象上被删除
 */
function isPropConfigurable(target: typeof window, prop: PropertyKey) {
  const descriptor = Object.getOwnPropertyDescriptor(target, prop);
  return descriptor ? descriptor.configurable : true;
}

/**
 * 设置window属性
 * @param prop
 * @param value
 * @param toDelete 是否是删除属性
 */
function setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
  if (value === undefined && toDelete) {
    delete (window as any)[prop];
  } else if (isPropConfigurable(window, prop) && typeof prop !== 'symbol') {
    Object.defineProperty(window, prop, { writable: true, configurable: true });
    (window as any)[prop] = value;
  }
}

/**
 * 基于 Proxy 实现的沙箱
 * TODO: 为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换
 */
export default class SingularProxySandbox implements SandBox {
  /** 沙箱期间新增的全局变量 */
  private addedPropsMapInSandbox = new Map<PropertyKey, any>();

  /** 沙箱期间更新的全局变量 */
  private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();

  /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
  private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();

  name: string; // 名称

  proxy: WindowProxy; // 初始化代理对象

  type: SandBoxType; // 沙箱类型

  sandboxRunning = true; // 沙箱是否在运行

  latestSetProp: PropertyKey | null = null; // 最后设置的props

  /**
   * 激活沙箱的方法
   */
  active() {
    if (!this.sandboxRunning) {
      // 之前记录新增和修改的全局变量更新到当前window上。
      this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
    }
    this.sandboxRunning = true; // 设置沙箱在运行
  }

  /**
   * 失活沙箱的方法
   */
  inactive() {
    // 失活沙箱把记录的初始值还原回去
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
    // 沙箱失活的时候把新增的属性从window上给删除
    this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));

    this.sandboxRunning = false; // 设置沙箱不在运行
  }

  constructor(name: string) {
    this.name = name;
    this.type = SandBoxType.LegacyProxy;
    const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;

    const rawWindow = window; // 获取当前window对象
    const fakeWindow = Object.create(null) as Window; // 创建一个代理对象的window对象

    const proxy = new Proxy(fakeWindow, {
      set: (_: Window, p: PropertyKey, value: any): boolean => {
        if (this.sandboxRunning) { // 判断沙箱是否在启动
          if (!rawWindow.hasOwnProperty(p)) {
            // 当前window上没有该属性,在addedPropsMapInSandbox上记录添加的属性
            addedPropsMapInSandbox.set(p, value);
          } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
            // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
            const originalValue = (rawWindow as any)[p];
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
          }

          // 记录新增和修改的属性
          currentUpdatedPropsValueMap.set(p, value);
          // 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据
          (rawWindow as any)[p] = value;

          // 更新下最后设置的props
          this.latestSetProp = p;

          return true;
        }

        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
        return true;
      },

      get(_: Window, p: PropertyKey): any {
        // 判断用window.top, window.parent等也返回代理对象,在ifream环境也会返回代理对象。做到了真正的隔离,
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }

        const value = (rawWindow as any)[p];
        return getTargetValue(rawWindow, value); // 返回当前值
      },

      /**
       * 用 in 操作判断属性是否存在的时候去window上判断,而不是在代理对象上判断
       */
      has(_: Window, p: string | number | symbol): boolean {
        return p in rawWindow;
      },

      /**
       * 获取对象属性描述的时候也是从window上去判断,代理对象上可能没有
       */
      getOwnPropertyDescriptor(_: Window, p: PropertyKey): PropertyDescriptor | undefined {
        const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
        if (descriptor && !descriptor.configurable) {
          descriptor.configurable = true;
        }
        return descriptor;
      },
    });

    this.proxy = proxy;
  }
}

上面代码都有注释,整个思路主要还是操作 window 对象,通过激活沙箱时还原子应用的状态,卸载时还原主应用的状态来实现沙箱隔离的。跟我们上篇文章的简单实现不同点 qiankun 做了兼容,在健壮性和严谨性都比较好。

# ProxySandbox 多实例沙箱

我们先看创建 fakeWindow 的方法,这里很巧妙,主要是把 window 上不支持改变和删除的属性,但有 get 方法的属性创建到 fakeWindow 上。这里有几个我们平常在业务开发用的不多的几个 API,主要是 Object.getOwnPropertyDescriptor 和 Object.defineProperty。具体详细细节,可以参考Object static function (opens new window)

/**
 * 创建一个FakeWindow, 把window上不支持改变和删除的属性创建到我们创建的fake window上
 * @param global
 * @returns
 */
function createFakeWindow(global: Window) {
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  const fakeWindow = {} as FakeWindow;

  Object.getOwnPropertyNames(global)
    // 筛选出不可以改变或者可以删除的属性
    .filter((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      return !descriptor?.configurable;
    })
    // 重新定义这些属性可以可以改变和删除
    .forEach((p) => {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      if (descriptor) {
        // 判断有get属性,说明可以获取该属性值
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');

        if (
          p === 'top' ||
          p === 'parent' ||
          p === 'self' ||
          p === 'window'
        ) {
          descriptor.configurable = true;

          if (!hasGetter) {
            descriptor.writable = true;
          }
        }

        if (hasGetter) propertiesWithGetter.set(p, true);
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });

  return {
    fakeWindow,
    propertiesWithGetter, // 记录有get方法的属性
  };
}

前期工作已准备好,接下来我们看沙箱的主要代码

// 全局变量,记录沙箱激活的数量
let activeSandboxCount = 0;

/**
 * 基于 Proxy 实现的沙箱
 */
export default class ProxySandbox implements SandBox {
  /** window 值变更记录 */
  private updatedValueSet = new Set<PropertyKey>();

  name: string; // 名称

  proxy: WindowProxy; // 初始化代理对象

  type: SandBoxType; // 沙箱类型

  sandboxRunning = true; // 沙箱是否在运行

  latestSetProp: PropertyKey | null = null; // 最后设置的props

  active() {
    // 沙箱激活记,记录激活数量
    if (!this.sandboxRunning) activeSandboxCount++;
    this.sandboxRunning = true;
  }

  inactive() {
    // 失活沙箱,减去激活数量
    if (--activeSandboxCount === 0) {
      // 在白名单的属性要从window上删除
      variableWhiteList.forEach((p) => {
        if (this.proxy.hasOwnProperty(p)) {
          delete window[p];
        }
      });
    }
    this.sandboxRunning = false;
  }

  constructor(name: string) {
    this.name = name;
    this.type = SandBoxType.Proxy;
    const { updatedValueSet } = this;
    const rawWindow = window;
    // 通过createFakeWindow创建一个fakeWindow对象
    const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);

    const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
    const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);

    // 代理 fakeWindow
    const proxy = new Proxy(fakeWindow, {
      set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
        if (this.sandboxRunning) {
          // 判断window上有该属性,并获取到属性的 writable, configurable, enumerable等值。
          if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {
            const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
            const { writable, configurable, enumerable } = descriptor!;
            if (writable) {
              // 通过defineProperty把值复制到代理对象上,
              Object.defineProperty(target, p, {
                configurable,
                enumerable,
                writable,
                value,
              });
            }
          } else {
            // window上没有属性,支持设置值
            target[p] = value;
          }

          // 存放一些变量的白名单
          if (variableWhiteList.indexOf(p) !== -1) {
            // @ts-ignore
            rawWindow[p] = value;
          }

          // 记录变更记录
          updatedValueSet.add(p);

          this.latestSetProp = p;

          return true;
        }
        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
        return true;
      },

      get(target: FakeWindow, p: PropertyKey): any {
        if (p === Symbol.unscopables) return unscopables;

        // 判断用window.top, window.parent等也返回代理对象,在ifream环境也会返回代理对象。做到了真正的隔离,
        if (p === 'window' || p === 'self') {
          return proxy;
        }

        if (p === 'globalThis') {
          return proxy;
        }

        if (
          p === 'top' ||
          p === 'parent'
        ) {
          if (rawWindow === rawWindow.parent) {
            return proxy;
          }
          return (rawWindow as any)[p];
        }

        // hasOwnProperty的值表示为rawWindow.hasOwnProperty
        if (p === 'hasOwnProperty') {
          return hasOwnProperty;
        }

        // 如果获取document和eval对象就直接返回,相当月共享一些全局变量
        if (p === 'document' || p === 'eval') {
          setCurrentRunningSandboxProxy(proxy);
          nextTick(() => setCurrentRunningSandboxProxy(null));
          switch (p) {
            case 'document':
              return document;
            case 'eval':
              return eval;
          }
        }

        // 返回当前值
        const value = propertiesWithGetter.has(p)
          ? (rawWindow as any)[p]
          : p in target
          ? (target as any)[p]
          : (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },

      /**
       * 以下这些方法都是在对象的处理上做了很多的兼容,保证沙箱的健壮性和完整性
       */
      has(target: FakeWindow, p: string | number | symbol): boolean {
      },
      getOwnPropertyDescriptor ....
      this.proxy = proxy;
      activeSandboxCount++;
  }
}

整体我们可以看到先创建 fakeWindow 对象,然后对这个对象进行代理,ProxySandbox 不会操作 window 上的实例,会使用 fakeWindow 上的属性,从而实现多实例。 实现代理的过程中还对 as、ownKeys、getOwnPropertyDescriptor、defineProperty、deleteProperty 做了重新定义,会保证沙箱的健壮性和完整性。跟我们上篇文章有点不一样的就是共享对象,qiankun 直接写死了,只有 doucument 和 eval 是共享的

# SapshotSandbox 快照沙箱


/**
 * 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
 */
export default class SnapshotSandbox implements SandBox {
  name: string; // 名称

  proxy: WindowProxy; // 初始化代理对象

  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];
      }
    });

    this.sandboxRunning = false;
  }
}

快照沙箱比较简单,激活的时候对变更的属性做些记录,失活的时候移除这些记录,还有运行期间所有的属性都报存在 window 上,所有只能是单实例。

# 参考链接

  • 说说 JS 中的沙箱 (opens new window)
  • 说说微前端 JS 沙箱实现的几种方式 (opens new window)
  • 乾坤的 Js 隔离机制原理剖析(快照沙箱、两种代理沙箱) (opens new window)
  • json怎么储存函数 (opens new window)
编辑 (opens new window)
上次更新: 2022/06/09, 15:46:09
tree-shaking的原理
思维导图

← tree-shaking的原理 思维导图→

最近更新
01
好玩的docker
07-04
02
我的第一个NFT
07-03
03
海外迅雷
06-01
更多文章>
Power by vuepress | Copyright © 2015-2023 神族九帝
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×