Skip to content

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 moduleERR_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 里会阻塞直到模块执行完),路径可动态拼接。
  • 导出的是值的拷贝或引用,取决于导出的是原始类型还是对象引用。
  • 每个文件被包在函数作用域里,自带 moduleexportsrequire__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".jsCJS 解析。
  • "type": "module".jsESM 解析。
  • .cjs 强制 CJS,.mjs 强制 ESM,与 type 无关。

这就是为什么拷贝一段 import 到旧项目会报错:文件扩展名与 type 没配对

4. 互操作时的几个坑(面试常问)

  • CJS require() ESM 包:很多场景会 ERR_REQUIRE_ESM,需要改用 **动态 import()**或让该包提供 CJS 入口(视包维护策略而定)。
  • ESM 里想用 require:默认不行;应统一用 import 或动态 import()
  • 「双发布」包:同时提供 exports 字段下的 requireimport 条件,对库作者友好,对读者只需跟文档走。

5. 和前端工程的关系

打包工具(Webpack/Vite/Rollup)在构建时往往把多种写法编成浏览器可运行的格式;在 Node 里直接跑 TS/ESM(如 ts-nodenode --loader)时,才更频繁踩到上述规则。
配置 "type": "module" 后,记得把脚本、测试、配置文件(如某些版本的 Jest)是否支持 ESM 一并核对。


总结

  • CJSrequire/module.exports,Node 元老,运行时加载,动态路径友好。
  • ESMimport/export,语言标准,静态结构友好,是现代默认方向。
  • Node 靠 package.json#type.mjs/.cjs 区分格式;混用时注意 ERR_REQUIRE_ESM 与互操作规则。
  • 前端开发多在打包层「消化」差异;写 Node 工具链或跑原生 Node 脚本时,这套规则必须心中有数。