ESM (ES Modules) 和 CJS (CommonJS)
是 JavaScript 的两种模块系统。
📊 核心概念对比表
| 特性 | ESM (ES Modules) | CJS (CommonJS) |
|---|---|---|
| 诞生时间 | 2015年(ES6标准) | 2009年(Node.js 原创) |
| 加载方式 | 静态编译时加载 | 动态运行时加载 |
| 语法 | import/export |
require()/module.exports |
| 文件扩展名 | .js, .mjs |
.js, .cjs |
| 使用场景 | 现代浏览器、Node.js ≥ 13.2.0 | Node.js 传统项目、旧系统 |
| Tree Shaking | ✅ 支持 | ❌ 不支持 |
🎯 简单理解
CJS (CommonJS) - "传统方式"
javascript
// 像传统的"商店购物"
// 需要的时候才去拿(运行时加载)
// 导入(买东西)
const fs = require('fs'); // 同步加载
const _ = require('lodash');
// 导出(卖东西)
module.exports = { name: '张三' };
exports.sayHello = function() {};
ESM (ES Modules) - "现代方式"
javascript
// 像现代的"工厂装配线"
// 提前规划好需要什么(编译时分析)
// 导入(声明需要的零件)
import fs from 'fs'; // 异步加载
import { map } from 'lodash';
import * as utils from './utils.js';
// 导出(提供产品)
export const name = '张三';
export function sayHello() {};
export default { name: '张三' };
🔧 实际代码示例
CJS 项目示例
javascript
// 📁 math.js (导出)
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
// 方式1:导出整个对象
module.exports = { add, multiply };
// 方式2:分别导出
exports.add = add;
exports.multiply = multiply;
// 📁 main.js (导入)
// 方式1:导入整个模块
const math = require('./math.js');
console.log(math.add(2, 3)); // 5
// 方式2:解构导入
const { add, multiply } = require('./math.js');
console.log(multiply(2, 3)); // 6
// 动态导入(Node.js 14+)
const path = './math.js';
const dynamicMath = require(path); // 路径可以是变量
ESM 项目示例
javascript
// 📁 math.mjs 或 math.js (导出)
// 命名导出
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// 默认导出
const PI = 3.14159;
export default PI;
// 📁 main.mjs 或 main.js (导入)
// 方式1:命名导入
import { add, multiply } from './math.js';
console.log(add(2, 3)); // 5
// 方式2:全部导入
import * as math from './math.js';
console.log(math.multiply(2, 3)); // 6
// 方式3:默认导入
import PI from './math.js';
console.log(PI); // 3.14159
// 动态导入(返回 Promise)
const path = './math.js';
import(path).then(module => {
console.log(module.add(2, 3));
});
📁 项目环境判断
如何判断项目是 ESM 还是 CJS?
json
// 查看 package.json
{
// 情况1:明确声明 ESM
"type": "module", // ← 这是 ESM 项目
// 情况2:明确声明 CJS
"type": "commonjs", // ← 这是 CJS 项目
// 情况3:没有 type 字段
// 默认是 CJS 项目
}
文件扩展名约定
bash
# ESM 环境
- .js # 当 package.json 有 "type": "module"
- .mjs # 总是 ESM,无论 package.json
# CJS 环境
- .js # 当 package.json 没有 type 或 "type": "commonjs"
- .cjs # 总是 CJS,无论 package.json
🏗️ 架构差异对比
CJS (CommonJS) 架构
text
┌─────────────────┐
│ main.js │
│ │
│ const a = │
│ require('a') │
│ │
│ const b = │
│ require('b') │
│ │
│ // 代码执行 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 运行时加载 │
│ 1. 执行到 │
│ require() │
│ 2. 同步读取 │
│ 文件 │
│ 3. 执行模块 │
│ 4. 返回结果 │
└─────────────────┘
ESM (ES Modules) 架构
text
┌─────────────────┐
│ main.js │
│ │
│ import a from │
│ 'a' │
│ │
│ import b from │
│ 'b' │
│ │
│ // 代码执行 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 编译时分析 │
│ 1. 解析所有 │
│ import │
│ 2. 构建依赖图 │
│ 3. 异步加载 │
│ 所有模块 │
│ 4. 执行代码 │
└─────────────────┘
⚡ 关键特性对比
加载时机
javascript
// CJS:运行时加载(同步)
const config = require('./config.js'); // 阻塞执行,直到加载完成
console.log('这里要等 config 加载完');
// ESM:编译时加载(异步)
import config from './config.js'; // 非阻塞,预加载
console.log('这里可能先执行');
循环依赖处理
javascript
// CJS:可能有问题
// a.js
exports.loaded = false;
const b = require('./b.js');
exports.loaded = true;
// b.js
const a = require('./a.js');
// 此时 a.loaded 是 false!
// ESM:更好的处理
// a.mjs
export let loaded = false;
import { setLoaded } from './b.mjs';
loaded = true;
// b.mjs
import { loaded } from './a.mjs';
// loaded 是 undefined(但不会部分加载)
export function setLoaded() {}
Tree Shaking(摇树优化)
javascript
// ESM 支持(打包工具可以删除未使用的代码)
// math.js
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export function unused() { console.log('我不会被打包'); }
// main.js
import { add } from './math.js';
// 打包后只包含 add 函数,multiply 和 unused 被删除
// CJS 不支持(很难静态分析)
const math = require('./math.js');
// 打包工具不知道你用了 math 的哪些部分
🚀 实际应用场景
场景1:Node.js 后端项目
json
// 传统 Node.js 项目(CJS)
// package.json
{
"name": "backend",
"main": "index.js", // 默认 .js 是 CJS
"scripts": {
"start": "node index.js"
}
}
// index.js
const express = require('express'); // CJS
const app = express();
module.exports = app;
场景2:现代前端项目(ESM)
json
// Vite/Webpack 5+ 项目(ESM)
// package.json
{
"name": "frontend",
"type": "module", // 声明为 ESM
"scripts": {
"dev": "vite"
},
"dependencies": {
"vue": "^3.0.0" // Vue 3 是 ESM
}
}
// main.js
import { createApp } from 'vue'; // ESM
import App from './App.vue';
场景3:混合项目
json
// 支持两种模块系统
// package.json
{
"name": "universal-library",
"type": "module", // 默认 ESM
"exports": {
".": {
"import": "./dist/esm/index.js", // ESM 入口
"require": "./dist/cjs/index.js" // CJS 入口
}
}
}
🔄 互相调用
在 ESM 中调用 CJS
javascript
// 📁 cjs-module.cjs (CJS)
module.exports = {
hello: function() {
return 'Hello from CJS';
}
};
// 📁 esm-module.js (ESM)
// 可以导入 CJS 模块
import cjsModule from './cjs-module.cjs'; // 需要 .cjs 扩展名
console.log(cjsModule.hello()); // "Hello from CJS"
// 或者使用 createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjs = require('./cjs-module.cjs');
在 CJS 中调用 ESM
javascript
// 📁 esm-module.mjs (ESM)
export function hello() {
return 'Hello from ESM';
}
// 📁 cjs-module.js (CJS)
// 必须使用动态 import()(返回 Promise)
async function main() {
const esmModule = await import('./esm-module.mjs'); // 需要 .mjs 扩展名
console.log(esmModule.hello()); // "Hello from ESM"
}
main();
// 注意:不能使用 require()
// const esm = require('./esm-module.mjs'); // ❌ 会报错
🛠️ 配置差异
ESLint 在不同环境下的配置
javascript
// 📁 .eslintrc.cjs (CJS 配置文件)
// 用于 CJS 项目或通用项目
module.exports = {
env: { node: true },
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module' // 这里指的是解析的代码类型
}
};
// 📁 .eslintrc.js (ESM 配置文件)
// 用于 ESM 项目
export default {
env: { node: true },
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
}
};
TypeScript 配置
json
// tsconfig.json (ESM 项目)
{
"compilerOptions": {
"module": "esnext", // ESM
"moduleResolution": "node",
"target": "es2020",
"outDir": "./dist/esm"
}
}
// tsconfig.cjs.json (CJS 项目)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs", // CJS
"outDir": "./dist/cjs"
}
}
⚠️ 常见问题
问题1:Error [ERR_REQUIRE_ESM]
bash
# 在 CJS 项目中尝试 require() ESM 模块
# 错误:require() of ES Module not supported
# 解决方案:
# 1. 将文件重命名为 .cjs
# 2. 使用动态 import()
# 3. 将项目转换为 ESM
问题2:__dirname 在 ESM 中不可用
javascript
// CJS 中有
console.log(__dirname); // /path/to/file
console.log(__filename); // /path/to/file/index.js
// ESM 中需要使用
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
问题3:模块扩展名
javascript
// ESM 需要明确扩展名
import './module.js'; // ✅
import './module'; // ❌ 在浏览器中会报错
// CJS 可以省略
require('./module'); // ✅ 自动查找 .js, .json, .node
✅ 选择建议
选择 ESM 的情况
-
✅ 新项目
-
✅ 前端项目(Vite、Webpack 5+)
-
✅ 需要 Tree Shaking 优化
-
✅ 需要更好的静态分析
-
✅ 目标环境支持(Node.js ≥ 14.8.0)
选择 CJS 的情况
-
✅ 维护现有 Node.js 项目
-
✅ 依赖大量仅支持 CJS 的包
-
✅ 需要动态 require()
-
✅ 兼容旧版本 Node.js
-
✅ 不需要打包优化的脚本
最佳实践
bash
# 1. 新项目优先选择 ESM
# 2. 库/包同时提供两种格式
# 3. 明确声明模块类型
# 4. 使用正确的文件扩展名
📚 历史发展
text
2009: CommonJS 诞生(Node.js 原创)
2015: ES6 标准引入 ES Modules
2017: Node.js 8.5.0 实验性支持 ESM
2019: Node.js 13.2.0 正式支持 ESM
2020: 主流打包工具全面支持 ESM
2023: ESM 成为现代 JavaScript 标准
简单记忆:
-
CJS =
require()/module.exports= Node.js 传统方式 -
ESM =
import/export= JavaScript 现代标准
对于新项目,推荐使用 ESM ,它是 JavaScript 的未来标准。对于维护现有项目或特定需求,可以选择 CJS。