ESM (ES Modules) 和 CJS (CommonJS)

ESM (ES Modules) 和 CJS (CommonJS) 

是 JavaScript 的两种模块系统。


📊 核心概念对比表

特性ESM (ES Modules)CJS (CommonJS)
诞生时间2015年(ES6标准)2009年(Node.js 原创)
加载方式静态编译时加载动态运行时加载
语法import/exportrequire()/module.exports
文件扩展名.js.mjs.js.cjs
使用场景现代浏览器、Node.js ≥ 13.2.0Node.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 的情况

  1. ✅ 新项目

  2. ✅ 前端项目(Vite、Webpack 5+)

  3. ✅ 需要 Tree Shaking 优化

  4. ✅ 需要更好的静态分析

  5. ✅ 目标环境支持(Node.js ≥ 14.8.0)

选择 CJS 的情况

  1. ✅ 维护现有 Node.js 项目

  2. ✅ 依赖大量仅支持 CJS 的包

  3. ✅ 需要动态 require()

  4. ✅ 兼容旧版本 Node.js

  5. ✅ 不需要打包优化的脚本


最佳实践

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端开发_穆金秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值