乐趣区

关于前端:解密微前端从qiankun看沙箱隔离

在我之前的文章提到过,微前端的实质是分治的解决前端利用以及利用间的关系,那么更进一步,落地一个微前端框架,就会波及到三点外围因素:

  • 子利用的加载;
  • 利用间运行时隔离
  • 路由劫持;

对于 qiankun 来说,路由劫持是在 single-spa 下来做的,而 qiankun 给咱们提供的能力,次要便是子利用的加载和沙箱隔离。

承接上文,这是系列的第二个 topic,这篇文章次要基于 qiankun 源码向大家讲一下沙箱隔离如何实现。

qiankun 做沙箱隔离次要分为三种:

  • legacySandBox
  • proxySandBox
  • snapshotSandBox。

其中 legacySandBox、proxySandBox 是基于 Proxy API 来实现的,在不反对 Proxy API 的低版本浏览器中,会降级为 snapshotSandBox。在现版本中,legacySandBox 仅用于 singular 单实例模式,而多实例模式会应用 proxySandBox。

legacySandBox

legacySandBox 的核心思想是什么呢?legacySandBox 的实质上还是操作 window 对象,然而他会存在三个状态池,别离用于子利用卸载时还原主利用的状态和子利用加载时还原子利用的状态

  • addedPropsMapInSandbox:存储在子利用运行时期间 新增的全局变量 ,用于卸载子利用时 还原主利用 全局变量;
  • modifiedPropsOriginalValueMapInSandbox:存储在子利用运行期间 更新的全局变量 ,用于卸载子利用时 还原主利用 全局变量;
  • currentUpdatedPropsValueMap:存储子利用全局变量的更新,用于运行时切换后 还原子利用 的状态;

咱们首先看下 Proxy 的 getter / setter:

const rawWindow = window;
const fakeWindow = Object.create(null) as Window;
// 创立对 fakeWindow 的劫持,fakeWindow 就是咱们传递给自执行函数的 window 对象
const proxy = new Proxy(fakeWindow, {set(_: Window, p: PropertyKey, value: any): boolean {
    // 运行时的判断
    if (sandboxRunning) {
      // 如果 window 对象上没有这个属性,那么就在状态池中记录状态的新增;if (!rawWindow.hasOwnProperty(p)) {addedPropsMapInSandbox.set(p, value);

        // 如果以后 window 对象存在该属性,并且状态池中没有该对象,那么证实改属性是运行时期间更新的值,记录在状态池中用于最初 window 对象的还原
      } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {const originalValue = (rawWindow as any)[p];
        modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
      }

      // 记录全局对象批改值,用于后体面利用激活时还原子利用
      currentUpdatedPropsValueMap.set(p, value);
      (rawWindow as any)[p] = value;

      return true;
    }

    return true;
  },

  get(_: Window, p: PropertyKey): any {
    // iframe 的 window 上下文
    if (p === "top" || p === "window" || p === "self") {return proxy;}

    const value = (rawWindow as any)[p];
    return getTargetValue(rawWindow, value);
  },
});

接下来看下子利用沙箱的激活 / 卸载:

  // 子利用沙箱激活
  active() {
    // 通过状态池,还原子利用上一次写在前的状态
    if (!this.sandboxRunning) {this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
    }

    this.sandboxRunning = true;
  }

  // 子利用沙箱卸载
  inactive() {
    // 还原运行时期间批改的全局变量
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
    // 删除运行时期间新增的全局变量
    this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));

    this.sandboxRunning = false;
  }

所以,总结起来,legacySandBox 还是会操作 window 对象,然而他通过激活沙箱时还原子利用的状态,卸载时还原主利用的状态来实现沙箱隔离的

proxySandBox

在 qiankun 中,proxySandBox 用于多实例场景。什么是多实例场景,这里我简略提下,个别咱们的中后盾零碎同一时间只会加载一个子利用的运行时。然而也存在这样的场景,某一个子利用聚合了多个业务域,这样的子利用往往会经验多个团队的多个同学独特保护本人的业务模块,这时候便能够采纳多实例的模式聚合子模块(这种模式也能够叫微前端模块)。

回到正题,和 legacySandBox 最间接的不同点就是,为了反对多实例的场景,proxySandBox 不会间接操作 window 对象。并且为了防止子利用操作或者批改主利用上诸如 window、document、location 这些重要的属性,会遍历这些属性到子利用 window 正本(fakeWindow)上,咱们首先看下创立子利用 window 的正本:

