JavaScript模块体验的最终改进之路

原文信息: 查看原文查看原文

How JavaScript Is Finally Improving the Module Experience

- Mary Branscombe

被统称为“模块和谐”的多项长期提案将完成JavaScript从CommonJS迁移时丢失的功能。

JavaScript曾被视为一种开发者能够快速编写代码的语言,但它并不一定适合大规模编写大型应用程序的团队。一个原因是,直到最近,它还没有原生的强大模块支持。

在ECMAScript 6引入ECMAScript模块(简称ESM)之前,并没有官方的标准格式来打包JavaScript代码,以便一次又一次地使用。开发者使用了Webpack、Babel和CommonJS(CJS)等工具。ESM自2018年现代浏览器广泛支持以来,具有明显的优势:浏览器可以接管优化模块加载,这比使用框架或库所需的客户端处理和往返更为高效。

但即使经过十多年的工作,ESM仍未包含CJS模块的所有特性和细微差别,特别是对于创建如打包器这类工具的开发者。

Igalia工程师和流行的Babel转译器的维护者Nicolò Ribaudo解释说:

使用CommonJS比ESM更容易编写打包器。

例如,Webpack继续在内部将代码编译为CJS。“如果你想依赖纯ESM语义,你必须重命名所有变量,你必须手动创建命名空间对象,甚至不可能打包一切。如果两个模块都使用顶级await,就没有方法可以编译。”

Babel之所以一直使用CJS,是因为这样可以延迟加载模块直到需要它们以提高性能:虽然ESM也可以做到这一点,但它的人体工程学要差得多。尽管Node.JS用户已经有一段时间可以在他们的项目中使用ESM,但Node 22仍在增加对一些ESM特性的支持以简化迁移。

Ribaudo说:“仅仅从CommonJS迁移就已经很难了。”

让模块重新和谐

为了解决这些差距,并普遍使ES模块对开发者更好用,一组被称为“模块和谐”的相关提案正在缓慢地通过标准流程。

“模块和谐”这个名字也是一个提醒,尽管各种提案看起来可能是分散的,但它们都是关于改善在JavaScript中使用模块的体验。

这些改进特别针对那些尚未能够迁移到ESM的工具开发者,因此当新特性成为语言的一部分时,大多数开发者将无需更改代码即可获得好处。

“模块和谐”这个词是一种双关语。它不仅是指协调JavaScript中模块的先前和当前选项,也是对Harmony的致敬,Harmony既是ECMAScript 6的代号,也是TC39用于其标准化过程的名字——这是以年度发布的方式重新启动JavaScript语言的定期开发,始于ECMAScript 2015(ECMAScript 6的官方名称)。这就是为什么ES模块有时被称为“Harmony模块”。

此外,在模块和谐伞下,有六到九个不同的JavaScript模块改进提案,它们都解锁了ESM的新能力,并且都在以自己的速度进行标准化。事实上,提案如此之多,我们将在两篇文章中查看它们。

Ecma副主席兼彭博软件工程师Daniel Ehrenberg解释说:“人们会说,‘嘿,有这么多模块提案,你们都在互相交流吗?’答案是‘是的,我们在互相交流!’”

模块和谐分层

不同模块和谐提案如何配合;通过TC39演示

所以模块和谐这个名字也是一个提醒,尽管各种提案可能看起来分散,它们都是关于改善在JavaScript中使用模块的体验。

Fastly工程师Guy Bedford说:“我们在JavaScript中拥有模块的悠久历史,它一直在逐步发展。”他参与了许多模块和谐提案。这是JavaScript中熟悉的过程,“人们急于前进并构建东西,采取最短的路径来解决他们的问题,标准是一个更慢的跟随过程。”

Bedford建议:“我们看到模块和谐努力填补的使用案例,非常乏味和直白地说,让我们回到了与CommonJS的同等水平,但实际上是一个一流的,本地浏览器模块系统。”

虽然ESM提供了非常直接的代码结构,但开发者希望从模块中获得额外的功能。Bedford建议:“比如懒加载和优化和虚拟化,以及编写你的工具和模拟的能力,以及支持打包器打包代码所需的所有特性。这些都是我们每天做的标准事情,但以一流的符合标准的方式做到这一点出奇地困难。”

ES模块确实有优势:“从一开始,我们就看到了直接的好处,”Bedford指出。“我们已经看到了大规模采用和对Web开发的大规模改进。我们在这里添加的只是锦上添花:获得最后一批高级用例。”

“我们模块和谐的目标是确保ESM足够强大,以至于完全不需要CJS。” — Nicolò Ribaudo,Igalia工程师

CommonJS的最初创建者Kris Kowal将模块和谐描述为“完成一个总是打算至少走这么远的设计。”

他说:“当我提出CommonJS时,意图是创建一种方式,让人们可以表达JavaScript,可以在项目之间共享,而不必将它们与特定框架耦合。”

当时,像Dojo和jQuery这样的框架有自己的插件系统,编写模块化代码的开发者必须选择他们要针对的框架。

