这应该是全网最详细的 Babel 转换 ESM 到 CJS 的配置指南

219 阅读16分钟

背景

ESM(ES 模块)走向广泛支持的道路比较漫长。

ES 模块在 2015 年标准化,到 2017 年大多数浏览器开始支持,2019 年 Node.js v12 开始支持。在这段时间里,虽然大家都知道 ESM 是 JavaScript 模块的未来,但能直接使用它的运行环境却很少。

为了让开发者能尽快的使用标准化的模块化规范写代码,同时也为将来全面拥抱 ESM 打下基础,Babel 等工具允许用 ESM 编写代码,然后将其转换为其他可以在 Node.js 或浏览器中使用的模块格式。比如 ESM 转 CJS (CommonJS)、ESM 转 UMD(Universal Module Definition) 等。

在 Babel 中将 ESM 的代码转为 CJS 的代码是通过插件实现的,该插件为 @babel/plugin-transform-modules-commonjs

这个插件将 ECMAScript 模块(ESM)转换为 CommonJS 模块。需要注意的是,它只转换 importexport 语句的语法(例如 import "./mod.js")以及动态导入表达式(例如 import('./mod.js')),因为 Babel 并不了解 ESM 和 CommonJS 之间不同的模块解析算法。

模块的实际解析和加载逻辑,依赖于运行环境的具体实现。

下面具体介绍一下 @babel/plugin-transform-modules-commonjs 插件的用法。

@babel/plugin-transform-modules-commonjs 插件的具体用法

@babel/plugin-transform-modules-commonjs 插件支持 5 个选项配置,分别为:

  • importInterop

  • loose

  • strict

  • lazy

  • noInterop

importInterop

我们知道 CommonJS 模块和 ECMAScript 模块并非完全兼容。不过,编译器、打包工具以及 JavaScript 运行时环境都制定了不同的策略,以尽可能让它们协同工作。

importInterop 选项用于指定 Babel 应该使用哪种互操作策略。

importInterop 可以传入的值有:babelnodenone 和函数,默认为 babel

函数为 (specifier: string, requestingFilename: string | undefined) => "babel" | "node" | "none" 的形式。例如,当 Babel 编译一个包含 import { a } from 'b' 的文件,该文件的路径为 /full/path/to/foo.js 时,specifierbrequestingFilename/full/path/to/foo.js

babel

importInterop 选项的默认值是 babel

当 ESM 转 CJS ,并使用了导出(export)的模块中,转换后的模块代码会导出一个不可枚举的 __esModule 属性。随后,该属性会被用于判断导入的是默认导出,还是包含默认导出的情况。该属性的主要作用是用于在 CJS 中模拟 ESM 的默认导出行为。

例如以下这个例子,该例子的目录结构如下:

43.png

Babel 的配置文件 babel.config.json 的代码如下,此配置的作用是将 ESM 格式的代码转为 CJS 格式的,互操作策略(importInterop)为 babel

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-modules-commonjs",
      {
        "importInterop": "babel"
      }
    ]
  ]
}

package.json 文件如下,其中包含了本例子的依赖包和 npm script 命令,运行 npm run babel 命令,会使用 Babel 将 src 文件夹下的文件编译到 lib 文件夹下:

{
  "name": "babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src --out-dir lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.26.4",
    "@babel/core": "^7.26.0",
    "@babel/plugin-transform-modules-commonjs": "^7.26.3",
    "@babel/preset-env": "^7.26.0",
    "core-js": "^3.39.0"
  }
}

src 文件夹下的文件的代码很简单:

// src/index.js
import add from "./util";

console.log("total ", add(3, 3));
// src/util.js
function add(a, b) {
  return a + b;
}
export default add;

然后在终端运行 npm run babel 命令,lib 文件夹下的编译结果如下:

// lib/index.js

"use strict";

