import() S4
- 阶段: Stage 4
- 状态: 已完成
- ECMAScript 版本: ES2020
- 同步时间: 2026年8月26日
- English original · 官方仓库
该提案引入了一种类函数的 import() 语法,用于在 JavaScript 中动态加载模块,返回一个解析为模块命名空间对象的 promise。该设计将模块检索委托给宿主环境,类似于现有的模块加载机制。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
import()
本仓库包含一个向 JavaScript 添加"类函数"import() 模块加载语法形式的提案。目前它处于 TC39 流程 的第 4 阶段。此前,它曾在 whatwg/loader#149 中与模块加载社区进行过讨论。
您可以查看进行中的 规范草案,并在 问题跟踪器 上参与讨论。
动机和使用场景
现有的导入模块语法形式是静态声明。它们接受字符串字面量作为模块说明符,并通过运行时前的"链接"过程将绑定引入本地作用域。对于 90% 的情况来说,这是一个很好的设计,并支持静态分析、打包工具和树摇动等重要用例。
然而,能够在运行时动态加载 JavaScript 应用程序的某些部分也是可取的。可能是因为仅在运行时才知道的因素(如用户的语言),出于性能原因(在可能使用之前不加载代码),或出于健壮性原因(在加载非关键模块失败时能够存活)。这种动态代码加载历史悠久,尤其是在 Web 上,在 Node.js 中也是如此(以延迟启动成本)。现有的 import 语法不支持此类用例。
真正动态的代码加载还支持高级场景,例如让多个模块相互竞争,并选择第一个成功加载的模块。
提议的解决方案
该提案添加了一个 import(specifier) 语法形式,它在许多方面表现得像一个函数(但见下文)。它返回一个 promise,该 promise 解析为所请求模块的模块命名空间对象,该对象在获取、实例化和评估所有模块的依赖项以及模块本身之后创建。
这里的 specifier 将与 import 声明中的解释方式相同(即,相同的字符串在两个地方都有效)。但是,虽然 specifier 是字符串,但它不一定是字符串字面量;因此,像 import(`./language-packs/${navigator.language}.js`) 这样的代码将可以工作——这是使用通常的 import 声明无法实现的。
import() 被提议在脚本和模块中都可以工作。这为脚本代码提供了一种简单的异步入口点进入模块世界,使其能够开始运行模块代码。
与现有的 JavaScript 模块规范一样,检索模块的确切机制留给宿主环境(例如,Web 浏览器或 Node.js)。这是通过引入一个新的由宿主环境实现的抽象操作 HostPrepareImportedModule 以及重用和略微调整现有的 HostResolveImportedModule 来实现的。
(这种两层结构的宿主操作是为了保持 HostResolveImportedModule 始终同步返回的语义,并使用其参数的 [[RequestedModules]] 字段。这样,HostPrepareImportedModule 可以被视为动态填充 [[RequestedModules]] 字段的机制。这类似于某些宿主环境已经提前获取和评估模块树的方式,以确保模块评估期间的所有 HostResolveImportedModule 调用都能找到请求的模块。)
示例
在这里,您可以看到 import() 如何在一个非常简单的单页应用中实现导航时的懒加载模块:
注意这里与通常的 import 声明相比的差异:
import()可以从脚本中使用,而不仅仅是模块。- 如果在模块中使用
import(),它可以出现在任何层级,并且不会被提升。 import()接受任意字符串(此处显示的是运行时确定的模板字符串),而不仅仅是静态字符串字面量。- 模块中存在
import()并不会建立一个必须在包含它的模块被评估之前获取和评估的依赖关系。 import()不建立可以被静态分析的依赖关系。(但是,在像import("./foo.js")这样的简单情况下,实现仍然可能能够执行推测性获取。)
探索过的替代方案
还有许多其他可能实现上述用例的方式。在这里,我们解释为什么我们认为 import() 是最佳选择。
使用宿主特定的机制
在某些宿主环境(如 Web 浏览器)中,可以通过滥用宿主特定的机制来动态加载模块。使用 HTML 的 <script type="module">,以下代码可以提供与 import() 类似的功能:
然而,这有许多缺陷,除了创建临时全局变量并在稍后将其插入文档树中才删除的明显丑陋之外。
最明显的是,它接受一个 URL,而不是模块说明符;此外,该 URL 相对于文档的 URL,而不是相对于执行脚本。这给开发人员带来了不必要的阻抗失配,因为他们在使用不同的导入模块方式时需要切换上下文,并且这使得相对 URL 成为潜在的 bug 滋生地。
另一个明显的问题是这是宿主特定的。Node.js 代码不能使用上述函数,必须发明自己的函数,这可能具有不同的语义(可能基于文件名而不是 URL)。这导致代码不可移植。
最后,它没有被标准化,这意味着人们每次想要为他们的应用添加动态代码加载时,都需要引入或编写自己的版本。可以通过在 HTML 中将其添加为标准方法(window.importModule)来解决,但如果我们打算标准化某些东西,不如标准化 import(),出于上述原因,它更好。
一个真正的函数
Loader 想法集合的草案在不同时期有真正命名为 System.import() 或 System.loader.import() 或类似的函数(不仅仅是类函数的语法形式),它们实现了相同的用例。
这里最大的问题,正如规范编辑者之前指出的,是如何解释这些函数的说明符参数。由于这些只是函数,它们在所有 Realm 中都是相同的,并且不会因脚本或模块而异,因此该函数必须从任何地方调用都相同地解释其参数。(除非实现了真正奇怪的东西,如堆栈检查。)因此,这很可能遇到与上面 importModule 函数的文档基本 URL 问题类似的问题,其中相对模块说明符会成为 bug 滋生地,并与附近的 import 声明不匹配。
一种新的绑定形式
在 2016 年 7 月的 TC39 会议上,在 讨论 嵌套 import 声明 的提案时,原始提案被否决,但提出了 await import 的替代方案作为潜在的前进路径。这将是一种新的绑定形式(即,一种将名称引入给定作用域的新方式),并且只能在异步函数内工作。
await import 尚未完全开发,因此很难说它的目标和能力在多大程度上与本提案重叠。然而,我的印象是它将与本提案互补;它介于静态顶层 import 语法和 import() 提供的完全动态性之间。
例如,在 TC39 明确表示,由 await import 创建的 promise 永远不会被具体化。这提供了更简单的编程体验,但 import() 返回的具体化的 promise 允许强大的技术,例如使用 promise 组合器来竞争不同的模块或并行加载模块。这种显式的 promise 创建允许 import() 在非异步函数上下文中使用,而(像正常的 await 表达式一样)await import 则受限制。目前也不清楚 await import 是否允许任意字符串作为模块说明符,还是坚持现有的只允许字符串字面量的顶层 import 语法。
我的理解是,await import 更适用于静态情况,允许它与打包和树摇动工具集成,同时仍然允许一些懒获取和评估。然后,import() 可以用作最低级别、最强大的构建块。
与现有工作的关系
到目前为止,模块工作已在三个方面展开:
- JavaScript 规范,主要定义模块的语法,通过 HostResolveImportedModule 将重活委托给宿主环境;
- HTML 标准,定义
<script type="module">,如何 获取模块,并通过在这些基础之上指定 HostResolveImportedModule 来履行其作为宿主环境的职责; - Loader 规范,它是一个有趣想法的集合,原型化了创建运行时配置的加载管道和反射式创建模块(即,不是从源代码文本)的方法。
本提案将是对现有 JavaScript 和 HTML 功能的小幅扩展,使用相同的框架在 JavaScript 规范中指定语法形式,并将其繁重工作委托给宿主环境。HTML 的获取和解析模块的基础设施将用于定义其侧的故事。类似地,Node.js 将提供自己的 HostPrepareImportedModule 和 HostResolveImportedModule 定义以使此提案在那里工作。
Loader 规范中的想法将基本保持不变,尽管这可能会取代当前的 System.loader.import() 提案,或使 System.loader.import() 成为在特殊情况下使用的较低级别版本。Loader 规范将继续原型化更通用的可插拔加载管道和反射式模块的想法,随着时间的推移,这些想法可以用于推广 HTML 和 Node 的宿主特定管道。
具体来说,本仓库旨在作为一个 TC39 提案,通过阶段流程推进,指定 import() 语法和相关宿主环境钩子。它还包含 对 HTML 标准的拟议变更大纲,该大纲将与本提案集成。