JavaScript
主页 > 网络编程 > JavaScript >

Qiankun原理之JS沙箱是如何做隔离的介绍

2022-09-29 | 佚名 | 点击:

前言

相信大家也知道 qiankun 有 SnapshotSandbox, LegacySandbox 和 ProxySandbox 这些沙箱,而它们又可以分为单例和多例两种模式,网上也有很多文章对其进行介绍。

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

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

总不能这样吧:

1

2

window = window.sandbox

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

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

复习一下沙箱

这里我们还是稍微复习一下 qiankun 的三大沙箱吧。

SanpshotSandbox

第一种是快照沙箱。

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

稍微做下小结:

LegacySandbox

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

1

2

3

4

5

6

7

8

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

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

ProxySandbox

前面两种沙箱都是 单例模式 下使用的沙箱。也即一个页面中只能同时展示一个微应用,而且无论是 set 还是 get 依然是直接操作 window 对象。

在这样单例模式下,当微应用修改全局变量时依然会在原来的 window 上做修改,因此如果在同一个路由页面下展示多个微应用时,依然会有环境变量污染的问题。

为了避免真实的 window 被污染,qiankun 实现了 ProxySandbox。它的想法是:

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

隔离原理

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

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

1

2

3

4

5

6

const executableScript = `

  ;(function(window, self, globalThis){

    ;${scriptText}${sourceUrl}

  }).bind(window.proxy)(window.proxy, window.proxy, window.proxy);

`

eval.call(window, executableScript)

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

1

2

3

4

5

function fn(window, self, globalThis) {

  // 你的 JavaScript code

}

const bindedFn = fn.bind(window.proxy);

bindedFn(window.proxy, window.proxy, window.proxy);

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

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

1

2

3

4

5

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

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

假如现在代码里有隐式声明或调用全局对象的代码:

1

2

3

4

add = (a, b) => {

  return a + b

}

add(1, 2)

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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

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

这样一来,你就得每次打包代码以及发布时执行一个脚本来做这些文本替换,非常麻烦。而京东的新微应用框架 MicroApp 则提供了一套插件系统:

它可以让开发者在执行 JS 前去做代码文本的替换:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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 种沙箱:

要和这些沙箱结合起来使用,qiankun 会把要执行的 JS 包裹在立即执行函数中,通过绑定上下文和传参的方式来改变 this 和 window 的值,让它们指向 window.proxy 沙箱对象,最后再用 eval 来执行这个函数。

原文链接:https://juejin.cn/post/7148075486403362846
相关文章
最新更新