共计 7687 个字符,预计需要花费 20 分钟才能阅读完成。
一些「炒冷饭」的背景介绍
本文并不会从头开始介绍 Web Worker 的基础知识和根本 API 的应用等(只是局部有波及),若还未理解过 Web Worker,可参考查阅 W3C 规范 Workers 文档 中的相干介绍。
自从 2014 年 HTML5 正式举荐规范公布以来,HTML5 减少了越来越多弱小的个性和性能,而在这其中,工作线程(Web Worker)概念的推出让人眼前一亮,但未曾随之激发多大的浪花,并被在其随后工程侧的 Angular、Vue、React 等框架的「反动」浪潮所吞没。当然,咱们总会偶尔看过一些文章介绍,或出于学习的目标做过一些利用场景下的练习,甚或在理论我的项目中的波及大量数据计算场景中真的应用过。但置信也有很多人和我一样茫然,找不到这种高大上的技术在理论我的项目场景中能有哪些能起到宽泛作用的利用。
究其原因,Web Worker 独立于 UI 主线程运行的个性使其被大量思考进行性能优化方面的尝试(比方一些图像剖析、3D 计算绘制等场景),以保障在进行大量计算的同时,页面对用户能有及时的响应。而这些性能优化的需要在前端侧一方面波及频率低,另一方面也能通过微工作或服务端侧解决来解决,它并不能像 Web Socket 这种技术为前端页面下的轮询场景的优化能带来质的扭转。
直至 2019 年爆火的微前端架构的呈现,基于微利用间 JavaScript 沙箱隔离的需要,Web Worker 才得以从新从边缘化的地位跃入到我的核心视线。依据我曾经理解到的 Web Worker 的相干常识,我晓得了 Web Worker 是工作在一个独立子线程下(尽管这个子线程比起 Java 等编译型语言的子线程实现得还有点弱,如无奈加锁等),线程之间自带隔离的个性,那基于这种「物理」性的隔离,能不能实现 JavaScript 运行时的隔离呢?
本文接下来的内容,将介绍我在摸索基于 Web Worker 实现 JavaScript 沙箱隔离计划过程中的一些材料收集、了解以及我的踩坑和思考的过程。尽管可能整篇文章内容都在「炒冷饭」,但还是心愿我的摸索计划的过程能对正在看这篇文章的你有所帮忙。
JavaScript 沙箱
在摸索基于 Web Worker 的解决方案之前,咱们先要对以后要解决的问题——JavaScript 沙箱有所理解。
提到沙箱,我会先想到出于趣味玩过的沙盒游戏,但咱们要摸索的 JavaScript 沙箱不同于沙盒游戏,沙盒游戏重视对世界根本元素的形象、组合以及物理力零碎的实现等,而 JavaScript 沙箱则更重视在应用共享数据时对操作状态的隔离。
在事实与 JavaScript 相干的场景中,咱们晓得平时应用的浏览器就是一个沙箱,运行在浏览器中的 JavaScript 代码无奈间接拜访文件系统、显示器或其余任何硬件。Chrome 浏览器中每个标签页也是一个沙箱,各个标签页内的数据无奈间接相互影响,接口都在独立的上下文中运行。而在同一个浏览器标签页下运行 HTML 页面,有哪些更细节的、对沙箱景象有需要的场景呢?
当咱们作为前端开发人员较长一段时间后,咱们很轻易地就能想到在同一个页面下,应用沙箱需要的诸多利用场景,譬如:
-
执行从不受信的源获取到的第三方 JavaScript 代码时(比方引入插件、解决 jsonp 申请回来的数据等)。
-
在线代码编辑器场景(比方驰名的 codesandbox)。
-
应用服务端渲染计划。
-
模板字符串中的表达式的计算。
-
… …
这里咱们先回到结尾,先将前提假如在我正在面对的微前端架构设计下。在微前端架构(举荐文章 Thinking in Microfrontend、拥抱云时代的前端开发架构——微前端 等)中,其最要害的一个设计便是各个子利用间的调度实现以及其运行态的保护,而运行时各子利用应用全局事件监听、使全局 CSS 款式失效等常见的需要在多个子利用切换时便会成为一种污染性的副作用,为了解决这些副作用,起初呈现的很多微前端架构(如 乾坤)有着各种各样的实现。譬如 CSS 隔离中常见的命名空间前缀、Shadow DOM、乾坤 sandbox css 的运行时动静增删等,都有着的确卓有成效的具体实际,而这里最麻烦辣手的,还是微利用间的 JavaScript 的沙箱隔离。
在微前端架构中,JavaScript 沙箱隔离须要解决如下几个问题:
-
挂在 window 上的全局办法 / 变量(如 setTimeout、滚动等全局事件监听等)在子利用切换时的清理和还原。
-
Cookie、LocalStorage 等的读写安全策略限度。
-
各子利用独立路由的实现。
-
多个微利用共存时互相独立的实现。
在 乾坤 架构设计中,对于沙箱有两个入口文件须要关注,一个是 proxySandbox.ts,另一个是 snapshotSandbox.ts,他们别离基于 Proxy 实现代理了 window 上罕用的常量和办法以及不反对 Proxy 时降级通过快照实现备份还原。联合其相干开源文章分享,简略总结下其实现思路:起初版本应用了 快照沙箱 的概念,模仿 ES6 的 Proxy API,通过代理劫持 window,当子利用批改或应用 window 上的属性或办法时,把对应的操作记录下来,每次子利用挂载 / 卸载时生成快照,当再次从内部切换到以后子利用时,再从记录的快照中复原,而起初为了兼容多个子利用共存的状况,又基于 Proxy 实现了代理所有全局性的常量和办法接口,为每个子利用结构了独立的运行环境。
另外一种值得借鉴的思路是阿里云开发平台的 Browser VM,其外围入口逻辑在 Context.js 文件中。它的具体实现思路是这样的:
-
借鉴
with
的实现成果,在 webpack 编译打包阶段为每个子利用代码包裹一层代码(见其插件包 breezr-plugin-os 下相干文件),创立一个闭包,传入本人模仿的 window、document、location、history 等全局对象(见 根目录下 相干文件)。 -
在模仿的 Context 中,new 一个 iframe 对象,提供一个和宿主利用空的(about:blank) 同域 URL 来作为这个 iframe 初始加载的 URL(空的 URL 不会产生资源加载,然而会产生和这个 iframe 中关联的 history 不能被操作的问题,这时路由的变换只反对 hash 模式),而后将其下的原生浏览器对象通过
contentWindow
取出来(因为 iframe 对象人造隔离,这里省去了本人 Mock 实现所有 API 的老本)。 -
取出对应的 iframe 中原生的对象之后,持续对特定须要隔离的对象生成对应的 Proxy,而后对一些属性获取和属性设置,做一些特定的实现(比方 window.document 须要返回特定的沙箱 document 而不是以后浏览器的 document 等)。
-
为了文档内容可能被加载在同一个 DOM 树上,对于 document,大部分的 DOM 操作的属性和办法仍旧间接应用宿主浏览器中的 document 的属性和办法解决等。
总的来说,在 Browser VM 的实现中,能够看出其实现局部还是借鉴了 乾坤 或者说其余微前端架构的思路,比方常见全局对象的代理和拦挡。并且借助 Proxy 个性,针对 Cookie、LocalStorage 的读写同样能做一些安全策略的实现等。但其最大的亮点还是借助 iframe 做了一些取巧的实现,当这个为每个子利用创立的 iframe 被移除时,写在其下 window 上的变量和 setTimeout、全局事件监听等也会一并被移除;另外基于 Proxy,DOM 事件在沙箱中做记录,而后在宿主中生命周期中实现移除,可能以较小的开发成本实现整个 JavaScript 沙箱隔离的机制。
除了以上社区中当初比拟火的计划,最近我也在 大型 Web 利用插件化架构摸索 一文中理解到了 UI 设计畛域的 Figma 产品也基于其插件零碎产出了一种隔离计划。起初 Figma 同样是将插件代码放入 iframe 中执行并通过 postMessage 与主线程通信,但因为易用性以及 postMessage 序列化带来的性能等问题,Figma 抉择还是将插件放入主线程去执行。Figma 采纳的计划是基于目前还在草案阶段 Realm API,并将 JavaScript 解释器的一种 C++ 实现 Duktape 编译到了 WebAssembly,而后将其嵌入到 Realm 上下文中,实现了其产品下的三方插件的独立运行。这种计划和摸索的基于 Web Worker 的实现可能可能联合得更好,继续关注中。
Web Worker 与 DOM 渲染
在理解了 JavaScript 沙箱的「前世今生」之后,咱们将眼光投回本文的配角——Web Worker 身上。
正如本文结尾所说,Web Worker 子线程的模式也是一种人造的沙箱隔离,现实的形式,是借鉴 Browser VM 的前段思路,在编译阶段通过 Webpack 插件为每个子利用包裹一层创立 Worker 对象的代码,让子利用运行在其对应的单个 Worker 实例中,比方:
__WRAP_WORKER__(`/* 打包代码 */}`);
function __WRAP_WORKER__(appCode) {var blob = new Blob([appCode]);
var appWorker = new Worker(window.URL.createObjectURL(blob));
}
但在理解过微前端下 JavaScript 沙箱的实现过程后,咱们不难发现几个在 Web Worker 上来实现微前端场景的 JavaScript 沙箱必然会遇到的几个难题:
-
出于线程平安设计思考,Web Worker 不反对 DOM 操作,必须通过 postMessage 告诉 UI 主线程来实现。
-
Web Worker 无法访问 window、document 之类的浏览器全局对象。
其余诸如 Web Worker 无法访问页面全局变量和函数、无奈调用 alert、confirm 等 BOM API 等问题,绝对于无法访问 window、document 全局对象曾经是小问题了。不过可喜的是,Web Worker 中能够失常应用 setTimeout、setInterval 等定时器函数,也仍能发送 ajax 申请。
所以,当先要解决问题,便是在单个 Web Worker 实例中执行 DOM 操作的问题了。首先咱们有一个大前提:Web Worker 中无奈渲染 DOM,所以,咱们须要基于理论的利用场景,将 DOM 操作进行拆分。
React Worker DOM
因为咱们微前端架构中的子利用局限在 React 技术栈下,我先将眼光放在了基于 React 框架的解决方案上。
在 React 中,咱们晓得其将渲染阶段分为对 DOM 树的扭转进行 Diff 和理论渲染扭转页面 DOM 两个阶段这一根本事实,那能不能将 Diff 过程置于 Web Worker 中,再将渲染阶段通过 postMessage 与主线程进行通信后放在主线程进行呢?简略一搜,颇为汗颜,曾经有大佬在 5、6 年前就有尝试了。这里咱们能够参考下 react-worker-dom 的开源代码。
react-worker-dom 中的实现思路很清晰。其在 common/channel.js 中对立封装了子线程和主线程相互通信的接口和序列化通信数据的接口,而后咱们能够看到其在 Worker 下实现 DOM 逻辑解决的总入口文件在 worker 目录下,从该入口文件顺藤摸瓜,能够看到其实现了计算 DOM 后通过 postMessage 告诉主线程进行渲染的入口文件 WorkerBridge.js 以及其余基于 React 库实现的 DOM 结构、Diff 操作、生命周期 Mock 接口等相干代码,而承受渲染事件通信的入口文件在 page 目录下,该入口文件承受 node 操作事件后再联合 WorkerDomNodeImpl.js 中的接口代码实现了 DOM 在主线程的理论渲染更新。
简略做下总结。基于 React 技术栈,通过在 Web Worker 下实现 Diff 与渲染阶段的进行拆散,能够做到肯定水平的 DOM 沙箱,但这不是咱们想要的微前端架构下的 JavaScript 沙箱。先不谈拆分 Diff 阶段与渲染阶段的老本与收益比,首先,基于技术栈框架的特殊性所做的这诸多致力,会随着这个框架自身版本的降级存在着保护降级难以掌控的问题;其次,如果各个子利用应用的技术栈框架不同,要为这些不同的框架别离封装适配的接口,扩展性和普适性弱;最初,最为重要的一点,这种办法临时还是没有解决 window 下资源共享的问题,或者说,只是启动了解决这个问题的第一步。
接下来,咱们先持续探讨 Worker 下实现 DOM 操作的另外一种计划。window 下资源共享的问题咱们放在其后再作探讨。
AMP WorkerDOM
在我开始纠结于如 react-worker-dom 这种思路理论落地开发的诸多「天堑」问题的同时,浏览过其余 DOM 框架因为同样具备插件机制偶尔迸进了我的脑海,它是 Google 的 AMP。
AMP 开源我的项目 中除了如 amphtml 这种通用的 Web 组件框架,还有很多其余工程采纳了 Shadow DOM、Web Component 等新技术,在我的项目下简略刷了一眼后,我欣慰地看到了工程 worker-dom。
粗略翻看下 worker-dom 源码,咱们在 src 根目录下能够看到 main-thread 和 worker-thread 两个目录,别离关上看了下后,能够发现其实现拆分 DOM 相干逻辑和 DOM 渲染的思路和下面的 react-worker-dom 根本相似,但 worker-dom 因为和下层框架无关,其下的实现更为贴近 DOM 底层。
先看 worker-thread DOM 逻辑层的相干代码,能够看到其下的 dom 目录 下实现了基于 DOM 规范的所有相干的节点元素、属性接口、document 对象等代码,上一层目录中也实现了 Canvas、CSS、事件、Storage 等全局属性和办法。
接着看 main-thread,其要害性能一方面是提供加载 worker 文件从主线程渲染页面的接口,另一方面能够从 worker.ts 和 nodes.ts 两个文件的代码来了解。
在 worker.ts 中像我最后所构想的那样包裹了一层代码,用于主动生成 Worker 对象,并将代码中的所有 DOM 操作都代理到模仿的 WorkerDOM 对象上:
const code = `
'use strict';
(function(){${workerDOMScript}
self['window'] = self;
var workerDOM = WorkerThread.workerDOM;
WorkerThread.hydrate(
workerDOM.document,
${JSON.stringify(strings)},
${JSON.stringify(skeleton)},
${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)}`;
this[TransferrableKeys.worker] = new Worker(URL.createObjectURL(new Blob()));
在 nodes.ts 中,实现了实在元素节点的结构和存储(基于存储数据结构是否以及如何在渲染阶段有优化还需进一步钻研源码)。
同时,在 transfer 目录下的源码,定义了逻辑层和 UI 渲染层的音讯通信的标准。
总的来看,AMP WorkerDOM 的计划摈弃了下层框架的束缚,通过从底层结构了 DOM 所有相干 API 的形式,真正做到了与框架技术栈无关。它一方面齐全能够作为下层框架的底层实现,来反对各种下层框架的二次封装迁徙(如工程 amp-react-prototype),另一方面联合了以后支流 JavaScript 沙箱计划,通过模仿 window、document 全局办法的并代理到主线程的形式实现了局部的 JavaScript 沙箱隔离(临时没看到路由隔离的相干代码实现)。
当然,从我集体角度来看,AMP WorkerDOM 也有其以后在落地上肯定的局限性。一个是对以后支流下层框架如 Vue、React 等的迁徙老本及社区生态的适配老本,另一个是其在单页利用下的尚未看到有相干实现计划,在大型 PC 微前端利用的反对上还无奈找到更优计划。
其实,在理解完 AMP WorkerDOM 的实现计划之后,基于 react-worker-dom 思路的后续计划也能够有个大略方向了:渲染通信的后续过程,可思考联合 Browser VM 的相干实现,在生成 Worker 对象的同时,也生成一个 iframe 对象,而后将 DOM 下的操作都通过 postMessage 发送到主线程后,以与其绑定的 iframe 兑现来执行,同时,通过代理将具体的渲染实现再转发给原 WorkerDomNodeImpl.js 逻辑来实现 DOM 的理论更新。
小结与一些集体前瞻
首先聊一聊集体的一些总结。Web Worker 下实现微前端架构下的 JavaScript 沙箱最后是出于一点集体灵光的闪现,在深刻调研后,尽管最终还是因为这样那样的问题导致在计划落地上无奈找到最优解从而放弃采纳社区通用计划,但仍不障碍我集体对 Web Worker 技术在实现插件类沙箱利用上的继续看好。插件机制在前端畛域始终是津津有味的一种设计,从 Webpack 编译工具到 IDE 开发工具,从 Web 利用级的实体插件到利用架构设计中插件扩大设计,联合 WebAssembly 技术,Web Worker 无疑将在插件设计上占据无足轻重的位置。
其次是一些集体的一些前瞻思考。其实从 Web Worker 实现 DOM 渲染的调研过程中能够看到,基于逻辑与 UI 拆散的思路,前端后续的架构设计有很大机会可能产生肯定的改革。目前不论是流行的 Vue 还是 React 框架,其框架设计不论是 MVVM 还是联合 Redux 之后的 Flux,其本质上仍旧还是由 View 层驱动的框架设计(集体浅见),其具备灵活性的同时也产生着性能优化、大规模我的项目层级升上后的合作开发艰难等问题,而基于 Web Worker 的逻辑与 UI 拆散,将促使数据获取、解决、生产整个流程的进一步的业务分层,从而固化出一整套的 MVX 设计思路。
当然,以上这些我集体还处于初步调研的阶段,不成熟之处还需多加推敲。且听之,后续再实际之。
作者:ES2049 / 靳志凯
文章可随便转载,但请保留此原文链接。
十分欢送有激情的你退出 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com。