本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
前言
哈喽,咱们好,我是海怪。
相信咱们也知道 qiankun 有 SnapshotSandbox
, LegacySandbox
和 ProxySandbox
这些沙箱,而它们又能够分为单例和多例两种形式,网上也有很多文章对其进行介绍。
但这些文章的关注点都是沙箱的环境康复做的事,那 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
上就能够了。 大约如下图所示。
略微做下小结:
- 微运用 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 也能以此作为康复环境的根据。
当然这儿的监听用到了 ES6 的新语法 Proxy
,不过这儿先不展开讨论,在之后的系列文章上会会自己手动实现一个简略的沙箱。
ProxySandbox
前面两种沙箱都是 单例形式 下运用的沙箱。也即一个页面中只能一起展现一个微运用,并且不管是 set
仍是 get
依然是直接操作 window
目标。
在这样单例形式下,当微运用修正大局变量时依然会在原来的 window
上做修正,因而假设在同一个路由页面下展现多个微运用时,依然会有环境变量污染的问题。
为了防止实在的 window
被污染,qiankun 实现了 ProxySandbox
。它的主意是:
- 把当时
window
的一些原生特点(如document
,location
等)复制出来,单独放在一个目标上,这个目标也称为fakeWindow
- 之后对每个微运用分配一个
fakeWindow
- 当微运用修正大局变量时:
- 假设是原生特点,则修正大局的
window
- 假设不是原生特点,则修正
fakeWindow
里的内容
- 假设是原生特点,则修正大局的
- 微运用获取大局变量时:
- 假设是原生特点,则从
window
里拿 - 假设不是原生特点,则优先从
fakeWindow
里获取
- 假设是原生特点,则从
这样一来连康复环境都不需要了,因为每个微运用都有自己一个环境,当在 active
时就给这个微运用分配一个 fakeWindow
,当 inactive
时就把这个 fakeWindow
存起来,以便之后再利用。
阻隔原理
看完上面,你大约也知道了这些沙箱是怎样康复环境的 但是,回到咱们的问题: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 则供给了一套插件系统:
它能够让开发者在履行 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 种沙箱:
-
SnapshotSandbox
:记载window
目标,每次 unmount 都要和微运用的环境进行 Diff -
LegacySandbox
:在微运用修正window.xxx
时直接记载 Diff,将其用于环境康复 -
ProxySandbox
:为每个微运用分配一个fakeWindow
,当微运用操作window
时,其实是在fakeWindow
上操作
要和这些沙箱结合起来运用,qiankun 会把要履行的 JS 包裹在立即履行函数中,经过绑定上下文和传参的方法来改动 this
和 window
的值,让它们指向 window.proxy
沙箱目标,最后再用 eval
来履行这个函数。