Node.js 模块导入
CommonJS 与 ESM 的差异、互操作规则、怎么选。
Key PointsESM 是静态的、异步的、值引用;CommonJS 是动态的、同步的、值拷贝。
两种写法
JavaScript
// CommonJS
const fs = require('node:fs');
const { join } = require('node:path');
module.exports = { run };
// ESM
import fs from 'node:fs';
import { join } from 'node:path';
export function run() {}怎么决定用哪个
| Signal | Module system |
|---|---|
"type": "module" | ESM |
"type": "commonjs" 或不写 | CommonJS |
文件扩展名 .mjs | ESM |
文件扩展名 .cjs | CommonJS |
关键差异
| Aspect | CommonJS | ESM |
|---|---|---|
| 加载时机 | 运行时同步 | 编译期静态解析 |
import 位置 | 可在 if 里 | 必须顶层 |
| 导出绑定 | 值拷贝 | 活绑定,变了能感知 |
__dirname | 有 | 没有,用 import.meta.dirname |
| 顶层 await | 不支持 | 支持 |
JavaScript
// ESM 里的「活绑定」
// counter.mjs
export let n = 0;
export const inc = () => n++;
// main.mjs
import { n, inc } from './counter.mjs';
console.log(n); // 0
inc();
console.log(n); // 1 ← CommonJS 里这里还是 0互操作
JavaScript
// ESM 里导入 CommonJS 包:默认导入拿到 module.exports
import express from 'express';
import { readFile } from 'node:fs/promises';
// CommonJS 里导入 ESM:只能动态 import()
(async () => {
const { run } = await import('./esm-only.mjs');
run();
})();ESM imports need the full extension:
import './util.js', not import './util'. CommonJS allows omission, ESM does not — the most common migration error.在 ESM 里拿 __dirname
JavaScript
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
// Node 20.11+ 直接可用
const here = import.meta.dirname;
console.log(join(here, 'data.json'));
// 兼容旧版本
const legacy = dirname(fileURLToPath(import.meta.url));怎么选
New projects: ESM. The ecosystem has moved — Vite, tsx, Bun and Deno default to it. Legacy projects: keep CommonJS. Do not rewrite just for consistency; the cost sits in third-party interop and the payoff is small.
两种写法
JavaScript
// CommonJS
const fs = require('node:fs');
const { join } = require('node:path');
module.exports = { run };
// ESM
import fs from 'node:fs';
import { join } from 'node:path';
export function run() {}怎么决定用哪个
| 信号 | 模块体系 |
|---|---|
"type": "module" | ESM |
"type": "commonjs" 或不写 | CommonJS |
文件扩展名 .mjs | ESM |
文件扩展名 .cjs | CommonJS |
关键差异
| 对比项 | CommonJS | ESM |
|---|---|---|
| 加载时机 | 运行时同步 | 编译期静态解析 |
import 位置 | 可在 if 里 | 必须顶层 |
| 导出绑定 | 值拷贝 | 活绑定,变了能感知 |
__dirname | 有 | 没有,用 import.meta.dirname |
| 顶层 await | 不支持 | 支持 |
JavaScript
// ESM 里的「活绑定」
// counter.mjs
export let n = 0;
export const inc = () => n++;
// main.mjs
import { n, inc } from './counter.mjs';
console.log(n); // 0
inc();
console.log(n); // 1 ← CommonJS 里这里还是 0互操作
JavaScript
// ESM 里导入 CommonJS 包:默认导入拿到 module.exports
import express from 'express';
import { readFile } from 'node:fs/promises';
// CommonJS 里导入 ESM:只能动态 import()
(async () => {
const { run } = await import('./esm-only.mjs');
run();
})();ESM 导入必须写完整扩展名:
import './util.js' 而不是 import './util'。CommonJS 可以省略,ESM 不行——这是迁移时最常见的报错来源。在 ESM 里拿 __dirname
JavaScript
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
// Node 20.11+ 直接可用
const here = import.meta.dirname;
console.log(join(here, 'data.json'));
// 兼容旧版本
const legacy = dirname(fileURLToPath(import.meta.url));怎么选
新项目用 ESM。生态已经转向,Vite、tsx、Bun、Deno 都以 ESM 为默认。维护老项目用 CommonJS,别为了统一而大改——迁移成本主要在第三方包的互操作上,收益不明显。