var _util = _interopRequireDefault(require("./util"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
console.log("total ", (0, _util.default)(3, 3));
// lib/util.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

lib 文件夹下的编译结果可知,lib/util.js 文件导出了不可枚举的 __esModule 属性。在 ESM 中的默认导出(export defaultadd 函数被挂到了 exports.default 下。

lib/index.js 中可看出,为了模拟 ESM 的默认导出,代码中出现了 _interopRequireDefault 辅助函数,该函数的作用是用于模拟 ESM 的默认导入,该函数会判断传入的模块 e__esModule 属性是否为 true ,如果为 true 则直接返回该模块,否则将该模块包装到一个新的对象中,并将原模块赋值给 default 属性:

function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }

我们再看一个纯 ESM 默认导入的例子,也许大家会对 _interopRequireDefault 函数的作用有更加深刻的理解,该例子的目录结构为:

44.png

package.json 用于告诉 Node.js ,这是个 ES 模块:

{
  "type": "module"
}

index.js 代码为:

import * as u from "./util.js";

console.log("total ", u.default(3, 3));

util.js 代码为:

function add(a, b) {
  return a + b;
}
export default add;

index.js 中整体导入了 util 模块,然在 index.js 中打上断点,并借助 VSCode 的调试工具,可以看到,util 模块的默认导出挂在了 default 上:

45.png

当使用 babel 这种导入互操作时(即 importInterop 设置为 babel),如果被导入的模块和导入模块都通过 Babel 进行了编译,那么它们的行为表现就好像二者都没有被编译过一样。

在用 Babel 将 ESM 编译为 CJS 时,Babel 会在 CJS 中尽量模拟原始的 ES 模块行为,仿佛这些模块从未被编译成 CommonJS 模块,对用户尽量做到无感知。

node

在导入 CommonJS 文件时(无论是直接用 CommonJS 编写的文件,还是通过编译器生成的文件),Node.js 总是将默认导出绑定到 module.exports 的值上。

这一点与 Babel 原来的方案不同,Babel 在 ESM 转 CJS 中,将 ESM 的默认导出挂在 exports.default 上,当 Node.js 同时支持 ESM 和 CJS 时(旧版本的 Node.js 只支持 CJS), Node.js 是将 CJS 的默认导出挂在 module.exports 上。

Babel 为了兼容 Node.js 这样的行为,可将 importInterop 设置为 node

importInterop 设置为 node 时,编译后的代码不会有 _interopRequireDefault 辅助函数,对于默认导出,也不会取 default 属性的值。

但是 Babel 的这种方式不完美,如下面的例子,此例子的目录结构如下:

46.png

此例子的 Babel 配置 babel.config.json 为:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-modules-commonjs",
      {
        "importInterop": "node"
      }
    ]
  ]
}

package.json 文件如下,其中包含了本例子的依赖包和 npm script 命令,运行 npm run babel 命令,会使用 Babel 将 src 文件夹下的文件编译到 lib 文件夹下:

{
  "name": "babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src --out-dir lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.26.4",
    "@babel/core": "^7.26.0",
    "@babel/plugin-transform-modules-commonjs": "^7.26.3",
    "@babel/preset-env": "^7.26.0",
    "core-js": "^3.39.0"
  }
}

src/index.js 文件的代码如下:

import add from "./util.js";

console.log("total ", add(3, 3));

src/util.js 文件的代码如下:

function add(a, b) {
  return a + b;
}
export default add;

然后在终端运行 npm run babel 命令,lib 文件夹下的编译结果如下:

// lib/index.js

"use strict";

var _util = require("./util.js");
console.log("total ", _util(3, 3));
// lib/util.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

但是在终端使用 node 运行 lib/index.js ,发现报错了:

47.png

原因是 lib/util.js 将默认导出挂在 exports.default 上,而 lib/index.js 中没有从 default 属性中取值,因此报错了。

所以在日常开发中尽量不要设置 importInteropnode

none

如果你知道被导入的文件已经通过某个编译器(比如 Babel)进行了转换,且该编译器会将默认导出存储在 exports.default,则可将 importInterop 设置为 none

importInterop 设置为 none 后,编译后的代码也不会有 _interopRequireDefault 辅助函数。

例如以下这个例子,该例子的目录结构如下:

48.png

Babel 的配置文件 babel.config.json 的代码如下,此配置的作用是将 ESM 格式的代码转为 CJS 格式的,互操作策略(importInterop)为 none

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-modules-commonjs",
      {
        "importInterop": "none"
      }
    ]
  ]
}

package.json 文件如下,其中包含了本例子的依赖包和 npm script 命令,运行 npm run babel 命令,会使用 Babel 将 src 文件夹下的文件编译到 lib 文件夹下:

{
  "name": "babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src --out-dir lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.26.4",
    "@babel/core": "^7.26.0",
    "@babel/plugin-transform-modules-commonjs": "^7.26.3",
    "@babel/preset-env": "^7.26.0",
    "core-js": "^3.39.0"
  }
}

src 文件夹下的文件的代码很简单,跟上面的例子一样:

// src/index.js
import add from "./util";

