diff --git a/docs/documentation/zh/modules-reference/appendices/ESM-CJS-Interop.md b/docs/documentation/zh/modules-reference/appendices/ESM-CJS-Interop.md index 4f5bd1a..f9123fe 100644 --- a/docs/documentation/zh/modules-reference/appendices/ESM-CJS-Interop.md +++ b/docs/documentation/zh/modules-reference/appendices/ESM-CJS-Interop.md @@ -1,12 +1,12 @@ --- -title: Modules - ESM/CJS Interoperability -short: ESM/CJS Interoperability +title: 模块——ESM/CJS 互操作性 +short: ESM/CJS 互操作性 layout: docs permalink: /zh/docs/handbook/modules/appendices/esm-cjs-interop.html -oneline: A detailed look at interoperability between ES modules and CommonJS modules +oneline: 对 ES 模块和 CommonJS 模块之间的互操作性进行详细介绍 --- -It’s 2015, and you’re writing an ESM-to-CJS transpiler. There’s no specification for how to do this; all you have is a specification of how ES modules are supposed to interact with each other, knowledge of how CommonJS modules interact with each other, and a knack for figuring things out. Consider an exporting ES module: +现在是 2015 年,你正在开发一个转译器,该转译器能够将 ECMAScript 模块(ESM)转换为 CommonJS(CJS)模块。尽管目前尚无具体规范指导此功能的实现,但你可以依据现有的 ES 模块间交互规范、CommonJS 模块间交互知识,以及你自身的问题解决能力来进行开发。请考虑以下一个 ES 模块的导出示例: ```ts export const A = {}; @@ -14,7 +14,7 @@ export const B = {}; export default "Hello, world!"; ``` -How would you turn this into a CommonJS module? Recalling that default exports are just named exports with special syntax, there seems to be only one choice: +你该如何将其转换为 CommonJS 模块呢?回想一下,默认导出实际上只是具有特殊语法的命名导出,似乎只有一种选择: ```ts exports.A = {}; @@ -22,31 +22,31 @@ exports.B = {}; exports.default = "Hello, world!"; ``` -This is a nice analog, and it lets you implement a similar on the importing side: +这是一个很好的类比,借此你能够在导入侧实现类似的特性: ```ts import hello, { A, B } from "./module"; console.log(hello, A, B); -// transpiles to: +// 转译为: const module_1 = require("./module"); console.log(module_1.default, module_1.A, module_1.B); ``` -So far, everything in CJS-world matches up one-to-one with everything in ESM-world. Extending the equivalence above one step further, we can see that we also have: +到目前为止,CJS 中的每个东西都能在 ESM 中找到对应的存在。进一步扩展上述等式,我们还可以看到: ```ts import * as mod from "./module"; console.log(mod.default, mod.A, mod.B); -// transpiles to: +// 转译为: const mod = require("./module"); console.log(mod.default, mod.A, mod.B); ``` -You might notice that in this scheme, there’s no way to write an ESM export that produces an output where `exports` is assigned a function, class, or primitive: +你可能会注意到,在这种方案中,无法编写一个导出函数、类或基本类型值的 ESM 导出: ```ts // @Filename: exports-function.js @@ -55,114 +55,114 @@ module.exports = function hello() { }; ``` -But existing CommonJS modules frequently take this form. How might an ESM import, processed with our transpiler, access this module? We just established that a namespace import (`import *`) transpiles to a plain `require` call, so we can support an input like: +但是现有的 CommonJS 模块经常采用这种形式。我们的转译器处理的 ESM 导入如何访问这个模块呢?我们刚刚确认了命名空间导入(`import *`)会转译为普通的 `require` 调用,因此我们可以支持以下输入: ```ts import * as hello from "./exports-function"; hello(); -// transpiles to: +// 转译为: const hello = require("./exports-function"); hello(); ``` -Our output works at runtime, but we have a compliance problem: according to the JavaScript specification, a namespace import always resolves to a [_Module Namespace Object_](https://tc39.es/ecma262/#sec-module-namespace-objects), that is, an object whose members are the exports of the module. In this case, `require` would return the function `hello`, but `import *` can never return a function. The correspondence we assumed appears invalid. +虽然我们的输出在运行时可以正常工作,但是我们有一个规范上的问题:根据 JavaScript 规范,命名空间导入始终解析为[*模块命名空间对象*](https://tc39.es/ecma262/#sec-module-namespace-objects)(一个对象,其成员是模块的导出)。在这种情况下,`require` 将返回函数 `hello`,但是 `import *` 永远无法返回函数。我们之前假设的对应关系似乎是无效的。 -It’s worth taking a step back here and clarifying what the _goal_ is. As soon as modules landed in the ES2015 specification, transpilers emerged with support for downleveling ESM to CJS, allowing users to adopt the new syntax long before runtimes implemented support for it. There was even a sense that writing ESM code was a good way to “future-proof” new projects. For this to be true, there needed to be a seamless migration path from executing the transpilers’ CJS output to executing the ESM input natively once runtimes developed support for it. The goal was to find a way to downlevel ESM to CJS that would allow any or all of those transpiled outputs to be replaced by their true ESM inputs in a future runtime, with no observable change in behavior. +值得在这里退一步,弄清楚*目标*是什么。自从模块出现在 ES2015 规范中,支持将 ESM 转译为 CJS 的转译器很快就出现了。借此用户可以在运行时实现对新语法的支持之前就采用新语法。甚至有人认为,编写 ESM 代码是“面向未来”新项目的好方法。要做到这一点,就需要有一个无缝的迁移路径(从执行转译器的 CJS 输出迁移到运行时开发出对 ESM 输入的支持后直接执行 ESM 输入)。我们的目标是找到一种方法,可以将 ESM 降级为 CJS。同时,这种方法还应允许在未来的运行时中,用原始的 ESM 输入替换任何或所有这些转译输出,而不会在行为上产生任何可观察的变化。 -By following the specification, it was easy enough for transpilers to find a set of transformations that made the semantics of their transpiled CommonJS outputs match the specified semantics of their ESM inputs (arrows represent imports): +通过遵循规范,转译器很容易找到一组转换操作,使其转译的 CommonJS 输出的语义与其 ESM 输入的指定语义相匹配(箭头表示导入关系): -![A flowchart with two similar flows side-by-side. Left: ESM. Right: ESM transpiled to CJS. In the ESM flow: "Importing module" flows to "Imported module" through arrow labeled "specified behavior". In the ESM transpiled to CJS flow: "Importing module" flows to "Imported module" through arrow labeled "designed based on spec".](../diagrams/esm-cjs-interop.md-1.svg) +![一个包含两个相似流程的流程图,左边是 ESM,右边是 ESM 转换为 CJS。在 ESM 流程中:“导入模块”通过标有“指定行为”的箭头流向“被导入模块”。在 ESM 转换为 CJS 的流程中:“导入模块”通过标有“基于规范的设计”的箭头流向“被导入模块”。](../diagrams/esm-cjs-interop.md-1.svg) -However, CommonJS modules (written as CommonJS, not as ESM transpiled to CommonJS) were already well-established in the Node.js ecosystem, so it was inevitable that modules written as ESM and transpiled to CJS would start “importing” modules written as CommonJS. The behavior for this interoperability, though, was not specified by ES2015, and didn’t yet exist in any real runtime. +然而,CommonJS 模块(以 CommonJS 编写,而非 ESM 转译为 CommonJS)在 Node.js 生态系统中已经得到了很好的建立,因此不可避免地会出现 ESM 转译为 CJS 的模块“导入” CommonJS 模块的情况。然而,ES2015 并未对这种互操作性的行为进行规定,而且在任何实际运行时中也不存在这种行为。 -![A flowchart with three areas side-by-side. Left: ESM. Middle: True CJS. Right: ESM transpiled to CJS. Left: ESM "Importing module" flows to ESM "Imported module" through arrow labeled "specified behavior," and to True CJS "Imported module" through dotted arrow labeled "unspecified behavior." Right: ESM transpiled to CJS "Importing module" flows to ESM transpiled to CJS "Imported module" through arrow labeled "designed based on spec," and to True CJS "Imported module" through dotted arrow labeled "❓🤷‍♂️❓"](../diagrams/esm-cjs-interop.md-2.svg) +![一个并排显示的有三个区域的流程图。左侧是 ESM,中间是真正的 CJS,右侧是 ESM 转译为 CJS。左侧:ESM 的“导入模块”通过标有“指定行为”的箭头连接到 ESM 的“被导入模块”,并通过带有“未指定行为”的虚线箭头连接到真正的 CJS 的“被导入模块”。右侧:ESM 转译为 CJS 的“导入模块”通过标有“基于规范设计”的箭头连接到 ESM 转译为 CJS 的“被导入模块”,并通过带有“❓🤷‍♂️❓”的虚线箭头连接到真正的 CJS 的“被导入模块”](../diagrams/esm-cjs-interop.md-2.svg) -Even if transpiler authors did nothing, a behavior would emerge from the existing semantics between the `require` calls they emitted in transpiled code and the `exports` defined in existing CJS modules. And to allow users to transition seamlessly from transpiled ESM to true ESM once their runtime supported it, that behavior would have to match the one the runtime chose to implement. +即使转译器的作者什么也不做,已有的语义在转译代码中的 `require` 调用和现有的 CJS 模块中定义的 `exports` 之间仍然会产生一种行为。为了让用户能够在运行时支持时从转译的 ESM 到真正的 ESM 无缝过渡,这种行为必须与运行时选择实现的行为相匹配。 -Guessing what interop behavior runtimes would support wasn’t limited to ESM importing “true CJS” modules either. Whether ESM would be able to recognize ESM-transpiled-from-CJS as distinct from CJS, and whether CJS would be able to `require` ES modules, were also unspecified. Even whether ESM imports would use the same module resolution algorithm as CJS `require` calls was unknowable. All these variables would have to be predicted correctly in order to give transpiler users a seamless migration path toward native ESM. +对于运行时将支持的互操作性行为的猜测不仅限于 ESM 导入“真正的 CJS”模块。ESM 是否能够将从 CJS 转译而来的 ESM 与 CJS 区分开来,以及 CJS 是否能够 `require` ES 模块,这些也都未经规定。甚至 ESM 导入是否将使用与 CJS 的 `require` 调用相同的模块解析算法都是无法预知的。所有这些变数都必须准确地预测,以便转译器用户可以平滑迁移向 ESM。 -## `allowSyntheticDefaultImports` and `esModuleInterop` +## `allowSyntheticDefaultImports` 和 `esModuleInterop` -Let’s return to our specification compliance problem, where `import *` transpiles to `require`: +让我们回到我们之前提到的规范兼容性问题,其中 `import *` 被转译为 `require`: ```ts -// Invalid according to the spec: +// 根据规范是无效的: import * as hello from "./exports-function"; hello(); -// but the transpilation works: +// 但是转译是有效的: const hello = require("./exports-function"); hello(); ``` -When TypeScript first added support for writing and transpiling ES modules, the compiler addressed this problem by issuing an error on any namespace import of a module whose `exports` was not a namespace-like object: +当 TypeScript 首次引入对 ES 模块的编写和转译支持时,为了解决这个问题,如果对一个模块进行任何命名空间导入(而该模块的 `exports` 不是命名空间对象),编译器会报错: ```ts import * as hello from "./exports-function"; // TS2497 ^^^^^^^^^^^^^^^^^^^^ -// External module '"./exports-function"' resolves to a non-module entity -// and cannot be imported using this construct. +// 外部模块 '"./exports-function"' 解析为非模块实体, +// 不能使用此结构进行导入。 ``` -The only workaround was for users to go back to using the older TypeScript import syntax representing a CommonJS `require`: +唯一的解决方法是让用户回到使用旧的 TypeScript 导入语法来表示 CommonJS 的 `require`: ```ts import hello = require("./exports-function"); ``` -Forcing users to revert to non-ESM syntax was essentially an admission that “we don’t know how or if a CJS module like `"./exports-function"` will be accessible with ESM imports in the future, but we know it _can’t_ be with `import *`, even though it will work at runtime in the transpilation scheme we’re using.” It doesn’t meet the goal of allowing this file to be migrated to real ESM without changes, but neither does the alternative of allowing the `import *` to link to a function. This is still the behavior in TypeScript today when `allowSyntheticDefaultImports` and `esModuleInterop` are disabled. +强制用户回到非 ESM 语法实质上是承认了“我们不知道或者不确定像 `"./exports-function"` 这样的 CJS 模块在将来是否可以通过 ESM 导入访问,但我们知道它*不能*通过 `import *` 来实现,尽管在我们使用的转译方案中它在运行时可以工作。”这并不符合允许该文件在不改动的情况下迁移到真正的 ESM 的目标,但是允许 `import *` 与函数建立链接的替代方案也不符合这个目标。这仍然是 TypeScript 今天的行为(当禁用 `allowSyntheticDefaultImports` 和 `esModuleInterop` 时)。 -> Unfortunately, this is a slight oversimplification—TypeScript didn’t fully avoid the compliance issue with this error, because it allowed namespace imports of functions to work, and retain their call signatures, as long as the function declaration merged with a namespace declaration—even if the namespace was empty. So while a module exporting a bare function was recognized as a “non-module entity”: +> 不幸的是,这是一个略微简化了的描述——TypeScript 并没有完全通过这个错误来避免规范兼容性问题,因为它允许函数的命名空间导入工作,并保留它们的调用签名,只要函数声明与命名空间声明合并,即使命名空间是空的。因此,尽管一个导出裸函数的模块被识别为“非模块实体”: > ```ts > declare function $(selector: string): any; -> export = $; // Cannot `import *` this 👍 +> export = $; // 不能 `import *` 这个模块 👍 > ``` -> A should-be-meaningless change allowed the invalid import to type check without errors: +> 但是一个应该毫无意义的更改却允许了无效的导入在类型检查时不报错: > ```ts > declare namespace $ {} > declare function $(selector: string): any; -> export = $; // Allowed to `import *` this and call it 😱 +> export = $; // 允许 `import *` 这个模块并调用它 😱 > ``` -Meanwhile, other transpilers were coming up with a way to solve the same problem. The thought process went something like this: +与此同时,其他的转译器也在寻找解决相同问题的方法。思路大致如下: -1. To import a CJS module that exports a function or a primitive, we clearly need to use a default import. A namespace import would be illegal, and named imports don’t make sense here. -2. Most likely, this means that runtimes implementing ESM/CJS interop will choose to make default imports of CJS modules _always_ link directly to the whole `exports`, rather than only doing so if the `exports` is a function or primitive. -3. So, a default import of a true CJS module should work just like a `require` call. But we’ll need a way to disambiguate true CJS modules from our transpiled CJS modules, so we can still transpile `export default "hello"` to `exports.default = "hello"` and have a default import of _that_ module link to `exports.default`. Basically, a default import of one of our own transpiled modules needs to work one way (to simulate ESM-to-ESM imports), while a default import of any other existing CJS module needs to work another way (to simulate how we think ESM-to-CJS imports will work). -4. When we transpile an ES module to CJS, let’s add a special extra field to the output: +1. 要导入一个导出函数或原始值的 CJS 模块,显然需要使用默认导入。命名空间导入是不合法的,在这里使用命名导入也没有意义。 +2. 很可能,实现 ESM/CJS 互操作性的运行时将选择使 CJS 模块的默认导入*总是*直接链接到整个 `exports`,而不仅仅在 `exports` 是函数或基本类型值时才这样做。 +3. 因此,真正的 CJS 模块的默认导入应该与 `require` 调用的行为相同。但是,我们需要一种区分真正的 CJS 模块和我们转译的 CJS 模块的方法,这样我们仍然可以将 `export default "hello"` 转译为 `exports.default = "hello"`,并且对该模块的默认导入链接到 `exports.default`。基本上,我们自己转译的模块的默认导入需要以某种方式工作(以模拟 ESM 到 ESM 导入),而对于其他任何现有 CJS 模块的默认导入,需要以另一种方式工作(以模拟我们认为的 ESM 到 CJS 导入将如何工作)。 +4. 当我们将 ES 模块转译为 CJS 时,让我们在输出中添加一个特殊的额外字段: ```ts exports.A = {}; exports.B = {}; exports.default = "Hello, world!"; - // Extra special flag! + // 额外的特殊标记! exports.__esModule = true; ``` - that we can check for when we transpile a default import: + 我们可以在转译默认导入时进行检查: ```ts // import hello from "./modue"; const _mod = require("./module"); const hello = _mod.__esModule ? _mod.default : _mod; ``` -The `__esModule` flag first appeared in Traceur, then in Babel, SystemJS, and Webpack shortly after. TypeScript added the `allowSyntheticDefaultImports` in 1.8 to allow the type checker to link default imports directly to the `exports`, rather than the `exports.default`, of any module types that lacked an `export default` declaration. The flag didn’t modify how imports or exports were emitted, but it allowed default imports to reflect how other transpilers would treat them. Namely, it allowed a default import to be used to resolve to “non-module entities,” where `import *` was an error: +`__esModule` 标志首次出现在 Traceur 中,然后在 Babel、SystemJS 和 Webpack 中不久之后也加入了该标志。TypeScript 在 1.8 版本中添加了 `allowSyntheticDefaultImports` 选项,以便类型检查器能够直接将默认导入与任何缺少 `export default` 声明的模块类型的 `exports` 关联起来,而不是 `exports.default`。该标志不会修改导入或导出的生成方式,但它允许默认导入反映其他转换器对它们进行处理的方式。换句话说,它允许默认导入用于解析到“非模块实体”,而使用 `import *` 将会报错。 ```ts -// Error: +// 错误: import * as hello from "./exports-function"; -// Old workaround: +// 旧的解决方法: import hello = require("./exports-function"); -// New way, with `allowSyntheticDefaultImports`: +// 新的方法,使用`allowSyntheticDefaultImports`: import hello from "./exports-function"; ``` -This was usually enough to let Babel and Webpack users write code that already worked in those systems without TypeScript complaining, but it was only a partial solution, leaving a few issues unsolved: +这通常足以让 Babel 和 Webpack 的用户编写在这些系统中已经工作的代码,而无需 TypeScript 抱怨,但这只是部分解决方案,仍然存在一些问题未解决: -1. Babel and others varied their default import behavior on whether an `__esModule` property was found on the target module, but `allowSyntheticDefaultImports` only enabled a _fallback_ behavior when no default export was found in the target module’s types. This created an inconsistency if the target module had an `__esModule` flag but _no_ default export. Transpilers and bundlers would still link a default import of such a module to its `exports.default`, which would be `undefined`, and would ideally be an error in TypeScript, since real ESM imports cause errors if they can’t be linked. But with `allowSyntheticDefaultImports`, TypeScript would think a default import of such an import links to the whole `exports` object, allowing named exports to be accessed as its properties. -2. `allowSyntheticDefaultImports` didn’t change how namespace imports were typed, creating an odd inconsistency where both could be used and would have the same type: +1. Babel 和其他转译器根据目标模块是否存在 `__esModule` 属性来改变其默认导入行为,但是 `allowSyntheticDefaultImports` 只在目标模块的类型中找不到默认导出时启用*回退*行为。如果目标模块有 `__esModule` 标志,但是*没有*默认导出,这就造成了不一致性。转译器和打包工具仍然会将这样一个模块的默认导入链接到它的 `exports.default`,而这将是 `undefined`,在 TypeScript 中理想情况下应该是一个错误,因为如果无法链接真正的 ESM 导入,就会导致错误。但是使用 `allowSyntheticDefaultImports`,TypeScript 会认为这样一个导入的默认导入链接到整个 `exports` 对象,允许通过它的属性访问命名导出。 +2. `allowSyntheticDefaultImports` 没有改变命名空间导入的类型,导致了一种奇怪的不一致性,即两者都可以使用,并且具有相同的类型: ```ts // @Filename: exportEqualsObject.d.ts declare const obj: object; @@ -172,17 +172,17 @@ This was usually enough to let Babel and Webpack users write code that already w import objDefault from "./exportEqualsObject"; import * as objNamespace from "./exportEqualsObject"; - // This should be true at runtime, but TypeScript gives an error: + // 这在运行时应该是 true,但是 TypeScript 报错了: objNamespace.default === objDefault; - // ^^^^^^^ Property 'default' does not exist on type 'typeof import("./exportEqualsObject")'. + // ^^^^^^^ 类型‘typeof import("./exportEqualsObject")’的属性‘default’不存在。 ``` -3. Most importantly, `allowSyntheticDefaultImports` did not change the JavaScript emitted by `tsc`. So while the flag enabled more accurate checking as long as the code was fed into another tool like Babel or Webpack, it created a real danger for users who were emitting `--module commonjs` with `tsc` and running in Node.js. If they encountered an error with `import *`, it may have appeared as if enabling `allowSyntheticDefaultImports` would fix it, but in fact it only silenced the build-time error while emitting code that would crash in Node. +3. 最重要的是,`allowSyntheticDefaultImports` 没有改变 `tsc` 输出的 JavaScript。因此,尽管该标志在将代码输入到 Babel 或 Webpack 等工具中时可以确保更准确的检查,但对于那些使用 `tsc` 输出 `--module commonjs` 并在 Node.js 中运行的用户来说,它带来了真正的风险。如果他们在 `import *` 中遇到错误,启用 `allowSyntheticDefaultImports` 可能看起来可以解决问题,但实际上只是掩盖了编译时的错误,而生成的代码将在 Node 中崩溃。 -TypeScript introduced the `esModuleInterop` flag in 2.7, which refined the type checking of imports to address the remaining inconsistencies between TypeScript’s analysis and the interop behavior used in existing transpilers and bundlers, and critically, adopted the same `__esModule`-conditional CommonJS emit that transpilers had adopted years before. (Another new emit helper for `import *` ensured the result was always an object, with call signatures stripped, fully resolving the specification compliance issue that the aforementioned “resolves to a non-module entity” error didn’t quite sidestep.) Finally, with the new flag enabled, TypeScript’s type checking, TypeScript’s emit, and the rest of the transpiling and bundling ecosystem were in agreement on a CJS/ESM interop scheme that was spec-legal and, perhaps, plausibly adoptable by Node. +TypeScript 在 2.7 中引入了 `esModuleInterop` 标志,它对导入的类型检查进行了改进,以解决 TypeScript 分析和现有转译器及打包工具中使用的互操作行为之间的剩余不一致性,并且关键地采用了与转译器多年前采用的基于 `__esModule` 条件的 CommonJS 输出方式。(为 `import *` 引入的另一个新的发出助手确保结果始终是一个对象,并且去除了调用签名,完全解决了前面提到的“解析为非模块实体”的错误没有完全避免的规范兼容性问题。)最后,启用新的标志后,TypeScript 的类型检查、输出和其他转译及打包工具之间达成了一致,采用了符合规范的 CJS/ESM 互操作方案,并且也许可以被 Node 采纳。 -## Interop in Node.js +## Node.js 中的互操作性 -Node.js shipped support for ES modules unflagged in v12. Like the bundlers and transpilers began doing years before, Node.js gave CommonJS modules a “synthetic default export” of their `exports` object, allowing the entire module contents to be accessed with a default import from ESM: +Node.js 在 v12 版本中无需标记即可支持 ES 模块。与打包工具和转译器几年前开始做的一样,Node.js 为 CommonJS 模块的 exports 对象提供了一个“合成默认导出”,使得我们可以通过从 ESM 进行默认导入来访问整个模块内容。 ```ts // @Filename: export.cjs @@ -193,11 +193,11 @@ import greeting from "./export.cjs"; greeting.hello; // "world" ``` -That’s one win for seamless migration! Unfortunately, the similarities mostly end there. +无缝迁移!不幸的是,相似之处在很大程度上只有这么多。 -### No `__esModule` detection (the “double default” problem) +### 没有 `__esModule` 检测(“双重默认导出”问题) -Node.js wasn’t able to respect the `__esModule` marker to vary its default import behavior. So a transpiled module with a “default export” behaves one way when “imported” by another transpiled module, and another way when imported by a true ES module in Node.js: +Node.js 无法识别 `__esModule` 标志以改变其默认导入行为。因此,当一个具有“默认导出”的经过转译的模块,被另一个经过转译的模块“导入”时,它的行为方式与在 Node.js 中由真正的 ES 模块导入的行为方式不同: ```ts // @Filename: node_modules/dependency/index.js @@ -206,17 +206,17 @@ exports.default = function doSomething() { /*...*/ } // @Filename: transpile-vs-run-directly.{js/mjs} import doSomething from "dependency"; -// Works after transpilation, but not a function in Node.js ESM: +// 经过转译后可以工作,但在 Node.js ESM 中不是一个函数: doSomething(); -// Doesn't exist after trasnpilation, but works in Node.js ESM: +// 经过转译后不存在,但可以在 Node.js ESM 中工作: doSomething.default(); ``` -While the transpiled default import only makes the synthetic default export if the target module lacks an `__esModule` flag, Node.js _always_ synthesizes a default export, creating a “double default” on the transpiled module. +虽然经过转译的默认导入仅在目标模块缺少 `__esModule` 标志时才生成合成的默认导出,但是 Node.js *始终*会合成默认导出,在转译的模块上创建了“双重默认导出”。 -### Unreliable named exports +### 不可靠的命名导出 -In addition to making a CommonJS module’s `exports` object available as a default import, Node.js attempts to find properties of `exports` to make available as named imports. This behavior matches bundlers and transpilers when it works; however, Node.js uses [syntactic analysis](https://github.com/nodejs/cjs-module-lexer) to synthesize named exports before any code executes, whereas transpiled modules resolve their named imports at runtime. The result is that imports from CJS modules that work in transpiled modules may not work in Node.js: +除了将 CommonJS 模块的 `exports` 对象作为默认导入进行使用外,Node.js 还尝试查找 `exports` 的属性,以作为命名导入进行使用。这种行为在与打包工具和转译器一起使用时是匹配的;然而,Node.js 会在任何代码执行之前使用[语法分析](https://github.com/nodejs/cjs-module-lexer)来合成命名导出,而转译后的模块在运行时解析它们的命名导入。其结果是,在转译模块中有效的 CJS 模块中的导入在 Node.js 中可能不起作用。 ```ts // @Filename: named-exports.cjs @@ -225,16 +225,16 @@ exports["worl" + "d"] = "hello"; // @Filename: transpile-vs-run-directly.{js/mjs} import { hello, world } from "./named-exports.cjs"; -// `hello` works, but `world` is missing in Node.js 💥 +// 在 Node.js 中,`hello` 可以工作,但是 `world` 会丢失 💥 import mod from "./named-exports.cjs"; mod.world; -// Accessing properties from the default always works ✅ +// 从默认导出中访问属性始终可以正常工作 ✅ ``` -### Cannot `require` a true ES module +### 无法 `require` 真正的 ES 模块 -True CommonJS modules can `require` an ESM-transpiled-to-CJS module, since they’re both CommonJS at runtime. But in Node.js, `require` crashes if it resolves to an ES module. This means published libraries cannot migrate from transpiled modules to true ESM without breaking their CommonJS (true or transpiled) consumers: +真正的 CommonJS 模块可以 `require` 一个经过 ESM 转译为 CJS 的模块,因为它们在运行时都是 CommonJS。但是在 Node.js 中,如果 `require` 解析到一个 ES 模块,它就会崩溃。这意味着发布的库无法从转译后的模块迁移到真正的 ESM,否则将破坏其 CommonJS(真正的或转译后的)使用者: ```ts // @Filename: node_modules/dependency/index.js @@ -242,15 +242,15 @@ export function doSomething() { /* ... */ } // @Filename: dependent.js import { doSomething } from "dependency"; -// ✅ Works if dependent and dependency are both transpiled -// ✅ Works if dependent and dependency are both true ESM -// ✅ Works if dependent is true ESM and dependency is transpiled -// 💥 Crashes if dependent is transpiled and dependency is true ESM +// ✅ 如果 dependent 和 dependency 都是转译后的模块,那么可以正常工作 +// ✅ 如果 dependent 和 dependency 都是真正的 ESM,那么可以正常工作 +// ✅ 如果 dependent 是真正的 ESM,而 dependency 是转译后的模块,那么可以正常工作 +// 💥 如果 dependent 是转译后的模块,而 dependency 是真正的 ESM,那么会崩溃 ``` -### Different module resolution algorithms +### 不同的模块解析算法 -Node.js introduced a new module resolution algorithm for resolving ESM imports that differed significantly from the long-standing algorithm for resolving `require` calls. While not directly related to interop between CJS and ES modules, this difference was one more reason why a seamless migration from transpiled modules to true ESM might not be possible: +Node.js 引入了一种新的模块解析算法,用于解析 ESM 导入,该算法与解析 `require` 调用的长期算法有很大的差异。虽然与 CJS 和 ES 模块之间的互操作性没有直接关系,但这种差异是从转译后的模块无缝迁移到真正的 ESM 可能无法实现的又一个原因: ```ts // @Filename: add.js @@ -261,28 +261,27 @@ export function add(a, b) { // @Filename: math.js export * from "./add"; // ^^^^^^^ -// Works when transpiled to CJS, -// but would have to be "./add.js" -// in Node.js ESM. +// 在转译为 CJS 时可以工作, +// 但在 Node.js ESM 中必须改为 "./add.js" ``` -## Conclusions +## 结论 -Clearly, a seamless migration from transpiled modules to ESM isn’t possible, at least in Node.js. Where does this leave us? +显然,在 Node.js 中无法实现从转译后的模块到 ESM 的无缝迁移。这给我们带来了什么结果? -### Setting the right `module` compiler option is critical +### 设置正确的 `module` 编译选项至关重要 -Since interoperability rules differ between hosts, TypeScript can’t offer correct checking behavior unless it understands what kind of module is represented by each file it sees, and what set of rules to apply to them. This is the purpose of the `module` compiler option. (In particular, code that is intended to run in Node.js is subject to stricter rules than code that will be processed by a bundler. The compiler’s output is not checked for Node.js compatibility unless `module` is set to `node16` or `nodenext`.) +由于不同的 host 环境之间的互操作规则不同,除非 TypeScript 理解每个文件表示的模块类型及应用于它们的规则,否则它无法提供正确的检查行为。这就是 `module` 编译选项的目的。(特别是,用于在 Node.js 中运行的代码受到比在打包工具中处理的代码更严格的规则限制。除非将 `module` 设置为 `node16` 或 `nodenext`,否则编译器的输出不会被检查是否与 Node.js 兼容。) -### Applications with CommonJS code should always enable `esModuleInterop` +### 在带有 CommonJS 代码的应用中,应始终启用 `esModuleInterop` -In a TypeScript _application_ (as opposed to a library that others may consume) where `tsc` is used to emit JavaScript files, whether `esModuleInterop` is enabled doesn’t have major consequences. The way you write imports for certain kinds of modules will change, but TypeScript’s checking and emit are in sync, so error-free code should be safe to run in either mode. The downside of leaving `esModuleInterop` disabled in this case is that it allows you to write JavaScript code with semantics that clearly violate the ECMASCript specification, confusing intuitions about namespace imports and making it harder to migrate to running ES modules in the future. +在 TypeScript 的*应用程序*(与可能由他人使用的库相对)中,使用 `tsc` 输出 JavaScript 文件时,启用或禁用 `esModuleInterop` 并不会产生重大后果。你编写某些类型模块的导入方式将发生变化,但 TypeScript 的检查和生成是同步的,因此无错误的代码应该可以在任何模式下安全运行。在这种情况下,禁用 `esModuleInterop` 的缺点是它允许你编写违反 ECMAScript 规范的语义的 JavaScript 代码,这会混淆对命名空间导入的直觉,并使将来迁移到运行 ES 模块变得更加困难。 -In an application that gets processed by a third-party transpiler or bundler, on the other hand, enabling `esModuleInterop` is more important. All major bundlers and transpilers use an `esModuleInterop`-like emit strategy, so TypeScript needs to adjust its checking to match. (The compiler always reasons about what will happen in the JavaScript files that `tsc` would emit, so even if another tool is being used in place of `tsc`, emit-affecting compiler options should still be set to match the output of that tool as closely as possible.) +另一方面,在由第三方转译器或打包工具处理的应用程序中,启用 `esModuleInterop` 更为重要。所有主要的打包工具和转译器都使用类似于 `esModuleInterop` 的输出策略,因此 TypeScript 需要调整其检查以匹配此策略。(编译器始终推理出 `tsc` 输出的 JavaScript 文件中会发生的情况,因此即使在使用 `tsc` 之外的其他工具替代 `tsc`,仍应将影响发出的编译选项设置为尽可能与该工具的输出保持一致。) -`allowSyntheticDefaultImports` without `esModuleInterop` should be avoided. It changes the compiler’s checking behavior without changing the code emitted by `tsc`, allowing potentially unsafe JavaScript to be emitted. Additionally, the checking changes it introduces are an incomplete version of the ones introduced by `esModuleInterop`. Even if `tsc` isn’t being used for emit, it’s better to enable `esModuleInterop` than `allowSyntheticDefaultImports`. +应避免在没有启用 `esModuleInterop` 的情况下使用 `allowSyntheticDefaultImports`。它会更改编译器的检查行为,而不会更改 `tsc` 输出的代码,从而可能导致输出潜在不安全的 JavaScript 代码。此外,它引入的检查更改是 `esModuleInterop` 引入的检查更改的不完整版本。即使不使用 `tsc` 进行输出,启用 `esModuleInterop` 而不是 `allowSyntheticDefaultImports` 也更好。 -Some people object to the inclusion of the `__importDefault` and `__importStar` helper functions included in `tsc`’s JavaScript output when `esModuleInterop` is enabled, either because it marginally increases the output size on disk or because the interop algorithm employed by the helpers seems to misrepresent Node.js’s interop behavior by checking for `__esModule`, leading to the hazards discussed earlier. Both of these objections can be addressed, at least partially, without accepting the flawed checking behavior exhibited with `esModuleInterop` disabled. First, the `importHelpers` compiler option can be used to import the helper functions from `tslib` rather than inlining them into each file that needs them. To discuss the second objection, let’s look at a final example: +有些人反对在启用 `esModuleInterop` 时在 `tsc` 的 JavaScript 输出中包含 `__importDefault` 和 `__importStar` 辅助函数,这要么是因为它在磁盘上略微增加了输出大小,要么是因为这些辅助函数使用的互操作算法似乎通过检查 `__esModule` 来错误地表示了 Node.js 的互操作行为,从而导致了前面讨论的问题。这些反对意见可以在不接受禁用 `esModuleInterop` 时出现的错误检查行为的情况下,至少部分地得到解决。首先,可以使用 `importHelpers` 编译选项从 `tslib` 导入辅助函数,而不是将它们内联到每个需要它们的文件中。为了讨论第二个反对意见,让我们看一个最后的例子: ```ts // @Filename: node_modules/transpiled-dependency/index.js @@ -303,17 +302,17 @@ import doSomethingElse from "true-cjs-dependency"; import sayHello from "./sayHello.js"; ``` -Assume we’re compiling `src` to CommonJS for use in Node.js. Without `allowSyntheticDefaultImports` or `esModuleInterop`, the import of `doSomethingElse` from `"true-cjs-dependency"` is an error, and the others are not. To fix the error without changing any compiler options, you could change the import to `import doSomethingElse = require("true-cjs-dependency")`. However, depending on how the types for the module (not shown) are written, you may also be able to write and call a namespace import, which would be a language-level specification violation. With `esModuleInterop`, none of the imports shown are errors (and all are callable), but the invalid namespace import would be caught. +假设我们将 `src` 编译为 CommonJS 以在 Node.js 中使用。如果没有使用 `allowSyntheticDefaultImports` 或 `esModuleInterop`,则从 `"true-cjs-dependency"` 导入的 `doSomethingElse` 将会报错,而其他导入则不会。为了修复此错误而不更改任何编译选项,你可以将导入更改为 `import doSomethingElse = require("true-cjs-dependency")`。然而,根据模块的类型定义方式(未显示),你还可以编写和调用命名空间导入,这将违反语言级别的规范。通过使用 `esModuleInterop`,所有所示的导入都不会报错(且可调用),但是无效的命名空间导入将会被捕获。 -What would change if we decided to migrate `src` to true ESM in Node.js (say, add `"type": "module"` to our root package.json)? The first import, `doSomething` from `"transpiled-dependency"`, would no longer be callable—it exhibits the “double default” problem, where we’d have to call `doSomething.default()` rather than `doSomething()`. (TypeScript understands and catches this under `--module node16` and `nodenext`.) But notably, the _second_ import of `doSomethingElse`, which needed `esModuleInterop` to work when compiling to CommonJS, works fine in true ESM. +如果我们决定将 `src` 迁移到 Node.js 的真正 ESM(例如,在根目录的 package.json 中添加 `"type": "module"`),会发生什么变化?第一个导入 `doSomething` 来自 `"transpiled-dependency"` 将不再可调用-它呈现出“双重默认”问题,我们必须调用 `doSomething.default()` 而不是 `doSomething()`。(TypeScript 在 `--module node16` 和 `nodenext` 下可以理解并捕获此问题。)但值得注意的是,第二个导入 `doSomethingElse` 在编译为真正的 ESM 时需要 `esModuleInterop` 才能正常工作。 -If there’s something to complain about here, it’s not what `esModuleInterop` does with the second import. The changes it makes, both allowing the default import and preventing callable namespace imports, are exactly in line with Node.js’s real ESM/CJS interop strategy, and made migration to real ESM easier. The problem, if there is one, is that `esModuleInterop` seems to fail at giving us a seamless migration path for the _first_ import. But this problem was not introduced by enabling `esModuleInterop`; the first import was completely unaffected by it. Unfortunately, this problem cannot be solved without breaking the semantic contract between `main.ts` and `sayHello.ts`, because the CommonJS output of `sayHello.ts` looks structurally identical to `transpiled-dependency/index.js`. If `esModuleInterop` changed the way the transpiled import of `doSomething` works to be identical to the way it would work in Node.js ESM, it would change the behavior of the `sayHello` import in the same way, making the input code violate ESM semantics (thus still preventing the `src` directory from being migrated to ESM without changes). +如果有什么要抱怨的,那不是 `esModuleInterop` 对第二个导入的处理方式。它所做的更改,既允许默认导入又阻止可调用的命名空间导入,完全符合 Node.js 的真正 ESM/CJS 互操作策略,并且使迁移到真正 ESM 变得更加容易。如果存在问题,那么问题似乎是 `esModuleInterop` 似乎无法为第一个导入提供无缝迁移路径。但是启用 `esModuleInterop` 并不会引入此问题;第一个导入完全不受其影响。不幸的是,这个问题无法解决,而不会破坏 `main.ts` 和 `sayHello.ts` 之间的语义契约,因为 `sayHello.ts` 的 CommonJS 输出在结构上与 `transpiled-dependency/index.js` 完全相同。如果 `esModuleInterop` 改变了转译后的 `doSomething` 导入的工作方式,使其与在 Node.js ESM 中的工作方式完全相同,那么它也会以同样的方式改变 `sayHello` 导入的行为,使输入代码违反 ESM 语义(因此仍然阻止 `src` 目录在不进行更改的情况下迁移到 ESM)。 -As we’ve seen, there is no seamless migration path from transpiled modules to true ESM. But `esModuleInterop` is one step in the right direction. For those who still prefer to minimize module syntax transformations and the inclusion of the import helper functions, enabling `verbatimModuleSyntax` is a better choice than disabling `esModuleInterop`. `verbatimModuleSyntax` enforces that the `import mod = require("mod")` and `export = ns` syntax be used in CommonJS-emitting files, avoiding all the kinds of import ambiguity we’ve discussed, at the cost of ease of migration to true ESM. +正如我们所看到的,从转译模块到真正的 ESM 之间并没有无缝的迁移路径。但是 `esModuleInterop` 是朝正确方向迈出的一步。对于那些仍然希望尽量减少模块语法转换和包含导入辅助函数的人来说,启用 `verbatimModuleSyntax` 比禁用 `esModuleInterop` 更好。`verbatimModuleSyntax` 强制在发出 CommonJS 文件时使用 `import mod = require("mod")` 和 `export = ns` 语法,避免了我们讨论过的所有导入模糊性问题,但牺牲了迁移到真正 ESM 的便利性。 -### Library code needs special considerations +### 库代码需要特别考虑 -Libraries (that ship declaration files) should take extra care to ensure the types they write are error-free under a wide range of compiler options. For example, it’s possible to write one interface that extends another in such a way that it only compiles successfully when `strictNullChecks` is disabled. If a library were to publish types like that, it would force all their users to disable `strictNullChecks` too. `esModuleInterop` can allow type declarations to contain similarly “infectious” default imports: +(发布声明文件的)库应特别注意确保他们编写的类型在广泛的编译选项下没有错误。例如,可以编写一个扩展另一个接口的接口,该接口只在禁用 `strictNullChecks` 时编译成功。如果一个库发布了这样的类型,它会强制所有用户也禁用 `strictNullChecks`。`esModuleInterop` 可以允许类型声明包含类似的“传染性”默认导入: ```ts // @Filename: /node_modules/dependency/index.d.ts @@ -322,16 +321,16 @@ declare function doSomething(req: express.Request): any; export = doSomething; ``` -Suppose this default import _only_ works with `esModuleInterop` enabled, and causes an error when a user without that option references this file. The user should _probably_ enable `esModuleInterop` anyway, but it’s generally seen as bad form for libraries to make their configurations infectious like this. It would be much better for the library to ship a declaration file like: +假设*只有*启用 `esModuleInterop` 才能正常工作并且在没有该选项的用户引用该文件时会导致错误。用户应该无论如何启用 `esModuleInterop`,但通常认为库使其配置具有传染性是不好的做法。库最好发布类似以下的声明文件: ```ts import express = require("express"); // ... ``` -Examples like this have led to conventional wisdom that says libraries should _not_ enable `esModuleInterop`. This advice is a reasonable start, but we’ve looked at examples where the type of a namespace import changes, potentially _introducing_ an error, when enabling `esModuleInterop`. So whether libraries compile with or without `esModuleInterop`, they run the risk of writing syntax that makes their choice infectious. +像这样的示例包含了传统智慧,即库*不*应启用 `esModuleInterop`。这个建议是一个合理的起点,但我们已经看过了一些示例,其中命名空间导入的类型在启用 `esModuleInterop` 时发生变化,可能会引入错误。因此,无论库是使用还是不使用 `esModuleInterop` 进行编译,它们都有可能编写出使其选择具有传染性的语法。 -Library authors who want to go above and beyond to ensure maximum compatibility would do well to validate their declaration files against a matrix of compiler options. But using `verbatimModuleSyntax` completely sidesteps the issue with `esModuleInterop` by forcing CommonJS-emitting files to use CommonJS-style import and export syntax. Additionally, since `esModuleInterop` only affects CommonJS, as more libraries move to ESM-only publishing over time, the relevance of this issue will decline. +希望超越最大兼容性的库作者应该根据一系列编译选项来验证其声明文件。但是,使用 `verbatimModuleSyntax` 完全规避了 `esModuleInterop` 的问题,因为它强制 CommonJS 发布文件使用 CommonJS 风格的导入和导出语法。此外,由于 `esModuleInterop` 只影响 CommonJS,随着越来越多的库随着时间的推移转向仅发布 ESM,这个问题的相关性将会下降。