JAVASCRIPT模块开发的5种改进方式

90 阅读16分钟

以下五个开发者福利是针对一组模块和谐提案的,这些提案旨在赋予 ECMAScript 模块更多 CommonJS 的能力。

译自5 Ways JavaScript Is Improving Modules for Developers,作者 Mary Branscombe。

现代 JavaScript 应用程序通常使用数千个模块,提高模块性能并使其更易于开发人员使用将是一个巨大的进步。这就是一组相互关联的 JavaScript 提案,称为模块和谐的动机。这些提案在JavaScript 语言中引入了用于处理原生 ECMAScript 模块的新功能,并赋予它们在从 CommonJS 模块切换后失去的一些功能。

“ECMAScript 模块不像以前系统那样强大和易于使用,”Igalia 工程师和流行 Babel 转译器维护者Nicolò Ribaudo告诉我们。

模块和谐提案旨在让高级开发人员更容易从模块中获得最大收益,以便他们的工具更适合主流开发人员使用。

虽然将这些选项内置到 JavaScript 中将是新的,但它们并不是新想法;其中许多已经被构建工具(如捆绑器)或复杂 JavaScript 平台(如彭博社用于运行其终端的系统)的开发人员使用:他们要么在内部使用 CommonJS,要么编写类似功能的自定义实现。

模块和谐提案旨在提高模块的性能,并让这些高级开发人员更容易从模块中获得最大收益,以便他们的工具更适合使用它们的普通开发人员。

但与往常一样,向 JavaScript 添加新功能需要时间,因为标准化过程依赖于共识。JavaScript 功能必须在多个浏览器、引擎、运行时和工具中得到支持,才能被认为足够成熟,成为该语言的永久功能。默认情况下,在所有相关人员都确信这些提案值得花费时间去实现之前,都会拒绝这些提案,尤其是如果这意味着更改已经正常工作的代码。

“架构是关于找到正确的层级和关注点分离以及耦合,所有这些都汇聚在模块和谐中,”CommonJS 的最初创建者Kris Kowal解释道。

1. 使用 Web Workers 合并模块文件

我们已经研究了一些关键的模块和谐提案,包括那些依赖于加载模块的管道不同阶段的提案。另外两个最有希望的提案旨在简化开发人员使用模块的文件结构,从将它们与 Web Workers 相结合开始。

事实证明,让开发人员将 worker 放入单独的文件是采用模块和谐的最大障碍,因为您必须处理解析模块的网络路由并将该路由传递给 worker——这对捆绑器来说是动态的且难以处理。此外,处理多个文件会增加复杂性。

使用模块表达式,您可以在同一个文件中包含多个模块。

“有时您的 worker 只需几行代码,可能导入其他模块,然后是模块本身。它可能与要运行的代码有很强的逻辑关系,因此将其拆分为单独的文件并不总是理想的,”Ribaudo 告诉我们。

各种库允许开发人员在其他代码中内联编写 worker,但这会导致内容安全策略问题。使用模块表达式,您可以在同一个文件中包含多个模块。这对于多线程代码特别有用,在多线程代码中,开发人员希望将模块发送到 worker 以供稍后执行——可能不止一次。

“当您需要进行一些昂贵的计算(例如在大型应用程序中)时,将其移到 worker 中是有益的,这样您的主线程(管理 UI)就可以继续执行其任务,而 worker 在后台进行计算,”Ribaudo 解释道。“这为您提供了一些语法,可以在另一个模块内内联声明模块并将其传递,而无需强制您创建单独的文件。然后,捆绑器可以轻松地弄清楚如何正确地拆分内容,并且您可以确定捆绑器可以找到所有文件,而无需您显式配置。” “能够编写多工作程序是我们能够在多核世界中编写高性能系统的方式,在这个世界中,CPU 的时钟速度不再提高,”Kowal 指出。

2. 模块声明

