本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

前言

哈喽,咱们好,我是海怪。

相信咱们也知道 qiankun 有 SnapshotSandbox, LegacySandboxProxySandbox 这些沙箱,而它们又能够分为单例和多例两种形式,网上也有很多文章对其进行介绍。

但这些文章的关注点都是沙箱的环境康复做的事,那 JS 的阻隔到底是怎样做到的呢?

换个问法,当我写 window.a = 1 的时分,a 是怎样被挂载到这些 XXXSandbox 上的呢?又或许我直接云修正 window.a = 123 时,JS 沙箱到底是怎样阻隔这个 a 的呢?

总不能这样吧:

window = window.sandbox
window.a = 1 // window.sandbox.a = 1

这篇文章就来简略聊聊 qiankun 沙箱那些事。

温习一下沙箱

这儿咱们仍是略微温习一下 qiankun 的三大沙箱吧。

SanpshotSandbox

第一种是快照沙箱。

它的原理是:把主运用的 window 目标做浅复制,将 window 的键值对存成一个 Hash Map。之后不管微运用对 window 做任何改动,当要在康复环境时,把这个 Hash Map 又运用到 window 上就能够了。 大约如下图所示。

Qiankun原理——JS沙箱是怎样做阻隔的

略微做下小结:

  • 微运用 mount 时
    • 先把上一次记载的改动 modifyPropsMap 运用到微运用的大局 window,没有则越过
    • 浅复制主运用的 window key-value 快照,用于下次康复大局环境
  • 微运用 unmount 时
    • 将当时微运用 windowkey-value快照key-value 进行 Diff,Diff 出来的成果用于下次康复微运用环境的根据
    • 将前次快照的 key-value 复制到主运用的 window 上,以此康复环境

LegacySandbox

上面的 SnapshotSandbox 有一个问题:每次微运用 unmount 时都要对每个特点值做一次 Diff,类似这样:

for (const prop in window) {
  if (window[prop] !== this.windowSnapshot[prop]) {
    // 记载微运用的改动
    this.modifyPropsMap[prop] = window[prop];
    // 康复主运用的环境
    window[prop] = this.windowSnapshot[prop];
  }
}

假设有 1000 个特点就要比照 1000 次,不是那么高雅。

LegacySandbox 的主意则是 经过监听对 window 的修正来直接记载 Diff 内容,因为只要对 window 特点进行设置,那么就会有两种情况:

  • 假设是新增特点,那么存到 addedMap
  • 假设是更新特点,那么把原来的键值存到 prevMap,把新的键值存到 newMap

(当然这儿的变量名做了简化)

经过 addedMap, prevMapnewMap 这三个变量就能反推出微运用以及原来环境的变化,qiankun 也能以此作为康复环境的根据。

Qiankun原理——JS沙箱是怎样做阻隔的

当然这儿的监听用到了 ES6 的新语法 Proxy,不过这儿先不展开讨论,在之后的系列文章上会会自己手动实现一个简略的沙箱。

ProxySandbox

前面两种沙箱都是 单例形式 下运用的沙箱。也即一个页面中只能一起展现一个微运用,并且不管是 set 仍是 get 依然是直接操作 window 目标。

在这样单例形式下,当微运用修正大局变量时依然会在原来的 window 上做修正,因而假设在同一个路由页面下展现多个微运用时,依然会有环境变量污染的问题。

为了防止实在的 window 被污染,qiankun 实现了 ProxySandbox。它的主意是:

  • 把当时 window 的一些原生特点(如document, location等)复制出来,单独放在一个目标上,这个目标也称为 fakeWindow
  • 之后对每个微运用分配一个 fakeWindow
  • 当微运用修正大局变量时:
    • 假设是原生特点,则修正大局的 window
    • 假设不是原生特点,则修正 fakeWindow 里的内容
  • 微运用获取大局变量时:
    • 假设是原生特点,则从 window 里拿
    • 假设不是原生特点,则优先从 fakeWindow 里获取

这样一来连康复环境都不需要了,因为每个微运用都有自己一个环境,当在 active 时就给这个微运用分配一个 fakeWindow,当 inactive 时就把这个 fakeWindow 存起来,以便之后再利用。

Qiankun原理——JS沙箱是怎样做阻隔的

阻隔原理

看完上面,你大约也知道了这些沙箱是怎样康复环境的 但是,回到咱们的问题:qiankun 是怎样把 a 和这些沙箱联系起来呢?也即写下 window.a = 1 是怎样做到对 a 变量阻隔的呢?

