方法2:隔离状态


package.json 文件可以直接定义单独的 CommonJS 和 ES 模块入口点:

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

如果包的 CommonJS 和 ES 模块版本相同,则可以这样做,例如因为一个是另一个的转译输出;并且包的状态管理被仔细隔离(或包是无状态的)。

状态是一个问题的原因是因为包的 CommonJS 和 ES 模块版本都可能在应用程序中使用;例如,用户的应用程序代码可以 import ES 模块版本,而依赖项 require 是 CommonJS 版本。 如果发生这种情况,包的两个副本将被加载到内存中,因此将出现两个不同的状态。 这可能会导致难以解决的错误。

除了编写无状态的包(例如,如果 JavaScript 的 Math 是一个包,它将是无状态的,因为它的所有方法都是静态的),还有一些方法可以隔离状态,以便在潜在加载的 CommonJS 和 ES 模块之间共享包的实例:

  1. 如果可能,在实例化对象中包含所有状态。 例如,JavaScript 的 Date 需要实例化以包含状态;如果它是包,则它会像这样使用:

    import Date from 'date';
    const someDate = new Date();
    // someDate 包含状态;Date 不包含

    new 关键字不是必需的;包的函数可以返回新的对象,或修改传入的对象,以保持包外部的状态。

  2. 在包的 CommonJS 和 ES 模块版本之间共享的一个或多个 CommonJS 文件中隔离状态。 比如 CommonJS 和 ES 模块入口点分别是 index.cjsindex.mjs

    // ./node_modules/pkg/index.cjs
    const state = require('./state.cjs');
    module.exports.state = state;
    // ./node_modules/pkg/index.mjs
    import state from './state.cjs';
    export {
      state
    };

    即使 pkg 在应用程序中通过 requireimport 使用(例如,通过应用程序代码中的 import 和依赖项通过 requirepkg 的每个引用都将包含相同的状态;并且从任一模块系统修改该状态将适用二者皆是。

任何附加到包单例的插件都需要分别附加到 CommonJS 和 ES 模块单例。

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

  • 该包目前是用 ES 模块语法编写的,包作者希望在支持此类语法的任何地方使用该版本。
  • 包是无状态的,或者它的状态可以很容易地被隔离。
  • 该包不太可能有其他依赖它的公共包,或者如果有,则该包是无状态的,或者具有不需要在依赖项之间或与整个应用程序共享的状态。

即使处于隔离状态,在 CommonJS 和 ES 模块版本之间仍然存在可能执行额外代码的成本。

与之前的方法一样,这种方法的变体不需要消费者有条件的导出,可以添加一个导出,例如 "./module",指向包的全 ES 模块语法版本:

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

A package.json file can define the separate CommonJS and ES module entry points directly:

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

This can be done if both the CommonJS and ES module versions of the package are equivalent, for example because one is the transpiled output of the other; and the package’s management of state is carefully isolated (or the package is stateless).

The reason that state is an issue is because both the CommonJS and ES module versions of the package might get used within an application; for example, the user’s application code could import the ES module version while a dependency requires the CommonJS version. If that were to occur, two copies of the package would be loaded in memory and therefore two separate states would be present. This would likely cause hard-to-troubleshoot bugs.

Aside from writing a stateless package (if JavaScript’s Math were a package, for example, it would be stateless as all of its methods are static), there are some ways to isolate state so that it’s shared between the potentially loaded CommonJS and ES module instances of the package:

  1. If possible, contain all state within an instantiated object. JavaScript’s Date, for example, needs to be instantiated to contain state; if it were a package, it would be used like this:

    import Date from 'date';
    const someDate = new Date();
    // someDate contains state; Date does not

    The new keyword isn’t required; a package’s function can return a new object, or modify a passed-in object, to keep the state external to the package.

  2. Isolate the state in one or more CommonJS files that are shared between the CommonJS and ES module versions of the package. For example, if the CommonJS and ES module entry points are index.cjs and index.mjs, respectively:

    // ./node_modules/pkg/index.cjs
    const state = require('./state.cjs');
    module.exports.state = state;
    // ./node_modules/pkg/index.mjs
    import state from './state.cjs';
    export {
      state
    };

    Even if pkg is used via both require and import in an application (for example, via import in application code and via require by a dependency) each reference of pkg will contain the same state; and modifying that state from either module system will apply to both.

Any plugins that attach to the package’s singleton would need to separately attach to both the CommonJS and ES module singletons.

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

  • The package is currently written in ES module syntax and the package author wants that version to be used wherever such syntax is supported.
  • The package is stateless or its state can be isolated without too much difficulty.
  • The package is unlikely to have other public packages that depend on it, or if it does, the package is stateless or has state that need not be shared between dependencies or with the overall application.

Even with isolated state, there is still the cost of possible extra code execution between the CommonJS and ES module versions of a package.

As with the previous approach, 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:

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