function createFakeWindow(global: Window) {// 这里 qiankun 给咱们了一个知识点:在 has 和 check 的场景下,map 有着更好的性能 :)
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  const fakeWindow = {} as FakeWindow;

  // 从 window 对象拷贝不可配置的属性
  // 举个例子:window、document、location 这些都是挂在 Window 上的属性,他们都是不可配置的
  // 拷贝进去到 fakeWindow 上,就间接防止了子利用间接操作全局对象上的这些属性办法
  Object.getOwnPropertyNames(global)
    .filter((p) => {const descriptor = Object.getOwnPropertyDescriptor(global, p);
      // 如果属性不存在或者属性描述符的 configurable 的话
      return !descriptor?.configurable;
    })
    .forEach((p) => {const descriptor = Object.getOwnPropertyDescriptor(global, p);
      if (descriptor) {
        // 判断以后的属性是否有 getter
        const hasGetter = Object.prototype.hasOwnProperty.call(
          descriptor,
          "get"
        );

        // 为有 getter 的属性设置查问索引
        if (hasGetter) propertiesWithGetter.set(p, true);

        // freeze the descriptor to avoid being modified by zone.js
        // zone.js will overwrite Object.defineProperty
        // const rawObjectDefineProperty = Object.defineProperty;
        // 拷贝属性到 fakeWindow 对象上
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });

  return {
    fakeWindow,
    propertiesWithGetter,
  };
}

接下来看下 proxySandBox 的 getter/setter:

const rawWindow = window;
// window 正本和下面说的有 getter 的属性的索引
const {fakeWindow, propertiesWithGetter} = createFakeWindow(rawWindow);

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

const proxy = new Proxy(fakeWindow, {set(target: FakeWindow, p: PropertyKey, value: any): boolean {if (sandboxRunning) {
      // 在 fakeWindow 上设置属性值
      target[p] = value;
      // 记录属性值的变更
      updatedValueSet.add(p);

      // SystemJS 属性拦截器
      interceptSystemJsProps(p, value);

      return true;
    }

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

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

    // 防止 window.window 或 window.self 或 window.top 穿透 sandbox
    if (p === "top" || p === "window" || p === "self") {return proxy;}

    if (p === "hasOwnProperty") {return hasOwnProperty;}

    // 批处理场景下会有场景应用,这里就不多赘述了
    const proxyPropertyGetter = getProxyPropertyGetter(proxy, p);
    if (proxyPropertyGetter) {return getProxyPropertyValue(proxyPropertyGetter);
    }

    // 取值
    const value = propertiesWithGetter.has(p)
      ? (rawWindow as any)[p]
      : (target as any)[p] || (rawWindow as any)[p];
    return getTargetValue(rawWindow, value);
  },

  // 还有一些对属性做操作的代码我就不一一列举了,能够自行查阅源码
});

接下来看下 proxySandBox 的 激活 / 卸载:

  active() {
    this.sandboxRunning = true;
    // 以后激活的子利用沙箱实例数量
    activeSandboxCount++;
  }

  inactive() {clearSystemJsProps(this.proxy, --activeSandboxCount === 0);

    this.sandboxRunning = false;
  }

可见,因为 proxySandBox 不间接操作 window,所以在激活和卸载的时候也不须要操作状态池更新 / 还原奴才利用的状态了。相比拟看来,proxySandBox 是现阶段 qiankun 中最齐备的沙箱模式,齐全隔离了奴才利用的状态,不会像 legacySandBox 模式下在运行时期间依然会净化 window。

snapshotSandBox

最初一种沙箱就是 snapshotSandBox,在不反对 Proxy 的场景下会降级为 snapshotSandBox,如同他的名字一样,snapshotSandBox 的原理就是在子利用激活 / 卸载时别离去通过快照的模式记录 / 还原状态来实现沙箱的。

源码很简略,间接看源码:

  active() {if (this.sandboxRunning) {return;}


    this.windowSnapshot = {} as Window;
    // iter 办法就是遍历指标对象的属性而后别离执行回调函数
    // 记录以后快照
    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 和记录的快照做 diff 来实现沙箱。

css 隔离

这其实是个惨重的话题,从我做微前端到当初对于 css 的解决也没有太好的方法,这里我间接总结了两种目前我的项目中应用的计划大家能够参考。

约定式编程

这里咱们能够采纳肯定的编程束缚:

  • 尽量不要应用可能抵触全局的 class 或者间接为标签定义款式;
  • 定义惟一的 class 前缀,当初的我的项目都是用诸如 antd 这样的组件库,这类组件库都反对自定义组件 class 前缀;
  • 主利用肯定要有自定义的 class 前缀;

css in js

这种形式其实有待商讨,因为齐全的 css in js 尽管肯定会实现 css 隔离,然而其实这样的编程写法不利于咱们前期的我的项目保护并且也比拟难去抽离一些公共 css。

举荐浏览

  • 《qiankun 官网文档》
  • 《解密微前端:” 巨石利用 ” 的诞生》
  • 《解密微前端:从 qiankun 看子利用加载》
退出移动版