前言

JavaScript 有两种主流的模块系统:CommonJS (CJS)ES Modules (ESM)

  • CJS:Node.js 最早使用的模块系统
  • ESM:JavaScript 官方标准模块系统(ES6 引入)

简单记忆:新项目用 ESM,老项目可能还在用 CJS。

CommonJS (CJS)

基本语法

// 导出
module.exports = {
  name: 'foo',
  sayHello: () => 'Hello',
}

// 或者
exports.foo = 'foo'
exports.bar = 'bar'

// 导入
const utils = require('./utils')
const { foo, bar } = require('./utils')

关键特点

  1. 运行时加载:代码执行到 require 时才加载模块
  2. 值拷贝:导出的是值的拷贝,不是引用
  3. 动态导入:可以在任何地方使用 require
// 条件导入
if (process.env.NODE_ENV === 'dev') {
  const devTools = require('./dev-tools')
}

ES Modules (ESM)

基本语法

// 导出
export const foo = 'foo'
export const bar = 'bar'

// 默认导出
export default {
  name: 'foo',
}

// 导入
import { foo, bar } from './utils.js'
import utils from './utils.js'
import * as utils from './utils.js'

关键特点

  1. 编译时加载:在代码执行前就确定了依赖关系
  2. 实时绑定:导出的是值的引用,会实时更新
  3. 静态分析:工具可以更好地优化(Tree Shaking)
// 动态导入
button.addEventListener('click', async () => {
  const Modal = await import('./Modal.js')
  new Modal()
})

核心区别

特性CommonJSES Modules
语法require / module.exportsimport / export
加载时机运行时编译时
导出方式值拷贝实时绑定
文件扩展名.cjs.js.mjs.js + type: "module"
顶级 await❌ 不支持✅ 支持

例子:值拷贝 vs 实时绑定

// counter.js
let count = 0
export function increment() {
  count++
}
export { count }

// main.js (ESM)
import { count, increment } from './counter.js'
console.log(count) // 0
increment()
console.log(count) // 1(实时更新)

// main.js (CJS)
const { count, increment } = require('./counter.js')
console.log(count) // 0
increment()
console.log(count) // 仍然是 0(值拷贝)

如何选择模块系统

启用 ESM

package.json 中添加:

{
  "type": "module"
}

现在所有 .js 文件都会被当作 ESM 处理。

文件扩展名

  • .js + type: "module" → ESM
  • .mjs → 强制 ESM
  • .cjs → 强制 CJS

推荐方案

// ✅ 新项目:使用 ESM
{
  "type": "module"
}

// ✅ 库项目:同时支持两种
{
  "exports": {
    "import": "./index.js",
    "require": "./index.cjs"
  }
}

常见转换

CJS → ESM

// CJS
const fs = require('fs')
module.exports = { foo: 'foo' }

// ESM
import fs from 'fs'
export const foo = 'foo'

**dirname 和 **filename

ESM 中没有 __dirname__filename,需要自己实现:

import { fileURLToPath } from 'url'
import { dirname } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

互操作性

ESM 导入 CJS

// math.cjs
module.exports.add = (a, b) => a + b

// main.mjs
import math from './math.cjs'
math.add(1, 2)

CJS 导入 ESM

// utils.mjs
export const helper = () => {}

// main.cjs
// ❌ 不能用 require
// const utils = require('./utils.mjs')

// ✅ 使用动态导入
async function loadUtils() {
  const { helper } = await import('./utils.mjs')
  helper()
}

最佳实践

1. 新项目用 ESM

{
  "type": "module"
}

2. 不要混用

// ❌ 错误:在 ESM 中用 require
import { require } from 'module'
const math = require('./math')

// ✅ 正确
import math from './math.js'

3. 使用一致的扩展名

src/
├── utils/
│   ├── math.js      # ESM
│   └── helper.cjs   # CJS

常见问题

Q: 报错 “Cannot use import statement outside a module”

A: 在 package.json 中添加 "type": "module",或将文件改为 .mjs

Q: 报错 “require is not defined”

A: ESM 模式下不能使用 require,改用 import

Q: Node.js 版本要求?

A:

  • Node.js 13.2+:实验性支持
  • Node.js 16+:稳定支持
  • Node.js 20+:完整支持

总结

场景选择
新项目✅ ESM
使用 Vite / Rollup✅ ESM
兼容老版本 Node.jsCJS
- 维护老项目保持 CJS

参考资源