---
title: "ES 模块"
description: "Nuxt 使用原生 ES 模块。"
canonical_url: "https://nuxt.zhcndoc.com/docs/3.x/guide/concepts/esm"
---
# ES 模块

> Nuxt 使用原生 ES 模块。

本指南帮助解释什么是 ES 模块，以及如何使 Nuxt 应用（或上游库）兼容 ESM。

## 背景

### CommonJS 模块

CommonJS（CJS）是由 Node.js 引入的一种格式，允许在隔离的 JavaScript 模块之间共享功能（[了解更多](https://node.zhcndoc.com/api/modules.html)）。你可能已经熟悉这种语法：

```js
const a = require('./a')

module.exports.a = a
```

像 webpack 和 Rollup 这样的打包器支持这种语法，允许你在浏览器中使用用 CommonJS 编写的模块。

### ESM 语法

大多数情况下，当人们讨论 ESM 与 CJS 时，他们指的是一种不同的[模块](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)写法语法。

```js
import a from './a'

export { a }
```

在 ECMAScript 模块（ESM）成为标准之前（耗时超过 10 年！），像 [webpack](https://webpack.js.org/guides/ecma-script-modules) 这样的工具甚至 TypeScript 语言开始支持所谓的 **ESM 语法**。然而，实际规范有一些关键差异；这里有[一个有用的解释](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/)。

### 什么是“原生” ESM？

你可能早已用 ESM 语法编写应用。毕竟，它被浏览器原生支持，在 Nuxt 2 中，我们将你写的所有代码编译为适当的格式（服务器端为 CJS，浏览器端为 ESM）。

当向你的包中添加模块时，情况稍有不同。一个示例库可能同时暴露 CJS 和 ESM 版本，并让我们选择想要的那个：

```json
{
  "name": "sample-library",
  "main": "dist/sample-library.cjs.js",
  "module": "dist/sample-library.esm.js"
}
```

所以在 Nuxt 2 中，打包器（webpack）会为服务器构建拉取 CJS 文件（`main`），而为客户端构建使用 ESM 文件（`module`）。

::note
`module` 字段是 webpack 和 Rollup 这样的打包器约定使用的，但 Node.js 本身不识别它。Node.js 只使用 [`exports`](https://nodejs.org/api/packages.html#exports) 和 [`main`](https://nodejs.org/api/packages.html#main) 字段进行模块解析。
::

然而，在近期的 Node.js LTS 版本中，现在可以在 Node.js 内[使用原生 ESM 模块](https://nodejs.org/api/esm.html)。这意味着 Node.js 本身可以处理使用 ESM 语法的 JavaScript，虽然默认情况下不启用。启用 ESM 语法的两种最常见方式是：

- 在你的 `package.json` 中设置 `"type": "module"`，并继续使用 `.js` 扩展名
- 使用 `.mjs` 文件扩展名（推荐）

这也是我们在 Nuxt Nitro 中使用的方法；我们输出 `.output/server/index.mjs` 文件，这告诉 Node.js 将该文件视为原生 ES 模块。

### 在 Node.js 环境中哪些导入是有效的？

当你 `import` 一个模块而不是 `require` 它时，Node.js 会有不同的解析方式。例如，当你导入 `sample-library` 时，Node.js 会查找该库的 `package.json` 中的 `exports` 字段，若未定义则退回到 `main` 字段。

动态导入，如 `const b = await import('sample-library')`，也同样如此。

Node 支持以下几类导入（参见[文档](https://node.zhcndoc.com/api/packages.html#determining-module-system)）：

1. 以 `.mjs` 结尾的文件——期望使用 ESM 语法
2. 以 `.cjs` 结尾的文件——期望使用 CJS 语法
3. 以 `.js` 结尾的文件——期望使用 CJS 语法，除非其 `package.json` 中有 `"type": "module"`

### 可能存在哪些问题？

长期以来，模块作者一直生产 ESM 语法构建，但使用诸如 `.esm.js` 或 `.es.js` 这类约定，并将其添加到 `package.json` 的 `module` 字段中。这以前没造成问题，因为它们仅被 webpack 等打包器使用，而这些打包器不特别关心文件扩展名。

然而，如果你尝试在 Node.js ESM 环境中导入一个带有 `.esm.js` 文件的包，则会失败，并出现类似错误：

```bash [Terminal]
(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 构建中做了命名导入，也可能出现此错误：

```bash [Terminal]
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)
```

## 解决 ESM 问题的排查方法

如果你遇到这些错误，问题几乎肯定出在上游库上。他们需要[修复自己的库](https://nuxt.zhcndoc.com/docs/3.x/guide/concepts/esm#library-author-guide)，使其支持被 Node 导入。

### 转译库

在此期间，你可以通过将这些库添加到 `build.transpile` 来告诉 Nuxt 不要直接导入它们：

```ts twoslash
export default defineNuxtConfig({
  build: {
    transpile: ['sample-library'],
  },
})
```

你可能还需要添加这些库导入的其他包。

### 别名库

在某些情况下，你还可能需要手动将库别名指向 CJS 版本，例如：

```ts twoslash
export default defineNuxtConfig({
  alias: {
    'sample-library': 'sample-library/dist/sample-library.cjs.js',
  },
})
```

### 默认导出

依赖中如果是 CommonJS 格式，可以通过 `module.exports` 或 `exports` 提供默认导出：

```js [node_modules/cjs-pkg/index.js]
module.exports = { test: 123 }
// 或者
exports.test = 123
```

如果用 `require` 引入此依赖，这通常工作正常：

```js [test.cjs]
const pkg = require('cjs-pkg')

console.log(pkg) // { test: 123 }
```

[Node.js 的本地 ESM 模式](https://node.zhcndoc.com/api/esm.html#interoperability-with-commonjs)、开启了 `esModuleInterop` 的 [TypeScript](https://www.typescriptlang.org/tsconfig#esModuleInterop) 和像 webpack 这样的打包器提供了兼容机制，可以默认导入这样的库。
此机制通常称作“interop require default”：

```js
import pkg from 'cjs-pkg'

console.log(pkg) // { test: 123 }
```

然而由于语法检测和不同打包格式的复杂性，互操作默认导入有时会失败，结果如下：

```js
import pkg from 'cjs-pkg'

console.log(pkg) // { default: { test: 123 } }
```

动态导入语法（无论在 CJS 还是 ESM 文件中）也总是这样：

```js
import('cjs-pkg').then(console.log) // [Module: null prototype] { default: { test: '123' } }
```

这时，我们需要手动取互操作默认导出：

<!-- eslint-disable import/no-named-default -->

```js
// 静态导入
import { default as pkg } from 'cjs-pkg'

// 动态导入
import('cjs-pkg').then(m => m.default || m).then(console.log)
```

为处理更复杂情况与提高安全，我们推荐且在 Nuxt 内部使用 [mlly](https://github.com/unjs/mlly)，该库可保持命名导出不变。

```js
import { interopDefault } from 'mlly'

// 假设形状是 { default: { foo: 'bar' }, baz: 'qux' }
import myModule from 'my-module'

console.log(interopDefault(myModule)) // { foo: 'bar', baz: 'qux' }
```

## 库作者指南

好消息是，ESM 兼容性问题相对容易修复。主要有两种选择：

1. **将你的 ESM 文件重命名为以 `.mjs` 结尾。**

   *这是推荐且最简单的做法。* 你可能需要解决库依赖和构建系统的问题，但大多数情况下，这能帮你解决问题。建议将 CJS 文件重命名为 `.cjs` 以便最明确地表示。
2. **选择让整个库只支持 ESM。**

   这意味着在你的 `package.json` 设置 `"type": "module"`，并确保构建库使用 ESM 语法。但是，可能会面临依赖问题，而且这意味着你的库只能被 ESM 环境使用。

### 迁移

从 CJS 转向 ESM 的第一步是把所有 `require` 用法改为 `import`：

::code-group
```ts [转换前]
module.exports = function () { /* ... */ }

exports.hello = 'world'
```

```ts [转换后]
export default function () { /* ... */ }

export const hello = 'world'
```
::

::code-group
```js [转换前]
const myLib = require('my-lib')
```

```js [转换后]
import myLib from 'my-lib'
// 或
const dynamicMyLib = await import('my-lib').then(lib => lib.default || lib)
```
::

在 ESM 模块中，不像 CJS，`require`、`require.resolve`、`__filename` 和 `__dirname` 这类全局变量不可用，
应替换为 `import()` 和 `import.meta.url`。

::code-group
```js [转换前]
const { join } = require('node:path')

const newDir = join(__dirname, 'new-dir')
```

```js [转换后]
import { fileURLToPath } from 'node:url'

const newDir = fileURLToPath(new URL('./new-dir', import.meta.url))
```
::

::code-group
```js [转换前]
const someFile = require.resolve('./lib/foo.js')
```

```js [转换后]
import { resolvePath } from 'mlly'

const someFile = await resolvePath('my-lib', { url: import.meta.url })
```
::

### 最佳实践

- 优先使用命名导出，而不是默认导出。这有助于减少 CJS 冲突。（参见[默认导出](https://nuxt.zhcndoc.com/docs/3.x/guide/concepts/esm#default-exports)一节）
- 尽可能避免依赖 Node.js 内置模块、CommonJS 或仅限 Node.js 的依赖项，以便你的库无需 Nitro polyfill 即可用于浏览器和 Edge Workers。
- 使用新的 `exports` 字段和条件导出。（[了解更多](https://nodejs.org/api/packages.html#conditional-exports)）。

```json
{
  "exports": {
    ".": {
      "import": "./dist/mymodule.mjs"
    }
  }
}
```


## Sitemap

See the full [sitemap](https://nuxt.zhcndoc.com/sitemap.md) for all pages.
