跳到主要内容
How to convert CommonJS to ESM

如何将 CommonJS 转换为 ESM

ECMAScript 模块(“ESM”)是编写和共享 JavaScript 的官方现代方式——它在许多环境(例如浏览器、边缘计算和 Deno 等现代运行时)中都受支持,并提供更好的开发体验(例如异步加载和无需全局变量即可导出)。虽然 CommonJS 多年来一直是标准,但今天支持 CommonJS 正在损害 JavaScript 社区

所有新的 JavaScript 都应该用 ESM 编写,以面向未来。但是,在许多情况下,为了与较新的软件包兼容,需要对遗留代码库进行现代化改造。在这篇博文中,我们将向您展示如何 迁移遗留 CommonJS 项目的语法以支持 ESM,以及帮助简化该过程的工具。

想要编写现代 JavaScript 和 TypeScript,而无需繁琐的配置或样板代码吗?

了解一下 Deno,一个“开箱即用”、默认安全一体化工具链,用于 JavaScript 开发,并原生支持 TypeScriptWeb 标准 API

模块导入和导出

以下是如何更新从 CommonJS 到 ESM 的导入和导出语法。

在导出方面

- function addNumbers(num1, num2) {
+ export function addNumbers(num1, num2) {
  return num1 + num2;
};

- module.exports = {
-   addNumbers,
- }

在导入方面

- const { addNumbers } = require("./add_numbers");
+ import { addNumbers } from "./add_numbers.js");

console.log(addNumbers(2, 2));

请注意,在 ESM 中,文件扩展名必须包含在模块路径中。完全指定的导入通过确保始终通过模块解析过程导入正确的文件来减少歧义。此外,它与浏览器处理模块导入的方式一致,从而更容易编写可预测和可维护的同构代码。

那么条件导入呢?如果您使用的是 Node.js v14.8 或更高版本(或 Deno),那么您将可以使用顶层 await,您可以使用它来使 import 同步

- const module = boolean ? require("module1") : require("module2");
+ const module = await (boolean ? import("module1") : import("module2"));

更新 package.json

如果您正在使用 package.json,您需要进行一些调整以支持 ESM

{
  "name": "my-project",
+ "type": "module",
- "main": "index.js",
+ "exports": "./index.js",
  // ...
}

请注意,ESM 中开头的 "./" 是必要的,因为每个引用都必须使用完整的路径名,包括目录和文件扩展名。

此外,"main""exports" 都定义了项目的入口点。但是,"exports""main" 的现代替代方案,因为它使作者能够通过允许多个入口点、支持环境之间的条件入口解析以及阻止 "exports" 中定义的入口点之外的其他入口点来清晰地定义其软件包的公共接口。

{
  "name": "my-project",
  "type": "module",
  "exports": {
    ".": "./index.js",
    "./other": "./other.js"
  }
}

最后,告诉 Node 在 ESM 中运行文件的另一种方法是使用 .mjs 文件扩展名。如果您只想将单个文件更新为 ESM,这非常棒。但是,如果您的目标是转换整个代码库,则更新 package.json 中的 type 会更容易。

其他更改

由于 ESM 中的 JavaScript 将自动在 严格模式 下运行,因此您可以从代码库中删除所有 "use strict"; 实例

- "use strict";

CommonJS 还支持一些 ESM 中不存在的内置全局变量,例如 __dirname__filename。一种简单的解决方法是使用快速 shim 来填充这些值

// Node 20.11.0+, Deno 1.40.0+
const __dirname = import.meta.dirname;
const __filename = import.meta.filename;

// Previously
const __dirname = new URL(".", import.meta.url).pathname;

import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);

迁移工具

虽然上面提到了将 CommonJS 代码库转换为 ESM 代码库所需的更改,但有一些工具可以帮助完成该过渡。

使用 VSCode,您可以快速将所有导入和导出语句从 CommonJS 转换为 ESM。只需将鼠标悬停在 require 关键字上,点击“快速修复”,该文件中的所有这些语句都将更新为 ESM

VSCode 提供了将 CommonJS require 转换为 ESM 导入的快速修复。

您会注意到 VSCode 可以替换用于导入和导出的正确关键字,但说明符缺少文件名扩展名。您可以通过运行 deno lint --fix 来快速添加它们。Deno 的 linter 带有 no-sloppy-imports 规则,当导入路径不包含文件扩展名时,将显示 linting 错误。

对于将 CommonJS 转换为 ESM 的更端到端的方法,有一些转译选项。有 CLI 工具 ts2esm,它可以将 CJS 转换为 ESM,并包含 分步说明,甚至还有 一个漂亮的视频演练

还有一些工具,例如 cjs2esmcjstoesm,以及 Babel 插件 babel-plugin-transform-commonjs,但这些工具并未得到积极维护,因此在评估它们时请记住这一点。

接下来是什么

ESM 是共享代码的标准 JavaScript 方式,所有新的 JavaScript 都应该支持它。今天选择支持 CommonJS 对于不想排除遗留兼容性问题的模块作者和开发人员来说可能非常痛苦。事实上,JSR,我们的开源现代 JavaScript 注册表明确禁止使用 CommonJS 的模块。我们敦促每个人都尽自己的一份力量来提升 JavaScript 生态系统。

🚨️ 立即试用 Deno 2。 🚨️

Deno 提供与 Node/npm 的向后兼容性、内置包管理、一体化零配置工具链原生 TypeScript 支持 等。