console.log("total ", add(3, 3));
// src/util.js
function add(a, b) {
  return a + b;
}
export default add;

然后在终端运行 npm run babel 命令,lib 文件夹下的编译结果如下:

// lib/index.js

"use strict";

var _util = require("./util");
console.log("total ", (0, _util.default)(3, 3));
// lib/util.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

lib 文件夹下的编译结果可知,lib/util.js 文件也导出了不可枚举的 __esModule 属性。在 ESM 中的默认导出(export defaultadd 函数被挂到了 exports.default 下。

lib/index.js 中可以发现,没有出现 _interopRequireDefault ,但会从 default 属性中取得默认导出的值。

并且在终端可以使用 Node.js 正常运行 lib/index.js 文件:

49.png

在实际开发中,可依据实际情况,放心地设置 importInteropnone

loose

loose 默认为 false ,会将 __esModule 属性设置为不可枚举的。

在将 ES 模块(ESM)编译为 CommonJS(CJS)模块时,Babel 会在 module.exports 对象上定义一个 __esModule 属性。假设你从不使用 for..in 循环或 Object.keys 去遍历 module.exportsrequire("your-module") 的键,那么将 __esModule 属性定义为可枚举的就是安全的。

如下面的例子,此例子的目录结构如下:

50.png

Babel 的配置文件 babel.config.json 的代码如下,此配置的作用是将 ESM 格式的代码转为 CJS 格式的,互操作策略(importInterop)为 babelloose 为 true :

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-modules-commonjs",
      {
        "importInterop": "babel",
        "loose": true
      }
    ]
  ]
}

package.json 文件如下,其中包含了本例子的依赖包和 npm script 命令,运行 npm run babel 命令,会使用 Babel 将 src 文件夹下的文件编译到 lib 文件夹下:

{
  "name": "babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src --out-dir lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.26.4",
    "@babel/core": "^7.26.0",
    "@babel/plugin-transform-modules-commonjs": "^7.26.3",
    "@babel/preset-env": "^7.26.0",
    "core-js": "^3.39.0"
  }
}

src 文件夹下的文件的代码很简单:

// src/index.js
import add from "./util";

console.log("total ", add(3, 3));
// src/util.js
function add(a, b) {
  return a + b;
}
export default add;

然后在终端运行 npm run babel 命令,lib 文件夹下的编译结果如下:

// lib/index.js

"use strict";

var _util = _interopRequireDefault(require("./util"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
console.log("total ", (0, _util.default)(3, 3));
// lib/util.js

"use strict";

// 已经不是使用 Object.defineProperty 定义的不可枚举的 __esModule 属性了
exports.__esModule = true;
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

如果将上面的例子的 Babel 配置 loose 设置为 false ,则编译结果 lib/util.js 文件中,在 exports 对象中定义的 __esModule 是不可枚举的:

51.png

要注意的是,Babel 推荐使用 enumerableModuleMeta 编译器假设代替 loose 的配置。当 enumerableModuleMeta 设置为 true 时,编译后,exports 对象中定义的 __esModule 是可枚举的:

// 原文件
function add(a, b) {
  return a + b;
}
export default add;

// 编译结果
"use strict";

exports.__esModule = true;// __esModule 是可枚举的
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

默认情况下,Babel 会尝试编译你的代码,使其尽可能贴近原生行为。不过,这有时意味着要生成更多的输出代码,或者生成运行速度更慢的输出代码,仅仅是为了支持一些你并不在意的边缘情况。

从 Babel 7.13.0 版本开始,你可以在配置中指定一个 assumptions 选项,告知 Babel 它可以对你的代码做出哪些假设,以便更好地优化编译结果。

strict

strict 默认为 false 。

在 ESM 转 CJS 的场景下,在使用 Babel 并通过 exports 进行导出时,会导出一个不可枚举的 __esModule 属性。在某些情况下,该属性用于判断导入的是默认导出,还是包含默认导出的内容。

如果不想导出 __esModule 属性,你可以将 strict 选项设置为 true 。

例如以下这个例子,该例子的目录结构如下:

52.png

Babel 的配置文件 babel.config.json 的代码如下,将 strict 选项设置为 true:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-modules-commonjs",
      {
        "strict": true
      }
    ]
  ]
}

package.json 文件如下,其中包含了本例子的依赖包和 npm script 命令,运行 npm run babel 命令,会使用 Babel 将 src 文件夹下的文件编译到 lib 文件夹下:

{
  "name": "babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src --out-dir lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.26.4",
    "@babel/core": "^7.26.0",
    "@babel/plugin-transform-modules-commonjs": "^7.26.3",
    "@babel/preset-env": "^7.26.0",
    "core-js": "^3.39.0"
  }
}

src 文件夹下的文件的代码很简单:

import add from "./util";

console.log("total ", add(3, 3));
function add(a, b) {
  return a + b;
}
export default add;

然后在终端运行 npm run babel 命令,lib 文件夹下的编译结果如下:

// lib/index.js

"use strict";

var _util = _interopRequireDefault(require("./util"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
console.log("total ", (0, _util.default)(3, 3));
// lib/util.js

"use strict";

exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

从文件 lib/util.js 中可以看出,将 strict 选项设置为 true,已经不会导出 __esModule 属性了。

lazy

可选值为 booleanArray<string>(string) => boolean ,默认为 false

更改 Babel 编译后的导入语句,使其在首次使用导入的绑定内容时才进行加载。

这可以缩短模块的初始加载时间,因为预先计算依赖项有时完全没有必要。在编写提供给他人使用的库时,尤其如此。

lazy 的值有几种可能的效果:

  • false - 任何导入的模块都不会进行懒加载。

  • true - 不对本地的 ./foo 导入进行懒加载,但 foo 是项目的依赖包(通过 npm 安装的)会懒加载。

本地的模块之间可能存在循环依赖问题,如果进行懒加载,可能会出错,所以默认情况下它们不会懒加载,而项目的依赖模块(通过 npm 等下载的)之间很少会出现循环依赖的情况。

  • Array<string> - 提供一个字符串数组,其中每个字符串代表一个模块路径或名称。对于所有导入来源匹配这些字符串之一的模块,将会进行懒加载。

  • (string) => boolean - 提供一个接收字符串参数并返回布尔值的回调函数。这个回调函数会在需要确定某个特定的模块(通过其来源字符串标识)是否应该懒加载时被调用。根据回调函数的返回值(truefalse),系统会决定是否对该模块实行懒加载。

以下两种导入情况永远不会懒加载:

  • import "foo";

副作用导入自动是非懒加载的,因为这种导入的主要目的是为了确保模块中的代码被执行(例如,执行某些全局操作或修改全局状态),而不是为了从模块中导入特定的变量、函数或类。因此,副作用导入需要在程序启动时立即执行,以确保所有预期的副作用都能正确发生,而不会延迟到某个具体的绑定被使用时才加载。

  • export * from "foo"

模块整体转发没法懒加载,需要提前执行。因为否则没有办法知道哪些函数或变量(API)需要被导出。

接下来看看 lazy 使用的具体案例

lazy 为 true

lazy 为 true 不对本地的模块导入进行懒加载,但是项目的依赖包(通过 npm 等安装的)会懒加载。

此例子的目录结构如下:

53.png

Babel 的配置文件 babel.config.json 的代码如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-modules-commonjs",
      {
        "lazy": true
      }
    ]
  ]
}

package.json 文件如下,其中包含了本例子的依赖包和 npm script 命令,运行 npm run babel 命令,会使用 Babel 将 src 文件夹下的文件编译到 lib 文件夹下:

{
  "name": "babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src --out-dir lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.26.4",
    "@babel/core": "^7.26.0",
    "@babel/plugin-transform-modules-commonjs": "^7.26.3",
    "@babel/preset-env": "^7.26.0",
    "core-js": "^3.39.0"
  },
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

src 文件夹下的文件的代码很简单:

// src/index.js
import add from "./util";
import { concat } from "lodash";

console.log("total ", add(3, 3));
console.log("concat ", concat([1], 6));
// src/util.js
function add(a, b) {
  return a + b;
}
export default add;

然后在终端运行 npm run babel 命令,lib 文件夹下的编译结果如下:

// lib/index.js

"use strict";

var _util = _interopRequireDefault(require("./util"));
function _lodash() {
  const data = require("lodash");
  _lodash = function _lodash() {
    return data;
  };
  return data;
}
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
console.log("total ", (0, _util.default)(3, 3));
console.log("concat ", (0, _lodash().concat)([1], 6));
// lib/util.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

从文件 lib/index.js 可以知道,外部依赖包 lodash ,已经是懒加载的了,如下面的 _lodash 函数 ,当第一次调用 _lodash 函数时,lodash 会被加载一次,后续调用则直接返回已经加载好的模块,因此 lodash 只会被加载一次:

function _lodash() {
  const data = require("lodash");
  _lodash = function _lodash() {
    return data;
  };
  return data;
}

lazy 为 Array<string>

如果 lazy 传入的是字符串数组,则会对导入来源匹配到字符串数组的模块进行懒加载。

此例子的目录结构如下:

54.png

Babel 的配置文件 babel.config.json 的代码如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": "17",
          "firefox": "60",
          "chrome": "67",
          "safari": "11.1"
        },
        "useBuiltIns": "usage",
        "corejs": "3.6.5"
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-transform-modules-commonjs",
      {
        "lazy": ["./util"]
      }
    ]
  ]
}

package.json 文件如下,其中包含了本例子的依赖包和 npm script 命令,运行 npm run babel 命令,会使用 Babel 将 src 文件夹下的文件编译到 lib 文件夹下:

{
  "name": "babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src --out-dir lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.26.4",
    "@babel/core": "^7.26.0",
    "@babel/plugin-transform-modules-commonjs": "^7.26.3",
    "@babel/preset-env": "^7.26.0",
    "core-js": "^3.39.0"
  }
}

src 文件夹下的文件的代码很简单:

// src/index.js
import add from "./util";

console.log("total ", add(3, 3));
// src/util.js
function add(a, b) {
  return a + b;
}
export default add;

然后在终端运行 npm run babel 命令,lib 文件夹下的编译结果如下:

// lib/index.js

"use strict";

function _util() {
  const data = _interopRequireDefault(require("./util"));
  _util = function _util() {
    return data;
  };
  return data;
}
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
console.log("total ", (0, _util().default)(3, 3));
// lib/util.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

从文件 lib/index.js 可以知道,本地模块 util 是懒加载的了。

lazy 为 (string) => boolean

lazy 为回调函数,如果传入该回调函数的模块说明符返回 true ,则该模块会懒加载,否则不会。

此例子的目录结构如下:

55.png

Babel 的配置文件 babel.config.js (因为要定义函数,所以不能用 json 文件的形式)的代码如下:

module.exports = function (api) {
  api.cache(false);
  return {
    presets: [
      [
        "@babel/preset-env",
        {
          targets: {
            edge: "17",
            firefox: "60",
            chrome: "67",
            safari: "11.1",
          },
          useBuiltIns: "usage",
          corejs: "3.6.5",
        },
      ],
    ],
    plugins: [
      [
        "@babel/plugin-transform-modules-commonjs",
        {
          lazy: (specifier) => {
            return specifier === "./util";
          },
        },
      ],
    ],
  };
};

package.json 文件如下,其中包含了本例子的依赖包和 npm script 命令,运行 npm run babel 命令,会使用 Babel 将 src 文件夹下的文件编译到 lib 文件夹下:

{
  "name": "babel-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "babel": "babel src --out-dir lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.26.4",
    "@babel/core": "^7.26.0",
    "@babel/plugin-transform-modules-commonjs": "^7.26.3",
    "@babel/preset-env": "^7.26.0",
    "core-js": "^3.39.0"
  },
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

src 文件夹下的文件的代码很简单:

// src/index.js
import add from "./util";
import { concat } from "lodash";

console.log("total ", add(3, 3));
console.log("concat ", concat([1], 6));
// src/util.js
function add(a, b) {
  return a + b;
}
export default add;

然后在终端运行 npm run babel 命令,lib 文件夹下的编译结果如下:

// lib/index.js

"use strict";

function _util() {
  const data = _interopRequireDefault(require("./util"));
  _util = function _util() {
    return data;
  };
  return data;
}
var _lodash = require("lodash");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
console.log("total ", (0, _util().default)(3, 3));
console.log("concat ", (0, _lodash.concat)([1], 6));
// lib/util.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
function add(a, b) {
  return a + b;
}
var _default = exports.default = add;

从文件 lib/index.js 可以知道,本地模块 util 是懒加载的了。

noInterop

默认为 false ,Babel 官方推荐使用 importInterop 配置代替。

noInterop 设置为 true 时,与 importInterop 设置为 none 是等价的。

总结

由于 ESM 得到广泛支持的时间比较晚,为了让开发者尽快的使用标准化的模块化规范写代码,Babel 等工具允许用 ESM 编写代码,然后将其转换为 CJS 格式的。

Babel 是通过 @babel/plugin-transform-modules-commonjs 插件将 ESM 的代码转为 CJS 的。

如果大家在实际中有 ESM 转 CJS 的需求,可以借助 Babel 来实现,赶快收藏学习吧!