Kowal说:“我想创建一种前瞻性的模块表达方式,当可能存在适当的系统时,可以轻松地过渡到适当的系统。”

Ribaudo同意:“我们模块和谐的目标是确保ESM足够强大,以至于完全不需要CJS,”他也是几个提案的倡导者。

这看起来可能是一个令人沮丧的后退步骤,慢慢地在ESM中重建开发者已经用CJS拥有的功能,但Bedford建议将其视为创建一个新的基础:“建立那个基线允许未来的新努力朝一个新的方向发展。”

JavaScript和WebAssembly之间的互操作性

在模块和谐提案中,取得最大进展的是源阶段导入。它已经达到了第三阶段,实现工作正在进行中,因为能够自定义模块的加载、链接和执行方式——通过导入一个呈现模型源的对象而不是直接使用模块——将提供更无缝的JavaScript和WebAssembly之间的互操作性。

Ribaudo解释说,目前一起使用WebAssembly和JavaScript是一个复杂的过程:“不清楚如何获取模型并准备它以使其准备好运行。”

“这不是大多数开发者会直接使用的特性,但是编写帮助开发者将JavaScript代码导入WebAssembly的工具可以使用它来提高安全性。” — Nicolò Ribaudo,Igalia工程师

“此外,获取某物并不遵循与普通导入相同的内容安全策略,所以你获取和评估WebAssembly模块的唯一方式是显著放宽你的安全策略。因为你可以将JavaScript函数传递给你的模块,你可能会不小心暴露新的能力。”

该提案允许开发者应用JavaScript风格的安全策略,作为一种限制可以运行的代码的方式,因为新对象包括原始源URL。“你可以说我只想让我的应用程序能够从这两个域加载和运行WebAssembly代码,而不是从任何其他域加载的代码。”它还启用了静态分析,确定正在执行哪些Wasm模块,就像对JavaScript模块所做的那样。

“这不是大多数开发者会直接使用的特性,但是编写帮助开发者将JavaScript代码导入WebAssembly的工具可以使用它来提高安全性,”他建议。

该提案类似于WebAssembly允许你创建多个模块实例的方式,Bedford指出,并另一个模块和谐提案Compartments有关,它将提供一种更细粒度的隔离机制在JavaScript内部。这是试图标准化虚拟化原语的一个相当大的尝试,我们将在未来更详细地查看。

我们所说的JavaScript中的虚拟化只是实例化:目前当我们加载一个模块时,你加载一次。你的整个应用程序中只有一个这样的模块,它拥有的任何状态都将存在于应用程序的整个生命周期中。”

源阶段导入将是虚拟化的构建块,它更像是WebAssembly选项:“能够采用模块的抽象表示并创建它的多个实例,可能有不同的导入,可能内部有不同的状态。”

这允许JavaScript中更大的灵活性——例如,启用用户空间加载器——但也提供了更好的集成和更符合人体工程学的JavaScript中WebAssembly的使用。

Bedford解释说:“当你使用源阶段从WebAssembly导入时,你得到了WebAssembly已经存在的高级模块,这些模块可以被多次实例化。”已经在V8 JavaScript引擎中开始了实现这项工作的步骤,这应该会更容易和可移植地在JavaScript工具链中运送Wasm。

虽然源阶段导入听起来像是一个相对有限的进步,但分离影响模块使用方式的加载模块的不同阶段(或阶段)的概念是许多其他模块和谐提案的关键。

实际上,在模块管道中有五个不同的阶段:

  1. 解析模块的网络路由,以便浏览器知道它在哪里。
  2. 获取(可能编译)模块。
  3. “源”阶段,检索并附加它将如何执行的上下文以及它需要从哪里加载依赖项。
  4. 链接那些加载的依赖项并将任何导入绑定。
  5. 最后,评估,所有加载的模块都被执行。

链接和评估阶段一起被称为“实例”阶段(因为现在有一个模块的实例)。

导入模块的五个阶段

导入模块的五个阶段;通过TC39演示

稍后再保存

源阶段导入允许开发者在执行模块代码之前使用已获取的模块及其上下文工作,但仍依赖于静态分析显示将执行什么代码的保证,并获得更好的人体工程学、工具支持和安全性。

其他提案——模块声明模块表达式(请稍后查看这两个的更多细节)和延迟模块导入——也依赖于模块管道的不同阶段,像将最终评估阶段推迟到你实际需要模块的属性时。

“有了这个提案,你可以将初始化工作推迟到需要模块时,同时仍然加载模块并解析它,并让它在模块系统中准备好。” — Guy Bedford,Fastly工程师

延迟导入的目标是提高包含大量JavaScript的网页和Node应用程序的启动时间,这些JavaScript实际上直到稍后才会被调用(也许如果用户点击一个按钮)或根本不会被调用,Bedford解释说。

“当你导入一个模块时,你必须先执行它所依赖的所有模块。当你访问一个网页时,它正在加载一大堆模块,你正在一次性初始化所有这些模块,即使你不使用它们。有了这个提案,你可以将初始化工作推迟到需要模块时,同时仍然加载模块并解析它,并让它在模块系统中准备好。”

