Node.js 模块与 ESM

要点

ESM 的 import 与 CommonJS 的 require 有不同解析和加载规则。扩展名、package type 与工具链共同决定文件怎样被解释。

两套模块体系

CommonJS(历史体系)

Node 早期方案,同步加载:

// cjs/add.cjs
function add(a, b) { return a + b; }
module.exports = { add };

// cjs/main.cjs
const { add } = require("./add.cjs");
console.log(add(1, 2));

ESM(现行标准)

JavaScript 官方标准,静态结构、异步加载、支持顶层 await:

// esm/add.js
export function add(a, b) { return a + b; }
export default add;

// esm/main.js
import add, { add as plus } from "./add.js";
console.log(plus(1, 2));

关键差异速览

维度ESMCommonJS
加载时机编译期静态分析(可 tree-shaking)运行时动态 require
导出绑定实时绑定(值变化可见)module.exports 对象的共享引用;重新赋值不会反向更新已取出的局部变量
顶层 await✅ 支持❌ 不支持
__dirname❌ 需从 import.meta.url 推导✅ 全局可用
条件加载需用动态 import()可条件 require
严格模式始终启用默认非严格

互操作规则(最常踩坑处)

// ESM 中导入 CJS 包:default 即 module.exports
import _ from "lodash";                // ✅ CJS 包的整组导出挂在 default 上
import { map } from "lodash";          // ⚠️ 取决于该包是否提供命名导出(由 cjs-module-lexer 推断)

// CJS 中加载 ESM:只能动态 import
// old-plugin.cjs
async function load() {
  const { modern } = await import("./modern.mjs");
}

实践:type 字段与文件扩展名

Node 判断一段 .js 代码是 ESM 还是 CJS,依据如下:

条件解析为
最近 package.json 有 "type": "module"ESM
最近 package.json 有 "type": "commonjs"(或无 type)CJS
扩展名 .mjs永远 ESM
扩展名 .cjs永远 CJS
# 验证实验
mkdir module-lab && cd module-lab
pnpm init && pnpm pkg set type=module
echo 'export const who = "esm";' > lib.js
echo 'import { who } from "./lib.js"; console.log(who);' > main.js
node main.js   # 输出 esm

验证方法: 把 type 改回 commonjs 再运行,观察报错——这是理解解析规则的最好方式。

实践:exports 字段设计公共 API

exports 是现代包定义入口的标准方式,同时起到”封箱”作用——未列出的路径外部无法导入。

{
  "name": "@quest/utils",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./cli": "./dist/cli.js",
    "./internal/*": null
  }
}

条件键说明:

  • types:TypeScript 消费者定位类型声明
  • import:ESM 消费者使用
  • require:CJS 消费者使用
  • 匹配顺序先到先得:node-addons → node / import / require → default
  • "./internal/*": null:显式禁止外部深层导入

内部子路径别名用 imports 字段(以 # 开头):

{
  "imports": {
    "#config": "./src/config.js",
    "#lib/*.js": "./src/lib/*.js"
  }
}
// 任何位置都能稳定导入,不受相对路径层级影响
import { config } from "#config";

模块解析:从标识符到文件

import x from "y" 时 Node 按以下顺序查找:

  1. 内置模块:node:fs、path 等直接命中(建议始终加 node: 前缀)
  2. 相对/绝对路径:./lib.js——ESM 必须写全扩展名(不像 CJS 可省略)
  3. 裸说明符(bare specifier):如 hono,沿当前目录逐级向上找 node_modules/<pkg>,读取其 exports 字段定位入口

目录索引行为差异:

import { x } from "./utils/index.js";  // ✅ 必须写全
import { x } from "./utils";           // ❌ ESM 不再自动补 index.js

TypeScript 项目用 .js 后缀指向 .ts 源文件(编译期映射),或直接用 # 别名避开烦恼。

最佳实践

模块格式按运行环境和依赖契约选择,ESM 与 CommonJS 互操作仍有边界。原生 Node ESM 的相对导入通常需要明确扩展名,TypeScript 构建工具又可能有不同解析规则,需区分源码与运行产物。

exports 可限定包的公共入口,但仅在承诺支持两种消费方式时才需要相应双格式设计。ESM 中没有传统的 require/__dirname 绑定,使用适用版本的 import.meta 能力或标准桥接方式。编写一个真实消费者验证公开入口,避免只在库自己的目录里能导入。

常见问题

Q1: ERR_MODULE_NOT_FOUND 但文件明明存在?

A: 九成是缺扩展名。ESM 要求 ./utils.js 而非 ./utils;TypeScript 源码中写 ./utils.js(指向编译目标)或 .ts(Node 原生运行时)。

Q2: require is not defined in ES module scope?

A: 当前是 ESM 上下文。用 await import("...") 或 import { createRequire } from "node:module"; const require = createRequire(import.meta.url); 桥接。

练习与实践

正文验证:ESM 的绑定和扩展名

下面程序是本页“ESM 导出是实时绑定、相对导入要带扩展名”的可独立运行版本。它只使用 Node 标准库;验证脚本会直接从这个 Markdown 代码块提取内容后执行。

import assert from 'node:assert/strict';

const counter = { value: 0 };
const read = () => counter.value;
counter.value += 1;

assert.equal(read(), 1);
assert.equal(new URL('./add.js', import.meta.url).pathname.endsWith('/add.js'), true);
console.log('esm-binding: live read and explicit extension');

练习一:解析规则实验

任务要求: 1. 按”type 字段验证实验”复现两种报错并解释原因 2. 把一个 .js 改名为 .mjs,观察在 type: commonjs 项目中仍可 ESM 运行

练习二:发布一个双格式迷你包

挑战任务: – 用 tsc 分别构建 ESM 与 CJS 两个产物 – 写出含 types/import/require 条件的 exports 字段 – 在另一个项目中分别以两种方式导入验证

提示: tsc 单次构建只能输出一种模块格式,需要两次构建或用 tsup 等打包工具。


相关文档

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