来源
- CommonJS 是社区(CommonJS 项目组)约定,非语言规范(2009 - 2010)。
- ES Module 是 ECMAScript 语言标准的一部分(2015 年(规范正式发布)。
导入:
- CJS 的
require()是同步的:require 执行到那一刻才会得到导入结果。 - ESM 的
import默认处于顶层位置。是模块级别的静态导入,会在模块解析阶段就建立依赖关系。ESM 也有动态导入:await import("./xxx.js")。
导出方式
-
CJS
- 导出对象:
exports = module.exports = { add } - 默认导出:
module.exports = function add() {}
- 导出对象:
-
ESM
- 导出对象:
export function add () {} - 默认导出:
export default function add() {}
- 导出对象:
导出内容
-
CJS
- 模块导出的是 值的拷贝(基本类型复制值,对象类型复制引用)。
- 导入方允许修改
- 导出方改变基本数据类型,替换整个对象,导入方均不会看到变化。原始对象上修改可以。
-
ESM
- 是活绑定,导出方修改可以实时看到变化。
- 导入方不可以修改。
使用环境
- CJS:服务器端(Node环境)
- 文件扩展名
.mjs; package.json设置"type": "module"。
- 文件扩展名
- ESM:浏览器 + 服务器端(通用)
- 标签引入:
<script type="module" src="index.js"></script>
- 标签引入:
注意事项
-
Node 用 package.json 的 "type" 决定 .js 是按 CJS 还是 ESM 解释。
- "type" 只影响当前 package 范围内的
.js。.mjs永远按 ESM,.cjs永远按 CJS。
- "type" 只影响当前 package 范围内的
-
ESM 下没有
__dirname/__filename(需要用 import.meta.url + fileURLToPath 处理)。import path from "node:path"; import { fileURLToPath } from "node:url"; // ESM 下获取所在文件夹地址 const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); console.log(__dirname); -
两套体系可以互相引用,但“互操作”不是随便写就能稳定运行,通常需要正确的后缀(.cjs/.mjs)、正确的导入方式,或者借助 createRequire。
// 在 CJS 代码里加载 ESM(require 不行),要用 import()(动态导入) // CJS 加载 ESM 的安全写法 (async () => { const mod = await import("./esm.mjs"); // 不可顶层导入 console.log(mod.default); })(); -
不要用 require 和 import 去加载
非模块化文件。json 文件 CJS 可以直接 reuquire,ESM 需要额外处理。
处理 json 文件
-
CJS: Node.js 内置了对 CommonJS 的 require 的支持,当 require 一个 .json 文件时,它会:读取文件内容;将内容解析为 JSON 对象;直接返回该对象。
-
ESM: ES6 的 import 语法是静态分析且默认只处理 JavaScript 模块。在 Node.js 中,直接使用
import pkg from "../package.json"会报错,因为:Node.js 默认不识别.json文件作为 ES 模块
解决方法
- 声明类型为
json
import pkg from "../package.json" with { type: "json" };
console.log(pkg);
const pkg = await import("../package.json", { with: { type: "json" } });
console.log(pkg.default); // 输出直接可使用的 json 对象
- 运行中读取
import { readFileSync } from "fs";
const pkg = JSON.parse(
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
);
- ESM 下的替代方案
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const pkg = require("../package.json");
requier 同步导入 ≠ 动态导入
- require() 是 CommonJS 的同步加载函数,并且在运行时执行
- 它“看起来动态”的点在于:你可以在 if、函数体里、甚至用变量拼路径后再调用 require()
- 可以说 require 是 运行时加载 / 可动态调用
- 说动态导入容易和
import()混淆。
既然 require 是运行时加载,那 webpack 怎么在编译的时候分析依赖关系呢?
不矛盾,因为这是两件事
- require 的“同步”描述的是
运行时行为:代码执行到这一行时,Node 同步加载并返回模块。 - webpack 分析依赖发生在构建时:它不执行你的业务逻辑,而是先把源码当文本做语法解析(AST),从中提取
require/import语句,建立依赖图。
// 对这种写法,webpack 很容易静态分析:
const a = require("./a");
// 对这种动态表达式,静态分析就困难:
const a = require("./" + name);
相对路径的不同含义(扩展)
- 在 Node.js 中,任何文件系统操作(如 fs.readFile、fs.stat、fs.createReadStream 等),如果传入的是相对路径,都会相对于当前工作目录(Current Working Directory, cwd) 进行解析。
- 当前工作目录是启动 Node.js 进程时的目录,可以通过 process.cwd() 获取。
- 因此在Node中操作文件,建议用绝对路径。
// 假设当前工作目录是 /home/user/project
// 文件 /home/user/project/data.txt 存在
fs.readFileSync("./data.txt"); // 读取 /home/user/project/data.txt
// 如果运行 node src/app.js,而 src/app.js 内部有上述代码,
// 因为进程是在 /home/user/project 启动的,所以仍然读取 /home/user/project/data.txt
- 当使用 require('./foo') 或 import './foo.js' 时,相对路径的解析基准是当前模块文件所在的目录。
// 文件 /home/user/project/src/app.js
require("../data.txt"); // 解析为 /home/user/project/data.txt