Node.js 中 require 与 import/export 的区别
require 是 CommonJS 动态加载(运行时同步求值),import/export 是 ES 模块静态声明(编译期分析、支持 tree-shaking)。Node 12+ 原生支持 ESM:package.json 设 "type":"module" 或用 .mjs 后缀。新项目推荐 ESM。
require/module.exports 属于 CommonJS(Node 的传统模块系统),import/export 属于 ECMAScript 标准模块(ESM)。 核心区别:require 是运行时的函数调用、可写在任何位置、同步加载;静态 import 声明必须在顶层,支持静态分析;动态 import() 可在表达式中使用,tree-shaking 由构建工具实施。Node 12+ 已原生支持 ESM,新项目一律用 ESM。
语法对照
// CommonJS
const fs = require('fs');
module.exports = myModule;
// ESM
import fs from 'node:fs';
export default myModule;
export const helper = () => {};
关键差异
| 维度 | CommonJS (require) | ESM (import) |
|---|---|---|
| 加载时机 | require 调用时 | 静态建立依赖图,模块代码仍在运行时执行 |
| 可否条件加载 | 可以(if 里 require) | 静态不行,用动态 import() |
| 加载方式 | 同步 | 异步 |
| tree-shaking | 动态模式通常更难分析 | 有利于构建工具分析,Node 不自动删去未用代码 |
| 浏览器原生 | 不支持 | 支持 |
| 顶层 await | 不支持 | 支持 |
如何启用 ESM
// package.json
{ "type": "module" }
或者文件直接用 .mjs 后缀;CommonJS 文件对应 .cjs。
互操作
- ESM 里可以
import cjs from './old.cjs'(拿到的是 module.exports 作为 default); - 现代 Node 的部分受支持版本可 require 同步 ESM(包括依赖图不含顶层 await 等限制),按目标版本检查;通用做法是在 async 函数内使用
const m = await import('./m.mjs')。从 CJS 检测到的具名导出并不保证覆盖所有动态写法。
常见问题(FAQ)
Q:老项目要迁移吗?
不必强迁。大量生产代码仍是 CJS 且运行良好。新包、新服务用 ESM;库作者可考虑双格式发布(bundler 会用 ESM 入口做 tree-shaking)。
Q:ESM 里 __dirname 没了怎么办?
用 import.meta.url 换算:const __dirname = path.dirname(fileURLToPath(import.meta.url));,支持该属性的 Node 版本可用 import.meta.dirname,仅适用于 file: 模块;状态与可用性应按目标版本核对。
Q:为什么 ESM 里引入本地文件必须写全后缀?
ESM 遵循浏览器规则,本地相对/绝对文件路径必须完整(裸包说明符受 exports 等规则控制):import './util.js' 而不是 ./util。CommonJS 的自动补全在 ESM 中不适用。