CommonJS 和 ESM 如何互操作(互导)?有什么坑?

ESM 导入 CJS

// CJS 模块 (math.cjs)
module.exports = { add: (a, b) => a + b, PI: 3.14 }

// ESM 导入
import math from './math.cjs'       // default 导入 = module.exports
console.log(math.add(1, 2))          // 3

// 命名导入(Node.js 支持,但有坑)
import { add } from './math.cjs'     // 可能可以,取决于环境

坑 1:命名导入不一定可用

CJS 的 module.exports 是运行时确定的,ESM 的 import {} 是编译时确定的。Node.js 通过静态分析尝试提供命名导入,但不保证成功:

// CJS 动态导出(ESM 无法静态分析)
if (condition) {
  module.exports = { a: 1 }
} else {
  module.exports = { b: 2 }
}

// ESM 中命名导入可能失败
import { a } from './dynamic.cjs'  // 可能报错或 undefined

安全做法:

import pkg from './math.cjs'
const { add, PI } = pkg  // 运行时解构

CJS 导入 ESM

CJS 不能用 require 加载 ESM(因为 ESM 是异步的),必须用 import():

// CJS 中加载 ESM
async function load() {
  const mod = await import('./math.mjs')
  console.log(mod.add(1, 2))
}
load()

坑 2:default 导出差异

// ESM 模块
export default { add: (a, b) => a + b }

// CJS 中 import()
const mod = await import('./math.mjs')
console.log(mod.default.add(1, 2))  // 需要通过 .default
console.log(mod.add)                 // undefined(没有命名导出 add)

坑 3:__dirname 和 __filename

ESM 中没有 __dirname 和 __filename:

// CJS
console.log(__dirname)  // /path/to/dir
console.log(__filename) // /path/to/dir/file.js

// ESM 替代方案
import { fileURLToPath } from 'url'
import { dirname } from 'path'

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

坑 4:JSON 导入

// CJS
const config = require('./config.json')  // ✅ 直接可用

// ESM(需要 assert 语法)
import config from './config.json' with { type: 'json' }
// 或旧语法
import config from './config.json' assert { type: 'json' }

// 兼容方案:读取文件
import { readFileSync } from 'fs'
const config = JSON.parse(readFileSync('./config.json', 'utf-8'))

坑 5:require.resolve

// CJS
const path = require.resolve('./module')

// ESM 需要使用 import.meta.resolve(实验性)
const path = import.meta.resolve('./module')

双格式包(Dual Package)

同时提供 CJS 和 ESM 两种入口:

// package.json
{
  "name": "my-lib",
  "exports": {
    "import": "./dist/index.mjs",   // ESM 入口
    "require": "./dist/index.cjs"    // CJS 入口
  }
}

最佳实践

  1. 新项目一律使用 ESM
  2. 库项目提供双格式包
  3. ESM 中导入 CJS 用 default import + 解构
  4. 避免在 ESM 中依赖 require

同分类其他题目