Node 里的 CommonJS 与 ESM:为什么有时 require 有时 import?
知识背景
JavaScript 在浏览器里长期没有官方模块标准,Node 早期采用了 require / module.exports 的 CommonJS(CJS);后来 ECMAScript 推出了 import / export 的 ESM。今天的新项目、打包工具与浏览器原生都偏向 ESM,但 npm 里仍有大量 CJS 包,Node 需要同时兼容两套系统。面试与排错里常见:SyntaxError: Cannot use import statement outside a module、ERR_REQUIRE_ESM 等,都源于模块格式与 package.json 配置不一致。
知识详解与通俗解释
1. CommonJS 长什么样?
js
// math.cjs
exports.add = (a, b) => a + b;
// 或 module.exports = { add }
// main.cjs
const { add } = require('./math.cjs');特点(简化理解):
require是运行时同步加载(在 Node 里会阻塞直到模块执行完),路径可动态拼接。- 导出的是值的拷贝或引用,取决于导出的是原始类型还是对象引用。
- 每个文件被包在函数作用域里,自带
module、exports、require、__dirname、__filename。
通俗说:CJS 像「打电话订外卖,送到的当下才拆开吃」,加载时机紧跟执行到那一行。
2. ESM 长什么样?
js
// math.mjs 或 package.json 中 "type": "module" 的 .js
export function add(a, b) {
return a + b;
}
// main.mjs
import { add } from './math.mjs';特点:
import在语法上静态(多数情况需在顶层),利于**摇树优化(tree-shaking)**与工具分析。- Node 里 ESM 与 CJS 互操作时规则更严(例如从 ESM 默认
import一个 CJS 包时,有时是「整个module.exports当 default」)。 - ESM 里没有天然的
__dirname,要用import.meta.url自己换算。
通俗说:ESM 像「提前列好购物清单」,打包器和引擎能更好预判依赖图。
3. Node 怎么决定「这个 .js 是 CJS 还是 ESM」?
主要看 package.json 的 "type" 字段:
- 缺省或
"type": "commonjs":.js按 CJS 解析。 "type": "module":.js按 ESM 解析。.cjs强制 CJS,.mjs强制 ESM,与type无关。
这就是为什么拷贝一段 import 到旧项目会报错:文件扩展名与 type 没配对。
4. 互操作时的几个坑(面试常问)
- CJS
require()ESM 包:很多场景会ERR_REQUIRE_ESM,需要改用 **动态import()**或让该包提供 CJS 入口(视包维护策略而定)。 - ESM 里想用
require:默认不行;应统一用import或动态import()。 - 「双发布」包:同时提供
exports字段下的require与import条件,对库作者友好,对读者只需跟文档走。
5. 和前端工程的关系
打包工具(Webpack/Vite/Rollup)在构建时往往把多种写法编成浏览器可运行的格式;在 Node 里直接跑 TS/ESM(如 ts-node、node --loader)时,才更频繁踩到上述规则。
配置 "type": "module" 后,记得把脚本、测试、配置文件(如某些版本的 Jest)是否支持 ESM 一并核对。
总结
- CJS:
require/module.exports,Node 元老,运行时加载,动态路径友好。 - ESM:
import/export,语言标准,静态结构友好,是现代默认方向。 - Node 靠
package.json#type与.mjs/.cjs区分格式;混用时注意ERR_REQUIRE_ESM与互操作规则。 - 前端开发多在打包层「消化」差异;写 Node 工具链或跑原生 Node 脚本时,这套规则必须心中有数。