一些“炒饭”,可以参考W3C标准Workers文档中的相关介绍。自2014年HTML5官方推荐标准发布以来,HTML5加入了越来越强大的特性和功能。其中,WebWorker概念的引入让人眼前一亮,但却没有激起太大的兴趣。并在后续工程端被Angular、Vue、React等框架的“革命性”浪潮所淹没。当然,我们总会偶然看一些文章,或者做一些应用场景的练习以达到学习的目的,甚至在涉及大量数据计算场景的实际项目中使用。但是我相信有很多人和我一样一头雾水,在实际的项目场景中找不到任何可以发挥广泛作用的高科技技术的应用。原因是WebWorker独立于UI主线程运行,使得它考虑了很多性能优化尝试(比如一些图像分析、3D计算和渲染等),从而保证页面是用户-在执行大量计算时友好。能有及时的反应。这些前端侧的性能优化需求一方面涉及到低频,另一方面也可以通过微任务或者服务端处理来解决。无法像WebSocket技术那样优化前端页面下的轮询场景。能带来质的变化。直到2019年流行的微前端架构出现,基于微应用之间JavaScript沙箱隔离的需求,WebWorker才得以从边缘位置再次跳到我的中心视野。根据我对WebWorker的了解,我知道WebWorker是在一个独立的子线程下工作的(虽然这个子线程比Java等编译型语言的子线程要弱一些,比如unabletolocketc.),线程有自己的隔离特性,那么基于这种“物理”隔离,能否实现JavaScript运行时隔离呢?本文接下来将介绍我基于WebWorker实现JavaScript沙箱的探索。一些资料的收集、理解,以及我在boxisolationplan过程中的步调和思考过程。虽然整篇文章的内容可能是“炒冷饭”,但还是希望我探索计划的过程能够对正在阅读本文的你有所帮助。JavaScript沙箱在探索基于WebWorker的解决方案之前,我们首先需要了解当前要解决的问题——JavaScript沙箱。说到沙盒,我首先会想到自己兴趣爱好玩过的沙盒游戏,但是我们要探索的JavaScript沙盒和沙盒游戏是不一样的。沙盒游戏侧重于世界基本元素的抽象与组合、物理力系统实现等,而JavaScript沙盒更侧重于使用共享数据时运行状态的隔离。在现实生活中与JavaScript相关的场景中,我们知道我们通常使用的浏览器是一个沙箱,在浏览器中运行的JavaScript代码无法直接访问文件系统、显示或任何其他硬件。Chrome浏览器中的每个标签页也是一个沙箱,每个标签页中的数据不能直接相互影响,界面运行在独立的上下文中。而在同一个浏览器选项卡下运行HTML页面,有哪些比较详细的场景需要沙箱现象?当我们长期做前端开发的时候,很容易想到在同一个页面下,很多使用沙箱需求的应用场景,比如:当执行从不可信来源获取的第三方JavaScript代码时(比如引入插件、处理jsonp请求返回的数据等)。在线代码编辑场景(比如著名的codesandbox)。使用服务器端渲染。评估模板字符串中的表达式。...这里我们回到开头,假设前提是在我所面对的微前端架构设计下。在微前端架构中(推荐文章微前端的思考,拥抱云时代的前端开发架构-微前端等),最关键的设计是各个子应用的调度实现和运行维护state,whilerunning子应用使用全局事件监听和全局CSS样式生效时,在多个子应用切换时会成为污染副作用。为了解决这些副作用,后来出现的很多微前端架构(比如乾坤)都有各种各样的实现。比如CSS隔离中常见的命名空间前缀、ShadowDOM、动态增删乾坤沙箱css等等,都有具体有效的做法,而这里最头疼的就是微应用之间的JavaScript沙箱隔离。在微前端架构中,JavaScript沙箱隔离需要解决以下问题:清理并恢复子应用切换时挂在窗口上的全局方法/变量(如setTimeout、scrolling等全局事件监听器)。对Cookie、LocalStorage等的读写安全策略限制。对每个子应用实现独立路由。多个微应用共存时的独立实现。在乾坤架构设计中,沙箱的入口文件有两个需要注意,一个是proxySandbox.ts,一个是snapshotSandbox.ts。它们分别在基于Proxy的窗口上实现基于代理的常量和方法,不支持Proxy时降级。通过快照备份和恢复。结合它的相关开源文章分享,简单总结一下它的实现思路:初始版本使用snapshotsandbox的概念,模拟ES6的ProxyAPI,通过proxy劫持window,当子应用修改或使用属性或方法时在window上,put会记录相应的操作,每次挂载/卸载子应用都会生成一个快照。当再次从外部切换到当前子应用时,从记录的快照中恢复。后来为了兼容多个子应用的共存,基于Proxy实现了代理的所有全局常量和方法接口,为每个子应用构建独立的运行环境。另一个值得借鉴的思路是阿里云开发平台的BrowserVM,其核心入口逻辑在Context.js文件中。其具体实现思路如下:借鉴with的实现效果,在webpack编译打包阶段为每个子应用代码包裹一层代码(参见插件包breezr-plugin-os下的相关文件)),创建闭包,通过Import自己模拟的window、document、location、history等全局对象(见根目录相关文件)。在模拟的Context中,提供了一个新的iframe对象,并提供了一个与宿主应用同域的空(about:blank)URL作为iframe的初始加载URL(空URL不会加载资源,但会生成与iframe相同的domain关联的history不可操作,此时路由转换只支持hash方式),然后通过contentWindow取出其下的nativebrowser对象(因为iframe对象是天然隔离的),这里省去了自己Mock实现所有API的成本)。取出对应iframe中的原始对象后,继续为需要隔离的具体对象生成对应的Proxy,然后对一些属性的获取和属性设置做一些具体的实现(比如window.document需要返回一个特定的沙箱文档而不是当前浏览器的文档等)。为了让文档的内容加载到同一棵DOM树上,对于文档来说,DOM操作的大部分属性和方法仍然直接由宿主浏览器中文档的属性和方法处理。总的来说,在BrowserVM的实现上,可以看出实现部分还是借鉴了乾坤或者其他微前端架构的思想,比如通用全局对象的代理和拦截。并且借助Proxy特性,对于Cookie和LocalStorage的读写也可以实现一些安全策略。但它最大的亮点是借助iframe的一些巧妙实现。当为每个子应用创建的iframe被移走时,它下面写在window上的变量,setTimeout,全局事件监听等也会被移走。此外;基于Proxy,将DOM事件记录在沙箱中,然后在宿主的生命周期中移除,可以以较小的开发成本实现整个JavaScript沙箱隔离机制。除了上述社区比较流行的解决方案外,我最近还在探索大型Web应用的插件架构一文中了解到,UI设计领域的Figma产品也基于其插件产生了隔离方案-在系统中。起初,Figma也将插件代码放入iframe中执行,并通过postMessage与主线程通信,但由于易用性以及postMessage序列化带来的性能问题,Figma选择将插件放入执行的主线程。Figma采用的解决方案是基于尚处于草案阶段的RealmAPI,将JavaScript解释器的C++实现Duktape编译成WebAssembly,然后嵌入到Realm上下文中,实现对其产品下的三方插件。这个方案和探索过的基于WebWorker的实现或许能更好的结合,我们会持续关注。WebWorker与DOM渲染了解了JavaScript沙箱的“前世今生”后,我们将目光转回本文的主角——WebWorker。正如本文开头提到的,WebWorker子线程的形式也是一种天然的沙箱隔离。理想的方式是在编译阶段使用Webpack插件为每个子应用包裹一层Worker对象。代码,让子应用运行在其对应的单个Worker实例中,例如:__WRAP_WORKER__(`/*packagecode*/}`);function__WRAP_WORKER__(appCode){varblob=newBlob([appCode]);varappWorker=newWorker(window.URL.createObjectURL(blob));}但是在了解了微前端下JavaScript沙箱的实现过程之后,我们不难发现Web下不可避免会遇到微前端场景的几种JavaScript沙箱Worker难点:出于线程安全设计考虑,WebWorker不支持DOM操作,必须通过postMessage通知UI主线程来实现。WebWorker无法访问浏览器全局对象,例如window和document。其他的问题,比如WebWorker无法访问页面全局变量和函数,无法调用alert、confirm等BOMAPI,相对于无法访问window和document全局对象来说,都是小问题。不过好在WebWorker中可以正常使用setTimeout、setInterval等定时器函数,依然可以发送ajax请求。所以,首先要解决的问题就是在单个WebWorker实例中进行DOM操作的问题。首先,我们有一个大前提:DOM不能在WebWorker中渲染,所以我们需要根据实际应用场景拆分DO??M操作。ReactWorkerDOM由于我们微前端架构中的子应用仅限于React技术栈,所以我首先将目光投向了基于React框架的解决方案。在React中,我们知道一个基本事实,它将渲染阶段分为两个阶段:DiffDOM树的变化和实际渲染和改变页面的DOM。是否可以将Diff进程放在WebWorker中,然后渲染阶段可以通过postMessage与主线程通信后,放在主线程中呢?简单的搜索一下就相当尴尬了,5、6年前就已经有大佬试过了。这里可以参考react-worker-dom的开源代码。react-worker-dom中的实现思路非常清晰。在common/channel.js中,统一封装了子线程与主线程通信的接口和通信数据序列化的接口。然后我们可以看到Worker下进行DOM逻辑处理的通用入口文件在worker目录下。从入口文件可以看出,它实现了计算DOM后通过postMessage通知主线程渲染的入口文件WorkerBridge.js,以及基于DOM构建、Diff操作、生命周期Mock接口等相关代码反应库。接受渲染事件通信的入口文件在页面目录中。入口文件接受节点操作事件,然后结合WorkerDomNodeImpl.js中的接口代码,实现DOM在主线程上的实际渲染更新。简单做个总结。基于React技术栈,通过在WebWorker下分离Diff和渲染阶段,可以实现一定程度的DOM沙箱,但这并不是我们想要的微前端架构下的JavaScript沙箱。先不说拆分Diff阶段和渲染阶段的性价比。首先,基于技术栈框架的特殊性所做的诸多努力,随着框架本身版本的升级,会带来难以控制的维护升级问题;其次,如果各个子应用使用的技术栈框架不同,需要分别封装这些不同框架适配的接口,扩展性和通用性较弱;最后,最重要的一点,这个方法仍然没有解决window下的资源共享问题,或者说,只是开始了解决这个问题的第一步。接下来我们继续讨论在Worker下实现DOM操作的另一种方案。关于windows下的资源共享问题,我们稍后再讨论。AMPWorkerDOM当我开始纠结很多其实是用react-worker-dom的思路开发出来的“天然护城河”问题时,浏览了其他的DOM框架,因为它们也有插件机制,一不小心就蹦了进去我脑海。它是谷歌的AMP。在AMP开源项目中,除了amphtml等通用的web组件框架外,还有很多使用了ShadowDOM、WebComponent等新技术的项目。在项目下简单浏览后,我很高兴地看到了项目工作人员。粗略看一下worker-dom的源码,我们可以看到src根目录下有main-thread和worker-thread两个目录。打开一看,我们发现实现拆分DOM和DOM渲染逻辑的思路和上面的react-worker-dom基本类似,但是因为worker-dom和上层没有关系framework,它的实现更接近DOM的底层。先看worker-threadDOM逻辑层的相关代码。可以看到在其下面的dom目录下实现了所有相关的节点元素、属性接口、文档对象等基于DOM标准的代码。canvas也是在上层目录实现的。,CSS,Events,Storage等全局属性和方法。然后看主线程。一方面,它的关键功能是提供一个接口,用于加载工作文件以从主线程渲染页面。另一方面,从worker.ts和nodes.ts这两个文件的代码可以理解。在worker.ts中,如我最初想象的那样包裹了一层代码,用于自动生成Worker对象,代码中的所有DOM操作都委托给了模拟的WorkerDOM对象:constcode=`'usestrict';(function(){${workerDOMScript}self['window']=self;varworkerDOM=WorkerThread.workerDOM;WorkerThread.hydrate(workerDOM.document,${JSON.stringify(strings)},${JSON.stringify(骨架)},${JSON.stringify(cssKeys)},${JSON.stringify(globalEventHandlerKeys)},[${window.innerWidth},${window.innerHeight}],${JSON.stringify(localStorageInit)},${JSON.stringify(sessionStorageInit)});workerDOM.document[${TransferrableKeys.observe}](this);Object.keys(workerDOM).forEach(function(k){self[k]=workerDOM[k]});})。call(self);${authorScript}//#sourceURL=${encodeURI(config.authorURL)}`;这个[TransferrableKeys.worker]=newWorker(URL.createObjectURL(newBlob([code])));在nodes.ts中,实现了真实元素节点的构建和存储(根据渲染阶段是否以及如何优化存储数据结构,需要进一步研究源码)。同时,transfer目录下的源码定义了逻辑层和UI渲染层之间的消息通信规范。总的来说,AMPWorkerDOM方案摒弃了上层框架的束缚,通过底层构建DOM的所有相关API,真正实现了框架技术栈的独立。一方面可以作为上层框架的底层实现,支持各种上层框架(如项目amp-react-prototype)的二次封装和迁移。另一方面结合目前主流的JavaScript沙箱方案,模拟window和document全局方法和代理到主线程的方式,实现部分JavaScript沙箱隔离(暂时没看到路由隔离的相关代码实现)).当然,从我个人的角度来看,AMPWorkerDOM在目前的实现上也有一定的局限性。一是目前主流的上层框架如Vue、React等的迁移成本和社区生态的适配成本,二是单页应用下还没有相关的实施方案。在支持大型PC微前端应用方面一直没有找到更好的解决方案。其实在了解了AMPWorkerDOM的实现方案后,后续基于react-worker-dom思想的方案也可以有一个大方向:渲染通信的后续过程可以结合相关的考虑实现BrowserVM以生成Worker对象。同时也生成了一个iframe对象,然后将DOM下的操作通过postMessage发送到主线程,再和绑定的iframe一起执行。同时将具体的渲染实现通过代理转发给原来的WorkerDomNodeImpl.js逻辑,实现DOM的实际更新。总结和个人的一些展望首先说一下个人的一些总结。WebWorker下微前端架构下的JavaScript沙箱的实现,最初是基于个人的一闪而过的灵感。经过深入研究,虽然最终由于种种问题无法找到实施方案的最优方案,因此放弃了社区通用应用。不过,仍然不妨碍我继续看好WebWorker技术在实现插件沙箱应用方面的表现。插件机制一直是前端领域津津乐道的设计。从Webpack编译工具到IDE开发工具,从Web应用级实体插件到应用架构设计中的插件扩展设计,结合WebAssembly技术,WebWorker无疑将在插件设计中扮演举足轻重的角色。二是一些人的前瞻性思考。其实从WebWorker实现DOM渲染的研究过程可以看出,基于逻辑和UI分离的思想,前端后续的架构设计很有可能产生一定的变化。目前无论是流行的Vue还是React框架,无论其框架设计是MVVM还是Flux结合Redux,其本质仍然是View层驱动的框架设计(个人观点),具有灵活性和性能优化,难度大在大型项目层级升级后的协同开发中,基于WebWorker的逻辑与UI分离将推动数据采集、处理、消费全流程的业务进一步分层,从而固化一整套MVX设计思路.当然,以上我还处于初步研究阶段,不成熟的地方还需要多思考。听听,以后再练习。
