Anthony Fu @ antfu.me BlogProjects Talks Sponsors

拥抱 ESM-only:是时候转变了

2月5日 · 15分钟

三年前,我写了一篇文章,关于在一个包中发布 ESM & CJS,提倡同时使用 CJS/ESM 格式,以方便用户迁移,并试图充分利用两者的优点。当时,我并不完全同意激进地发布 ESM-only 包,因为我认为生态系统还没有准备好,特别是这种推动主要来自底层库。随着时间的推移,随着工具和生态系统的发展,我的观点逐渐转向更多地采用 ESM-only。

截至 2025 年,自从 2015 年首次引入 ESM 以来,已经过去了十年。现代工具和库越来越多地采用 ESM 作为主要的模块格式。根据 WOOORM脚本,2021 年在 npm 上发布 ESM 的包占比为 7.8%,到 2024 年底,已经达到 25.8%。尽管很大一部分包仍然使用 CJS,但趋势明显表明正在向 ESM 转变。

ESM 随时间推移的采用情况,由 npm-esm-vs-cjs 脚本生成。最后更新于 2024-11-27

在这篇文章中,我想分享我对当前生态系统状态的看法,以及为什么我认为现在是时候转向 ESM-only 了。

工具链已准备就绪 #

现代工具 #

随着 Vite 作为一种流行的现代前端构建工具的崛起,许多元框架(如 NuxtSvelteKitAstroSolidStartRemixStorybookRedwood 等等)现在都建立在 Vite 之上,它们将 ESM 视为一等公民

作为补充,我们还有测试库 Vitest,它从一开始就为 ESM 设计,具有强大的模块模拟能力和高效的细粒度缓存支持。

tsxjiti 这样的 CLI 工具为运行 TypeScript 和 ESM 代码提供了无缝体验,而无需额外的配置。这简化了开发过程,并减少了设置项目以使用 ESM 的开销。

其他工具,例如 ESLint,在最近的 v9.0 中,引入了一个新的扁平配置系统,该系统通过 eslint.config.mjs 实现了原生 ESM 支持,即使在 CJS 项目中也是如此。

自上而下 & 自下而上 #

早在 2021 年,当 SINDRESORHUS 首次开始将其所有包迁移到 ESM-only 时,例如 find-upexeca,这是一个大胆的举动。我认为此举是自下而上的方法,因为这些包相当底层,并且它们的许多依赖项尚未为 ESM 做好准备。我担心这会迫使这些依赖项停留在旧版本的包上,这可能会导致生态系统碎片化。(截至今天,我实际上很欣赏这一举动,它为我们带来了很多高质量的 ESM 包,尽管这个过程并不是非常顺利)。

ESM 或双格式的包依赖 CJS 包要容易得多,反之则不然。就平稳采用而言,我相信自上而下的方法在推动生态系统向前发展方面更有效。在自上而下的高级框架和工具的支持下,使用 ESM-only 包不再是一个重大障碍。ESM 采用方面剩余的挑战主要在于包作者需要迁移并以 ESM 格式发布其代码。

Node.js 中 Requiring ESM #

Node.js 中 能够 require() ESM 模块,由 JOYEECHEUNG 发起,标志着一个令人难以置信的里程碑。此功能允许将包发布为 ESM-only,同时仍然可以通过最少的修改被 CJS 代码库使用。它有助于避免动态 import() ESM 引入的 async infection (也称为 Red Functions,在某些情况下,迁移和适配非常困难,甚至是不可能的。

此功能最近已 取消标记反向移植到 Node.js v22并且很快会移植到 v20),这意味着它应该已经可供许多开发人员使用。考虑到 自上而下或自下而上 的隐喻,此功能实际上使得从 中间向外 开始 ESM 迁移成为可能,因为它允许像 ESM → CJS → ESM → CJS 这样的导入链无缝工作。