另一方面,模块声明允许您将多个模块捆绑到单个 JavaScript 文件中,并让它们相互执行,而无需进行任何其他配置。即使使用 HTTP2,当您加载大量小文件时,性能也会下降——而且大量小文件无法像一个大文件那样有效地压缩,这是开发人员首先使用捆绑器的原因之一。

“真正的程序使用如此多的模块,当您想要预取所有内容时,就会出现级联依赖问题,”Ecma 副总裁兼彭博软件工程师Daniel Ehrenberg解释道,他正在参与多个提案的开发。

模块声明并非旨在取代捆绑器,但它们将简化编写捆绑器的任务。

模块声明并非旨在取代捆绑器,但它们将简化编写捆绑器的任务,消除一些繁琐的工作,并让工具开发人员能够专注于更有趣的功能;它可能允许他们最终从内部迁移到 CJS。

“捆绑器可以使用浏览器实现的模块语义,将代码输出到单个文件中:它们不必重新实现 ESM 的工作方式,”Ribaudo 建议道。

Ehrenberg 同意。“对捆绑器进行优化仍然有意义,这些优化可能跨越多个边界,因此我们并不认为捆绑器会变得微不足道,但至少如果它们不必自己解释语义,那就太好了,”他说。

模块声明仅涵盖 JavaScript 模块。一些资源包需要包含 JavaScript 模块以外的内容。Web Incubator 社区组捆绑预加载提案解决了这个问题,但 JavaScript 模块往往是 Web 应用程序加载大量小文件的地方,这会影响性能。

3. 导入属性

这两个提案都处于第 2 阶段,这意味着它们仍然是草案,可能会随着开放问题的解决而发生变化,并通过实验性实现进行验证。但另一个最早的模块和谐提案即将发布。

导入属性创建了一种语法,允许模块导入语句传递有关模块的更多信息——例如,告诉捆绑器如何解释或处理您要导入的文件。您无需依赖文件类型,而是可以指定是否应将图像加载为位图,是否要将文件加载为纯文本,甚至告诉捆绑器捆绑文件并返回指向它的 URL。

“这将允许开发人员设置有关他们要导入的模块的一些属性,”Ribaudo 告诉我们。最初的动机是支持将 JSON 文件作为模块导入,同时提供安全保障。“也许您认为您正在导入一个 JSON 模块,”他继续说道,“所以您认为您是安全的,但实际上您正在从 CDN 导入一个 JavaScript 模块,而该模块已被入侵。”

使用导入属性指定您期望一个 JSON 模块,如果事实证明它不是其他东西,浏览器将拒绝加载它。

“导入属性将成为捆绑器的巨大福音,以便了解如何以有效的方式将您的程序捆绑在一起。”

– Justin Ridgewell,Vercel

“导入属性将成为捆绑器的巨大福音,以便了解如何将您的程序捆绑在一起,以有效的方式,让用户控制并控制捆绑方式,”Vercel 的 TC39 代表Justin Ridgewell(他负责Turbopack捆绑器)补充道。

导入属性已在 TypeScript、Webpack 等捆绑器以及 Safari 和基于 Chromium 的浏览器中实现。但自第一个实现创建以来,语法已经发生了一些重大变化(该提案已经从第 3 阶段回到第 2 阶段以进行更多工作,然后再次达到第 3 阶段)。

“在实现它之后,我们注意到仅仅断言模型的属性并不完全是网络需要的:我们需要能够稍微影响模块的导入方式,例如,传递当前的 HTTP 标头,”Ribaudo 解释道。

最初的语法足够有用,很快就受到开发人员的欢迎,但浏览器和工具开发人员已经在努力采用新的语法,他建议“这将在不久的将来普遍可用”。

4. 使用隔室进行细粒度安全

与其他提案相比,隔间具有更广泛的范围——从兼容性到阻止软件供应链攻击——因此处于更早的阶段。第一阶段意味着 TC39 委员会已经同意存在需要解决的问题,但并不意味着提案一定是解决问题的正确方法。

