首页天道酬勤Qiankun JS沙箱是怎么做隔离的

Qiankun JS沙箱是怎么做隔离的

admin 10-16 19:32 357次浏览
这篇“Qiankun JS沙箱是怎么做隔离的”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Qiankun JS沙箱是怎么做隔离的”文章吧。

    前言

    当我写 window.a = 1 的时候,a 是怎么被挂载到这些 XXXSandbox 上的呢?又或者我直接云修改 window.a = 123 时,JS 沙箱到底是怎么隔离这个 a 的呢?

    总不能这样吧:

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

    复习一下沙箱

    SanpshotSandbox

    第一种是快照沙箱。

    它的原理是:把主应用的 window 对象做浅拷贝,将 window 的键值对存成一个 Hash Map。之后无论微应用对 window 做任何改动,当要在恢复环境时,把这个 Hash Map 又应用到 window 上就可以了。 大概如下图所示。

    Qiankun JS沙箱是怎么做隔离的

    稍微做下小结:

    • 微应用 mount 时

      • 先把上一次记录的变更 modifyPropsMap 应用到微应用的全局 window,没有则跳过

      • 浅复制主应用的 window key-value 快照,用于下次恢复全局环境

    • 微应用 unmount 时

      • 将当前微应用 window 的 key-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, prevMap 和 newMap 这三个变量就能反推出微应用以及原来环境的变化,qiankun 也能以此作为恢复环境的依据。

    Qiankun JS沙箱是怎么做隔离的

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

    可以发现这里的代码做了三件事:

    • 把要执行 JS 代码放在一个立即执行函数中,且函数入参有 window, self, globalThis

    • 给这个函数 绑定上下文 window.proxy

    • 执行这个函数,并 把上面提到的沙箱对象 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.a 的 window 就不是全局 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 上,所以当执行 add,eval 就会报 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>

    这个问题的解决的方法也很简单:

    • 把代码 a = 1 改成 window.a

    • 添加全局声明 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 JS沙箱是怎么做隔离的”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注花开半夏行业资讯频道。

    Qiankun JS沙箱是怎么做隔离的
    vue项目中main.js使用方法详解 怎么在SpringBoot中使用Spring AOP实现接口鉴权
    相关内容