为了解决这种情况下 CJS 和 ESM 之间的互操作问题,Node.js 还引入了 ESM 中的一种新语法 export { Foo as 'module.exports' } 来导出 CJS 兼容的导出(通过 [此 PR](https://antfu.me/posts/https:/github.com/nodejs/node/pull/54563))。这允许包作者发布 ESM-only 包,同时仍然支持 CJS 用户,甚至不会引入重大更改(除了更改所需的 Node.js 版本)。

有关此功能的进展和讨论的更多详细信息,请跟踪 此 issue

双格式的麻烦 #

虽然双 CJS/ESM 包一直是一种非常有用的过渡机制,但它们也带来了一系列挑战。维护两种不同的格式可能很麻烦且容易出错,尤其是在处理复杂的代码库时。以下是维护双格式时出现的一些问题:

互操作问题 #

从根本上讲,CJS 和 ESM 是具有不同设计理念的不同模块系统。尽管 Node.js 已经可以在 ESM 中导入 CJS 模块,在 CJS 中动态导入 ESM,甚至 require() ESM 模块,但仍然存在许多棘手的情况可能导致互操作问题。

一个关键的区别是 CJS 通常使用单个 module.exports 对象,而 ESM 同时支持默认导出和命名导出。在 ESM 中编写代码并转译为 CJS 时,处理导出可能特别具有挑战性,尤其是在导出的值是非对象(例如函数或类)时。此外,为了使类型正确,我们还需要引入 .d.mts.d.cts 声明文件带来的进一步复杂性。等等…

当我试图更深入地解释这个问题时,我发现我实际上希望您根本不需要为这个问题烦恼。坦率地说,它太复杂且令人沮丧了。如果您只是包的用户,更不用说让包的作者担心了。这是我提倡整个生态系统过渡到 ESM 的原因之一,以摆脱这些问题,并使每个人免受这种不必要的麻烦。

依赖解析 #

当一个包同时具有 CJS 和 ESM 格式时,依赖项的解析可能会变得很复杂。例如,如果一个包依赖于另一个仅发布 ESM 的包,则使用者必须确保使用 ESM 版本。这可能会导致版本冲突和依赖解析问题,尤其是在处理传递依赖时。

此外,对于设计为与单例模式一起使用的包,这可能会引入同一包的多个副本,并导致意外行为。

包大小 #

发布双格式本质上会使包大小加倍,因为需要包括 CJS 和 ESM 包。虽然对于单个包而言,多出几 KB 可能看起来并不重要,但在具有数百个依赖项的项目中,开销会迅速增加,从而导致臭名昭著的 node_modules 膨胀。因此,包作者应密切关注其包大小。转向 ESM-only 是一种优化它的方法,尤其是在包对 CJS 没有强烈要求的情况下。

何时应该迁移到 ESM-only? #

这篇文章的目的不是要减少双格式发布的价值。相反,我想鼓励评估生态系统的当前状态以及过渡到 ESM-only 的潜在好处。

在决定是否迁移到 ESM-only 时,需要考虑以下几个因素:

新包 #

我强烈建议所有新包都以 ESM-only 形式发布,因为没有遗留依赖项需要考虑。新的采用者可能已经在使用现代的、ESM 就绪的堆栈,因此仅使用 ESM 不应影响采用。此外,维护单个模块系统简化了开发,减少了维护开销,并确保您的包受益于未来的生态系统发展。

面向浏览器的包 #

如果一个包主要面向浏览器,那么以 ESM-only 形式发布是完全有道理的。在大多数情况下,浏览器包会通过 bundler,其中 ESM 在静态分析和 tree-shaking 方面提供了显着优势。这会导致更小、更优化的包,这也将提高加载性能并减少最终用户的带宽消耗。

独立 CLI #

对于独立的 CLI 工具,它是 ESM 还是 CJS 对最终用户没有区别。但是,使用 ESM 将使您的依赖项也能够使用 ESM,从而促进生态系统从 自上而下方法 过渡到 ESM。

Node.js 支持 #

如果一个包以常青的 Node.js 版本为目标,那么现在是考虑 ESM-only 的好时机,尤其是在最近的 require(ESM) 支持 下。

了解你的用户 #

如果一个包已经有某些用户,那么了解依赖项的状态和要求至关重要。例如,对于需要 ESLint v9 的 ESLint 插件/utils,虽然 ESLint v9 的新配置系统即使在 CJS 项目中也原生支持 ESM,但它以 ESM-only 形式发布没有任何障碍。

当然,不同的项目需要考虑不同的因素。但总的来说,我相信生态系统已经为更多包迁移到 ESM-only 做好准备,现在是评估过渡的收益和潜在挑战的好时机。

我们进展如何? #

过渡到 ESM 是一个渐进的过程,需要整个生态系统的协作和努力。我相信我们正在朝着良好的方向前进。

为了提高 ESM 采用的透明度和可见性,我最近构建了一个可视化工具,名为 Node Modules Inspector,用于分析包的依赖项。它提供了对依赖项的 ESM 采用状态的深入了解,并有助于识别迁移到 ESM 时的潜在问题。

以下是一些工具的屏幕截图,可让您快速了解:

Node Modules Inspector - 概述

Node Modules Inspector - 依赖关系图

Node Modules Inspector - 诸如 ESM 采用情况和重复包之类的报告

该工具仍处于早期阶段,但我希望它将成为包作者和维护人员的宝贵资源,以跟踪其依赖项的 ESM 采用进度,并做出关于过渡到 ESM-only 的明智决策。

要了解有关如何使用它和检查您的项目的更多信息,请查看存储库 node-modules-inspector

展望未来 #

我计划逐步将我维护的包过渡到 ESM-only,并仔细研究我们依赖的依赖项。我们对 Node Modules Inspector 也有很多令人兴奋的想法,旨在提供更有用的见解并帮助找到最佳前进方向。

我期待一个更便携、更具弹性和优化的 JavaScript/TypeScript 生态系统。

我希望这篇文章能阐明迁移到 ESM-only 的好处以及生态系统的当前状态。如果您有任何想法或问题,请随时使用下面的链接与我联系。感谢您的阅读!

comment on bluesky / mastodon / twitter cd ..

CC BY-NC-SA 4.0 2021-PRESENT © Anthony Fu