两套并存

Node 同时支持:

CommonJS(CJS) ES Module(ESM)
关键词 require / module.exports import / export
文件后缀 .cjs.js(默认) .mjs.js(需 "type": "module"
同步加载 ✗(异步)
顶层 await
__dirname / __filename 自动有 需要 import.meta.url 换算
现代推荐 老项目 / 工具 新项目首选

CommonJS(传统)

// math.js
function add(a, b) { return a + b; }
const PI = 3.14;

module.exports = { add, PI };
// 或单个:module.exports = function () { ... };
// index.js
const { add, PI } = require('./math');
console.log(add(1, 2), PI);

ESM(现代)

package.json 里:

{ "type": "module" }
// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14;
// 或默认导出:
export default function () { ... };
// index.js
import { add, PI } from './math.js';   // ★ 注意必须带 .js 扩展名
import myDefault from './math.js';
console.log(add(1, 2), PI);

顶层 await(ESM 独有)

// 直接在文件顶层用 await
const config = await loadConfig();
console.log(config);

CJS 不能这么写。

ESM 里的 __dirname / __filename

import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Node 20.11+ 有更简单的:
const __dirname = import.meta.dirname;
const __filename = import.meta.filename;

互操作(混用)

ESM 里 require CJS 包

ESM 可以 import 大多数 CJS 包:

import express from 'express';      // express 是 CJS 包,但能 import

如果不行(命名导出问题):

import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const someCjsLib = require('some-cjs-lib');

CJS 里 require ESM 包

不能直接 require——CJS 是同步,ESM 是异步:

// ❌ 报错
const lib = require('esm-only-package');

// ✓ 用动态 import(返回 Promise)
const lib = await import('esm-only-package');

选哪个

新项目

  • 库 / 工具 / Web 服务 → ESM
  • 跑构建脚本 / 简单 CLI → CJS 也 OK

老项目

  • 维护就维护,别为切而切
  • 想升级 → 一步步迁(先小模块)

实战:迁 CJS 到 ESM

# 1. package.json 加 "type": "module"

# 2. require → import
# CJS:
const fs = require('fs');
# ESM:
import fs from 'fs';
# 或:
import { readFileSync } from 'fs';

# 3. module.exports → export
# CJS:
module.exports = { foo };
module.exports.bar = bar;
# ESM:
export const foo = ...;
export const bar = ...;
export default ...;

# 4. 所有相对路径 import 加 .js 后缀
import './utils';     ❌
import './utils.js';  ✓

# 5. __dirname / __filename 用上面提到的换算

  • ESM 必须带文件后缀——import './utils' 报错,要 './utils.js'
  • JSON 导入import data from './data.json' 在新 Node 要 with { type: 'json' }
  • 循环依赖在 ESM 行为不同——避免循环引用是好做法
  • CJS 包的 default export 在 ESM 里不一定能直接解构——看包的 exports 字段

下一篇:JS 数据类型。