方法1:使用 ES 模块封装器


在 CommonJS 中编写包或将 ES 模块源代码转换为 CommonJS,并创建定义命名导出的 ES 模块封装文件。 使用条件导出, import 使用 ES 模块封装器,require 使用 CommonJS 入口点。

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    "import": "./wrapper.mjs",
    "require": "./index.cjs"
  }
}

前面的示例使用显式扩展 .mjs.cjs。 如果你的文件使用 .js 扩展名,"type": "module" 会导致这些文件被视为 ES 模块,就像 "type": "commonjs" 会导致它们被视为 CommonJS。 参阅启用

// ./node_modules/pkg/index.cjs
exports.name = 'value';
// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;

在这个例子中,import { name } from 'pkg' 中的 nameconst { name } = require('pkg') 中的 name 是相同的单例。 因此,当比较两个 name 时,=== 返回 true,避免了发散说明符的危险。

如果模块不是简单的命名导出列表,而是包含独特的函数或对象导出,如 module.exports = function () { ... },或者如果需要封装器支持 import pkg from 'pkg' 模式,则封装器将被编写为可选地导出默认值以及任何命名的导出:

import cjsModule from './index.cjs';
export const name = cjsModule.name;
export default cjsModule;

此方法适用于以下任何用例:

  • 该包目前是用 CommonJS 编写的,作者不希望将其重构为 ES 模块语法,而是希望为 ES 模块使用者提供命名导出。
  • 该包还有其他依赖它的包,最终用户可能会同时安装这个包和那些其他包。 比如 utilities 包直接在应用中使用,utilities-plus 包给 utilities 增加了一些功能。 因为封装器导出了底层的 CommonJS 文件,所以 utilities-plus 是用 CommonJS 还是 ES 模块语法编写的并不重要;无论哪种方式都可以。
  • 包存储内部状态,包作者宁愿不重构包以隔离其状态管理。 请参阅下一章节。

此方法的变体不需要消费者有条件导出,可以添加一个导出,例如 "./module",指向包的全 ES 模块语法版本。 如果用户确定 CommonJS 版本不会在应用程序的任何地方加载,例如通过依赖项,或者如果可以加载 CommonJS 版本但不影响 ES 模块版本(例如, 因为包是无状态的):

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    ".": "./index.cjs",
    "./module": "./wrapper.mjs"
  }
}

Write the package in CommonJS or transpile ES module sources into CommonJS, and create an ES module wrapper file that defines the named exports. Using Conditional exports, the ES module wrapper is used for import and the CommonJS entry point for require.

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    "import": "./wrapper.mjs",
    "require": "./index.cjs"
  }
}

The preceding example uses explicit extensions .mjs and .cjs. If your files use the .js extension, "type": "module" will cause such files to be treated as ES modules, just as "type": "commonjs" would cause them to be treated as CommonJS. See Enabling.

// ./node_modules/pkg/index.cjs
exports.name = 'value';
// ./node_modules/pkg/wrapper.mjs
import cjsModule from './index.cjs';
export const name = cjsModule.name;

In this example, the name from import { name } from 'pkg' is the same singleton as the name from const { name } = require('pkg'). Therefore === returns true when comparing the two names and the divergent specifier hazard is avoided.

If the module is not simply a list of named exports, but rather contains a unique function or object export like module.exports = function () { ... }, or if support in the wrapper for the import pkg from 'pkg' pattern is desired, then the wrapper would instead be written to export the default optionally along with any named exports as well:

import cjsModule from './index.cjs';
export const name = cjsModule.name;
export default cjsModule;

This approach is appropriate for any of the following use cases:

  • The package is currently written in CommonJS and the author would prefer not to refactor it into ES module syntax, but wishes to provide named exports for ES module consumers.
  • The package has other packages that depend on it, and the end user might install both this package and those other packages. For example a utilities package is used directly in an application, and a utilities-plus package adds a few more functions to utilities. Because the wrapper exports underlying CommonJS files, it doesn't matter if utilities-plus is written in CommonJS or ES module syntax; it will work either way.
  • The package stores internal state, and the package author would prefer not to refactor the package to isolate its state management. See the next section.

A variant of this approach not requiring conditional exports for consumers could be to add an export, e.g. "./module", to point to an all-ES module-syntax version of the package. This could be used via import 'pkg/module' by users who are certain that the CommonJS version will not be loaded anywhere in the application, such as by dependencies; or if the CommonJS version can be loaded but doesn't affect the ES module version (for example, because the package is stateless):

// ./node_modules/pkg/package.json
{
  "type": "module",
  "exports": {
    ".": "./index.cjs",
    "./module": "./wrapper.mjs"
  }
}