Qiankun原理详解JS沙箱是如何做隔离
前言
相信大家也知道 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 来执行这个函数。
以上就是Qiankun原理详解JS沙箱是如何做隔离的详细内容,更多关于Qiankun原理JS沙箱隔离的资料请关注猪先飞其它相关文章!
原文出处:https://juejin.cn/post/7148075486403362846
相关文章
- 本篇文章主要分享了通过window.navigator来判断浏览器及其版本信息的实例代码。具有一定的参考价值,下面跟着小编一起来看下吧...2017-01-23
- 这篇文章主要介绍了js如何实现浏览器打印功能,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下...2020-07-15
- 这篇文章主要给大家介绍了关于Nest.js参数校验和自定义返回数据格式的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-28
- 下面小编就为大家带来一篇利用JS实现点击按钮后图片自动切换的简单方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2016-10-25
- 作为前端,一直以来都知道HTTP劫持与XSS跨站脚本、CSRF跨站请求伪造。防御这些劫持最好的方法是从后端入手,前端能做的太少。而且由于源码的暴露,攻击者很容易绕过防御手段。但这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。...2021-05-24
- 这篇文章主要介绍了js实现调用网络摄像头及常见错误处理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-07
- 这篇文章主要为大家详细介绍了JS实现随机生成验证码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-06
- 这篇文章主要介绍了js组件SlotMachine实现图片切换效果制作抽奖系统的相关资料,需要的朋友可以参考下...2016-04-19
- 这篇文章主要介绍了基于JavaScript实现文字超出部分隐藏 的相关资料,需要的朋友可以参考下...2016-03-01
- 这篇文章主要为大家详细介绍了js实现列表按字母排序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-11
- 这篇文章主要介绍了JS实现响应鼠标点击动画渐变弹出层效果代码,具有非常自然流畅的动画过度效果,涉及JavaScript针对鼠标事件的响应及页面元素样式的动态操作相关技巧,需要的朋友可以参考下...2016-03-28
- 本文给大家介绍的是nodejs实现使用阿里大鱼短信API发送消息的方法和代码,有需要的小伙伴可以参考下。...2016-01-20
- Vue.js通过简洁的API提供高效的数据绑定和灵活的组件系统.这篇文章主要介绍了vue.js 表格分页ajax 异步加载数据的相关资料,需要的朋友可以参考下...2016-10-20
- 这次文章要给大家介绍的是node.JS md5加密中文与php结果不一致怎么办,不知道具体解决办法的下面跟小编一起来看看。 因项目需要,需要Node.js与PHP做接口调用,发现nod...2017-07-06
- 这篇文章主要为大家详细介绍了js实现上传图片及时预览的相关资料,具有一定的参考价值,感兴趣的朋友可以参考一下...2016-05-09
- 系统的学习了一下angularjs,发现angularjs的有些思想根php的模块smarty很像,例如数据绑定,filter。如果对smarty比较熟悉的话,学习angularjs会比较容易一点,这篇文章给大家介绍angularjs filter用法详解,感兴趣的朋友一起学习吧...2015-12-29
- 为了网站的安全性,很多朋友都把密码设的比较复杂,但是如何密码不能明显示,不知道输的是对是错,为了安全起见可以把密码显示的,那么基于js代码如何实现的呢?下面通过本文给大家介绍JavaScript实现表单密码的隐藏和显示,需要的朋友参考下...2016-03-03
- 这篇文章主要给大家介绍了一个关于JS正则匹配的踩坑记录,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-04-13
- v-for标签可以用来遍历数组,将数组的每一个值绑定到相应的视图元素中去,下面这篇文章主要给大家介绍了关于在Vue.js中轻松解决v-for执行出错的三个方案,文中通过示例代码介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面来一起看看吧。...2017-06-15
- 使用require('crypto')调用加密模块。加密模块需要底层系统提供OpenSSL的支持。它提供了一种安全凭证的封装方式,可以用于HTTPS安全网络以及普通HTTP连接。该模块还提供了一套针对OpenSSL的hash(哈希),hmac(密钥哈希),cipher...2014-06-07