隔间为模块提供虚拟环境,但该提案还提供了一些功能,使其他模块和谐提案能够更好地工作。例如,模块阶段导入使模块加载器加载的模块如何融入模块图更加清晰,并允许您拥有一个可信的主线程,该线程可以加载和审核模块并将它们传递给工作线程以执行,而隔间通过提供细粒度的隔离来锁定这些功能。

“我们的目标是将安全边界降低到对象级别。”

– Kris Kowal,CommonJS 的创建者

Kowal 将此称为对象能力编程:“使 JavaScript 能够获得比来源更细粒度的安全边界,甚至比工作线程更细粒度的安全边界。我们的目标是将安全边界降低到对象级别,这样程序员就可以通过构造来推断将强大的对象提供给第三方所固有的权限。”

“您可以明确赋予能力,并隐式拒绝所有强大的 IO 能力,除非主机明确授予它们。为了使这能够正常工作,您必须能够执行模块,并且您需要能够虚拟化模块加载器,以便主机可以控制哪些源可供访客使用。”

Kowal 说,这里有一个明显的安全角度,可以避免软件供应链攻击

“网页完全由单方利益组成,这是一个迅速恶化的虚构。将对象能力编程作为开发人员可用的选项,使他们能够隔离其第三方依赖项,并限制他们可能造成的损害,如果他们设法通过获取他们不应该拥有的对象来提升其权限。”

使用 JavaScript 的嵌入式系统开发人员将隔间和虚拟化模块视为保护设备的一种方式。Kowal 解释说,一个可编程的灯泡可能会使用隔间来提供一个强大的 API 来控制 LED,但使用称为衰减的方法来限制谁可以访问某些功能。

“他们可以使用隔间来获取本机灯泡控制 API 并为用户程序创建该 API 的衰减,这样他们就无法以如此快的速度闪烁灯泡,从而导致癫痫发作,或者通过灯泡运行如此多的功率,从而导致灯泡烧毁。”

5. 使 Jest 等开发工具更轻松

为模块提供虚拟化环境也是 Jest 和 Playwright 等开发工具编写者需要的这种高级用例:Jest 是建立在 Node.js 中的虚拟化系统之上的,该系统类似于隔间将提供的功能,Fastly 工程师Guy Bedford(参与了许多模块和谐提案)解释说。

“我们正在标准中构建这些原语,以便我们可以在本机 ES 模块系统中对这些功能提供一流的支持。”

– Guy Bedford,Fastly

“假设您希望能够模拟导入,您希望稍微更改执行行为:Jest 控制所有这些,并通过这些虚拟化原语注入大量检测和行为。我们正在标准中构建这些原语,以便我们可以在本机 ES 模块系统中对这些功能提供一流的支持,因为现在没有对这种功能提供一流的本机支持,因此存在实现复杂性。”

可观察性提供者也发现很难在今天挂钩到 ES 模块系统以提供检测:使用隔间,“您可以包装导出的函数并挂钩它们,并查看它们何时被调用,您可以包装导入以记录或模拟它们,并用替代方案替换它们,并以跨多个运行时有效的方式执行此操作。”

Ribaudo 建议,这对插件也很有用,您可以在同一代码中并行运行多个选项,而不会相互干扰。它可能有助于代码重用。 “大多数开发人员不会考虑虚拟化其他 JavaScript 环境,但这意味着您将能够例如在浏览器中模拟 Node 的工作方式,实现 Node.js 模块解析语义,在虚拟机中提供 Node.js 全局变量,使它们与您的其他代码分开。”

当然,我们之前看过的依赖于隔间的模块和谐提案带来了自己的好处,例如通过简化使用工作线程并允许模块传递来提高性能,Kowal 指出。“你可以解析一次模块,然后在线程之间共享一个不可变对象,或者有一个专门的模块加载器工作线程,它能够将工作传递给其他线程,”他说。

协调一群提案

