本文并不会从头开始介绍 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 沙箱隔离方案过程中的一些资料收集、理解以及我的踩坑和思考的过程。虽然可能整篇文章内容都在「炒冷饭」,但还是希望我的探索方案的过程能对正在看这篇文章的你有所帮助。
在探索基于 Web Worker 的解决方案之前,我们先要对当前要解决的问题——JavaScript 沙箱有所了解。
提到沙箱,我会先想到出于兴趣玩过的沙盒游戏,但我们要探索的 JavaScript 沙箱不同于沙盒游戏,沙盒游戏注重对世界基本元素的抽象、组合以及物理力系统的实现等,而 JavaScript 沙箱则更注重在使用共享数据时对操作状态的隔离。
在现实与 JavaScript 相关的场景中,我们知道平时使用的浏览器就是一个沙箱,运行在浏览器中的 JavaScript 代码无法直接访问文件系统、显示器或其他任何硬件。Chrome 浏览器中每个标签页也是一个沙箱,各个标签页内的数据无法直接相互影响,接口都在独立的上下文中运行。而在同一个浏览器标签页下运行 HTML 页面,有哪些更细节的、对沙箱现象有需求的场景呢?
当我们作为前端开发人员较长一段时间后,我们很轻易地就能想到在同一个页面下,使用沙箱需求的诸多应用场景,譬如:
这里我们先回到开头,先将前提假设在我正在面对的微前端架构设计下。在微前端架构(推荐文章 Thinking in Microfrontend 、[拥抱云时代的前端开发架构——微前端] 等)中,其最关键的一个设计便是各个子应用间的调度实现以及其运行态的维护,而运行时各子应用使用全局事件监听、使全局 CSS 样式生效等常见的需求在多个子应用切换时便会成为一种污染性的副作用,为了解决这些副作用,后来出现的很多微前端架构(如乾坤)有着各种各样的实现。譬如 CSS 隔离中常见的命名空间前缀、Shadow DOM、 乾坤 sandbox css 的运行时动态增删等,都有着确实行之有效的具体实践,而这里最麻烦棘手的,还是微应用间的 JavaScript 的沙箱隔离。
在微前端架构中,JavaScript 沙箱隔离需要解决如下几个问题:
在乾坤架构设计中,关于沙箱有两个入口文件需要关注,一个是 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 等全局对象。contentWindow
取出来(因为 iframe 对象天然隔离,这里省去了自己 Mock 实现所有 API 的成本)。总的来说,在 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 的实现可能能够结合得更好,持续关注中。
在了解了 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 无法访问页面全局变量和函数、无法调用 alert、confirm 等 BOM API 等问题,相对于无法访问 window、document 全局对象已经是小问题了。不过可喜的是,Web Worker 中可以正常使用 setTimeout、setInterval 等定时器函数,也仍能发送 ajax 请求。
所以,当先要解决问题,便是在单个 Web Worker 实例中执行 DOM 操作的问题了。首先我们有一个大前提:Web Worker 中无法渲染 DOM,所以,我们需要基于实际的应用场景,将 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 下资源共享的问题我们放在其后再作讨论。
在我开始纠结于如 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([code])));
在 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 设计思路。
当然,以上这些我个人还处于初步调研的阶段,不成熟之处还需多加琢磨。且听之,后续再实践之。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/_HZdKQQNDmPQNdN9LNtlRw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。