这个逻辑的实现并不在 qiankun 的源码里,而是在它所依靠的 import-html-entry 中,这儿做一下简化:

const executableScript = `
  ;(function(window, self, globalThis){
    ;${scriptText}${sourceUrl}
  }).bind(window.proxy)(window.proxy, window.proxy, window.proxy);
`
eval.call(window, executableScript)

把上面字符串代码展开来看看:

function fn(window, self, globalThis) {
  // 你的 JavaScript code
}
const bindedFn = fn.bind(window.proxy);
bindedFn(window.proxy, window.proxy, window.proxy);

能够发现这儿的代码做了三件事:

  1. 把要履行 JS 代码放在一个立即履行函数中,且函数入参有 window, self, globalThis
  2. 给这个函数 绑定上下文 window.proxy
  3. 履行这个函数,并 把上面提到的沙箱目标 window.proxy 作为入参别离传入

因而,当咱们在 JS 文件里有 window.a = 1 时,实际上会变成:

function fn(window, self, globalThis) {
  window.a = 1;
}
const bindedFn = fn.bind(window.proxy);
bindedFn(window.proxy, window.proxy, window.proxy);

那么此时,window.awindow 就不是大局 window 而是 fn 的入参 window 了。又因为咱们把 window.proxy 作为入参传入,所以 window.a 实际上为 window.proxy.a = 1。这也正好解说了 qiankun 的 JS 阻隔逻辑。

XXX is undefined

不知道看完上面的实现,你有没有发现问题。

假设现在代码里有隐式声明或调用大局目标的代码:

add = (a, b) => {
  return a + b
}
add(1, 2)

当这样调用 add 时,上下文 this 则为刚刚绑定的 window.proxy。因为隐式声明 add 不会自动挂载到 window.proxy 上,所以当履行 addeval 就会报 add is undefined。详见 这个 Issue。

不要觉得这种情况不会发生,实际上,这仍是挺常见的:

  • 老旧的第三方 SDK JS 文件
  • Webpack 插件引入的 JS
  • 公司网关层自动注入的 JS
  • 等等…

我之前就遇到过这种情况:比方下面 Webpack 会注入脚手架定义好的 CDN 资源重试逻辑:

<script>
  var __JS_RETRY__ = {};
  function __rpReport(data) {
    console.log('__rpReport');
  }
  function __rpJsReport(loadType, msidType, url) {
    console.log('__rpJsReport');
  }
  function __retryPlugin(event) {
    console.log('retryPlugin')
  }
  // 改成下面就能够了
  // window.__JS_RETRY__ = {};
  //
  // window.__rpReport = (data) => {
  //     console.log('__rpReport');
  // }
  //
  // window.__rpJsReport = (loadType, msidType, url) => {
  //     console.log('__rpJsReport');
  // }
  //
  // window.__retryPlugin = (event) => {
  //     console.log('retryPlugin')
  // }
</script>

这个问题的处理的办法也很简略:

  1. 把代码 a = 1 改成 window.a
  2. 增加大局声明 window a

这样一来,你就得每次打包代码以及发布时履行一个脚本来做这些文本替换,十分麻烦。而京东的新微运用结构 MicroApp 则供给了一套插件系统:

Qiankun原理——JS沙箱是怎样做阻隔的

它能够让开发者在履行 JS 前去做代码文本的替换:

import microApp from '@micro-zoe/micro-app'
microApp.start({
  plugins: {
    // ...
    modules: {
      'appName1': [{
        loader(code, url, options) {
          if (url === 'xxx.js') {
            // 替换有问题的代码
            code = code.replace('var abc =', 'window.abc =')
          }
          return code
        }
      }],
    }
  }
})

假设要对接别的团队的微运用时,并且正好他们有 a = 1 这样的代码,那么在加载微运用的时分直接修复大局变量的问题,不需要告诉他们修正,也不失为一种战略吧。

总结

总结一下,qiankun 一共有 3 种沙箱:

  1. SnapshotSandbox:记载 window 目标,每次 unmount 都要和微运用的环境进行 Diff
  2. LegacySandbox:在微运用修正 window.xxx 时直接记载 Diff,将其用于环境康复
  3. ProxySandbox:为每个微运用分配一个 fakeWindow,当微运用操作 window 时,其实是在 fakeWindow 上操作

要和这些沙箱结合起来运用,qiankun 会把要履行的 JS 包裹在立即履行函数中,经过绑定上下文和传参的方法来改动 thiswindow 的值,让它们指向 window.proxy 沙箱目标,最后再用 eval 来履行这个函数。