这是CommonJS中的一个熟悉的技术(它在其他语言中也可用,比如Go、Python、Ruby和Swift)。“当你的初始化变慢时,你将require放在函数内部,这与原生模块非常相似。”许多尚未从CJS迁移的打包器依赖这种加速。

这比使用异步代码和动态导入所需的重大重构要简单得多,目前是延迟加载JavaScript模块的唯一选项。

Ehrenberg解释说,动态导入“让你将代码的不同部分分开,在不同的时间加载”,这就是为什么许多打包器实现它的原因。“但这只有在你能够承受去网络并加载某些东西的慢速的情况下才有效。在其他情况下,你正在进行深度计算,一切都是同步的,你希望能够快速响应。”这就是延迟导入的用武之地:“它允许某些延迟执行的情况,否则是不可能的。”

使用顶级await(增加异步加载逻辑)的模块不能延迟评估,将在应用程序启动时执行。尽管在提案从第二阶段(它在2023年达到)向前推进之前,需要明确这一点的含义,但Ehrenberg在彭博的经验,它已经使用了自己的顶级await和延迟导入的JavaScript等价物有一段时间了,表明与其说是一个错误,“这是它们组合的自然方式。”

加速不会像在Node.js中那样大,因为对于服务器端代码,模块文件存储在代码执行的地方,但浏览器必须从其他地方加载文件。

Ribaudo承认,但延迟导入使添加一些性能改进变得更容易,而不需要要求开发者重构代码。

“如果你试图在特定地方提高性能,必须重构你的整个应用程序以支持异步函数调用是不理想的。我们正在给开发者一个工具来选择他们在人体工程学和性能之间的权衡。现在,你有不良的性能和最佳人体工程学,或者非常良好的性能和不太理想的人体工程学,我们正在增加一个中间点,仍然保持良好的人体工程学,同时提高性能——即使不是尽可能多。”

Mozilla在Firefox前端使用了自己的懒加载模块版本:在其内部代码中,加载和解析模块负责大约一半的启动时间,但一半被延迟导入可以推迟到实际需要时的评估所占据。其他实验显示了较小的改进,将JavaScript模块所花费的时间减少了20%:在一个加载时间超过一秒钟的页面上,这是6%的改进。这仍然是一个显著的改进,你可以使用新的导入延迟语法快速轻松地进行代码更改。

Bedford指出:“对于应用程序来说,最重要的事情是你能多快开始与它交互?”“如果我们能从这个看到真正的性能数字,我认为能够说你可以让你的应用程序加速将是非常有说服力的。”

使Worker更易于使用

在此基础上,模块阶段导入也承诺性能改进,使用与源阶段导入相同的源阶段甚至相同的语法获得类似的好处,但对于Worker而不是Wasm模块。

今年早些时候达到第一阶段,它即将被考虑进入第二阶段,并承诺一种更好的加载Worker的方式,使代码更加符合人体工程学,并使处理模块之间的关系的静态分析更容易。

当模块在Worker中加载时,这目前是困难的,对于库来说甚至更加不透明,因为并非所有构建工具都能很好地处理使用Worker的代码。这阻碍了库作者利用Worker提供的加速,以避免为用户增加构建复杂性。

更好的Worker支持在开发工具和JavaScript运行时中最终应该会使得JavaScript应用程序更快,因为如果Worker更容易使用,开发者和库作者更有可能使用它们来卸载计算密集型任务。

分而治之

成为模块和谐的最早尝试之一,资源引用旨在在模块管道的初始“解析”阶段工作,当你可以引用一个模块并像处理一样传递它而无需加载或初始化它,Ribaudo解释说。

故意放慢JavaScript标准化的步伐,因为默认情况下在功能被证明有用之前不会向语言添加新功能。

“你可以静态声明在某个时候,你将使用一个资源。那可能是一个模块,可能是一个CSS文件或一个图像——一个尚未加载但你的打包器可以打包它的数据集,知道[你]在某个时候会使用它。”

这听起来很有用,但随着不同提案的工作进展,资源引用没有得到太多关注,因为其他提案涵盖了类似的问题,可能解决更多的问题。事实上,自2018年以来它一直处于第一阶段,可能因为其他领域取得的进展而不再需要。但这不是浪费时间。其他模块和谐提案中采取的不同方法都有不同的优点和缺点,通过哪种方法最好地解决问题是标准化过程中的关键部分。

这反映了JavaScript标准化的故意缓慢步伐,其中默认情况下在功能被证明有用之前不会向语言添加新功能。

模块和谐套件的提案确实包括一些更雄心勃勃的方法,特别是对模块的安全性。在后续文章中,我们将深入探讨这意味着什么,并涵盖像模块表达式和模块声明这样的其他提案,展示新的语言特性是如何随着它们通过标准化过程而演变的。

分享于 2024-07-03

访问量 59

预览图片