本指南旨在解释什么是 ES 模块以及如何使 Nuxt 应用(或上游库)与 ESM 兼容。
CommonJS (CJS) 是 Node.js 引入的一种格式,允许在独立的 JavaScript 模块之间共享功能(阅读更多)。你可能已经熟悉这种语法。
const a = require('./a')
module.exports.a = a
像 webpack 和 Rollup 这样的打包工具支持这种语法,并允许你在浏览器中使用用 CommonJS 编写的模块。
大多数情况下,当人们谈论 ESM vs. CJS 时,他们指的是编写代码的另一种语法。modules.
import a from './a'
export { a }
在 ECMAScript Modules (ESM) 成为标准之前(花了十多年!),像webpack甚至像 TypeScript 这样的语言也开始支持所谓的 ESM 语法。然而,与实际规范相比,存在一些关键差异;这是一个有用的解释器.
你可能已经用 ESM 语法编写应用程序很长时间了。毕竟,它在浏览器中是原生支持的,在 Nuxt 2 中,我们把你编写的所有代码都编译成了适当的格式(服务器使用 CJS,浏览器使用 ESM)。
当将模块添加到你的包时,事情有点不同。一个示例库可能同时公开 CJS 和 ESM 版本,并让我们选择我们想要的那个。
{
"name": "sample-library",
"main": "dist/sample-library.cjs.js",
"module": "dist/sample-library.esm.js"
}
因此,在 Nuxt 2 中,打包工具(webpack)会为服务器构建引入 CJS 文件(“main”),并为客户端构建使用 ESM 文件(“module”)。
然而,在最近的 Node.js LTS 版本中,现在可以在使用原生 ESM 模块在 Node.js 中。这意味着 Node.js 本身可以使用 ESM 语法处理 JavaScript,尽管它默认不这样做。启用 ESM 语法的两种最常见方式是:
package.json 中设置 "type": "module" 并继续使用 .js 扩展名.mjs 文件扩展名(推荐)这就是我们为 Nuxt Nitro 所做的;我们输出一个 .output/server/index.mjs 文件。这告诉 Node.js 将此文件视为原生 ES 模块。
当你 import 模块而不是 require 它时,Node.js 会以不同的方式解析它。例如,当你导入 sample-library 时,Node.js 会在该库的 package.json 中查找 exports 条目,如果未定义 exports,则回退到 main 条目。
动态导入也是如此,例如 const b = await import('sample-library')。
Node 支持以下类型的导入(参见文档):
.mjs 结尾的文件 - 这些文件应使用 ESM 语法。.cjs 结尾的文件 - 这些文件应使用 CJS 语法。.js 结尾的文件 - 除非其 package.json 包含 "type": "module",否则这些文件应使用 CJS 语法。长期以来,模块作者一直在生成 ESM 语法构建,但使用 .esm.js 或 .es.js 等约定,并将其添加到 package.json 中的 module 字段中。到目前为止,这并没有成为问题,因为它们只被 webpack 等打包工具使用,而这些工具并不特别关心文件扩展名。
但是,如果你尝试在 Node.js ESM 环境中导入一个包含 .esm.js 文件的包,它将无法工作,你会收到如下错误:
(node:22145) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
/path/to/index.js:1
export default {}
^^^^^^
SyntaxError: Unexpected token 'export'
at wrapSafe (internal/modules/cjs/loader.js:1001:16)
at Module._compile (internal/modules/cjs/loader.js:1049:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
....
at async Object.loadESM (internal/process/esm_loader.js:68:5)
如果你从一个 Node.js 认为是 CJS 的 ESM 语法构建中进行命名导入,你可能也会收到此错误。
file:///path/to/index.mjs:5
import { named } from 'sample-library'
^^^^^
SyntaxError: Named export 'named' not found. The requested module 'sample-library' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from 'sample-library';
const { named } = pkg;
at ModuleJob._instantiate (internal/modules/esm/module_job.js:120:21)
at async ModuleJob.run (internal/modules/esm/module_job.js:165:5)
at async Loader.import (internal/modules/esm/loader.js:177:24)
at async Object.loadESM (internal/process/esm_loader.js:68:5)
如果你遇到这些错误,问题几乎肯定出在上游库。它们需要修复它们的库以支持 Node.js 导入。
同时,你可以通过将这些库添加到 build.transpile 中,告诉 Nuxt 不尝试导入它们。
export default defineNuxtConfig({
build: {
transpile: ['sample-library'],
},
})
你可能会发现你*也*需要添加这些库正在导入的其他包。
在某些情况下,你可能还需要手动将库别名为 CJS 版本,例如:
export default defineNuxtConfig({
alias: {
'sample-library': 'sample-library/dist/sample-library.cjs.js',
},
})
具有 CommonJS 格式的依赖项可以使用 module.exports 或 exports 提供默认导出。
module.exports = { test: 123 }
// or
exports.test = 123
如果我们 require 这样的依赖项,这通常运行良好。
const pkg = require('cjs-pkg')
console.log(pkg) // { test: 123 }
原生 ESM 模式下的 Node.js, 启用 esModuleInterop 的 TypeScript以及像 webpack 这样的打包工具,提供了一种兼容机制,使我们能够默认导入此类库。这种机制通常被称为“interop require default”。
import pkg from 'cjs-pkg'
console.log(pkg) // { test: 123 }
然而,由于语法检测和不同捆绑格式的复杂性,总是存在 interop default 失败的可能性,最终我们得到如下结果:
import pkg from 'cjs-pkg'
console.log(pkg) // { default: { test: 123 } }
此外,在使用动态导入语法时(在 CJS 和 ESM 文件中),我们总是遇到这种情况:
import('cjs-pkg').then(console.log) // [Module: null prototype] { default: { test: '123' } }
在这种情况下,我们需要手动进行默认导出互操作。
// Static import
import { default as pkg } from 'cjs-pkg'
// Dynamic import
import('cjs-pkg').then(m => m.default || m).then(console.log)
为了处理更复杂的情况和更高的安全性,我们推荐并内部使用mlly在 Nuxt 中,它可以保留命名导出。
import { interopDefault } from 'mlly'
// Assuming the shape is { default: { foo: 'bar' }, baz: 'qux' }
import myModule from 'my-module'
console.log(interopDefault(myModule)) // { foo: 'bar', baz: 'qux' }
好消息是,修复 ESM 兼容性问题相对简单。主要有两种选择:
.mjs 结尾。.cjs 结尾。package.json 中设置 "type": "module",并确保你的构建库使用 ESM 语法。但是,你可能会遇到依赖项问题——并且这种方法意味着你的库_只能_在 ESM 环境中使用。从 CJS 到 ESM 的第一步是更新所有 require 的用法,将其替换为 import。
module.exports = function () { /* ... */ }
exports.hello = 'world'
export default function () { /* ... */ }
export const hello = 'world'
const myLib = require('my-lib')
import myLib from 'my-lib'
// or
const dynamicMyLib = await import('my-lib').then(lib => lib.default || lib)
在 ESM 模块中,与 CJS 不同,require、require.resolve、__filename 和 __dirname 全局变量不可用,应替换为 import() 和 import.meta.filename。
const { join } = require('node:path')
const newDir = join(__dirname, 'new-dir')
import { fileURLToPath } from 'node:url'
const newDir = fileURLToPath(new URL('./new-dir', import.meta.url))
const someFile = require.resolve('./lib/foo.js')
import { resolvePath } from 'mlly'
const someFile = await resolvePath('my-lib', { url: import.meta.url })
exports 字段和条件导出。(阅读更多).{
"exports": {
".": {
"import": "./dist/mymodule.mjs"
}
}
}