大多数 JavaScript 开发人员不会直接使用新的模块功能,但他们依赖的工具生态系统会使用。这里的进展可能并不明显,即使是第 3 阶段的提案也需要一段时间才能成为 JavaScript 的一部分,因为社区正在努力解决细节问题以及这些更改将如何影响 JavaScript 生态系统的各个部分(例如 TypeScript)。

“这套模块和谐提案已经酝酿了三四年,我认为它还会持续几年,因为存在实现者方面的担忧,也存在 API 方面的担忧,”Ridgewell 警告说。“我试图解决我的捆绑器用例:这套提案影响了很多东西。”

这就是故意保守的 JavaScript 标准流程的工作方式,以便在功能成为语言的一部分时交付足够成熟的依赖功能。

这就是故意保守的 JavaScript 标准流程的工作方式,以便在功能成为语言的一部分时交付足够成熟的依赖功能。缓慢的步伐让每个人——从工具开发人员到构建服务器端 JavaScript 运行时的团队——都有机会对他们需要的先进用例提供反馈。

“作为一种流程,共识确实需要很多人对这些基础感到相当自信:这很慢,但值得慢慢来,”Bedford 指出。

在整个过程中,可能需要对不同的提案进行重组。这已经发生在模块声明和模块表达式中,它们最初是截然不同的提案,但后来演变为模块声明现在建立在模块表达式的基础之上。

几个提案在进展过程中被重新命名,以更好地解释它们提供的功能。延迟导入获得了新的名称,并且被简化为最初只涵盖能够延迟评估模块直到你真正需要使用它。一些被放弃的更复杂的选择可能会卷土重来,但在讨论这些选择的同时,它们并没有减缓将提案推进到新阶段的尝试。

此外,我们上次看过的资产引用提案引发了对模块和谐的大量思考,但很大程度上已被其他方法取代,例如模块声明。

正在取得巨大进展

标准化 JavaScript 的 TC39 委员会在协调提案方面没有太多经验,这些提案相互关联以交付(大部分)独立的更大目标的一部分,就像模块和谐套件一样,但倡导这套提案的专家付出了很多努力来找到正确的方法将功能逻辑地拆分并保持这么多项目的进展。

一个包含所有这些功能的单一巨型提案将更难理解,并且必须一次性实现所有功能可能会让浏览器制造商望而却步。“逐块解决这些问题比一次性过于雄心勃勃更容易,这就是为什么总体努力有效的原因,”Bedford 建议道。

“每个提案都是自发性的,即使所有提案都没有获得批准,每个提案仍然有意义,”Ribaudo 同意道。

不同项目之间存在大量协作,以保持一切同步,例如确保所有提案都依赖的新模块类(表示当前 JavaScript 语言中不存在的模块的对象)在开发过程中保持一致。

“即使有时很难看到,也正在取得巨大进展。”

– Nicolò Ribaudo,Igalia 工程师

在过去两年中,几乎每次 TC39 会议上都会有一个或多个模块提案的演示,因此“所有代表现在都了解了所有功能的空间”,这导致多个提案在标准化的不同阶段快速推进。

“如果你看一个单独的提案,它可能感觉进展非常缓慢,但如果你看整个模块空间,总是有东西在进行,总是有东西在向前推进,”Ribaudo 指出。

Bedford 强调,总体势头正在朝着交付开发人员将从中受益的重要更改的方向发展,而无需重写他们自己的代码。

“即使有时难以察觉,但我们正在取得巨大的进展,”Ribaudo 说。

“我们在这里处理的每个规范都是一项改进,”他解释道。“这些将是调整更改和平台级更改,主要是添加,而不是重大破坏性更改。”

“我们的目标是解决虚拟化、工作者可移植性和延迟加载问题,无论我们是否能解决所有问题,这些都将是改进,不会对大多数用户编写的代码造成成本。”

本文在云云众生yylives.cc/)首发,欢迎大家访问。