Node.js 模块导入

CommonJS 与 ESM 的差异、互操作规则、怎么选。

Intermediate
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() {}

怎么决定用哪个

SignalModule system
"type": "module"ESM
"type": "commonjs" 或不写CommonJS
文件扩展名 .mjsESM
文件扩展名 .cjsCommonJS

关键差异

AspectCommonJSESM
加载时机运行时同步编译期静态解析
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.

Friends & Partners