Node.js 中如何区分 ESM 和 CJS?有哪些方式?

区分方式

1. package.json 的 type 字段
2. 文件扩展名 (.mjs / .cjs)
3. import 语句(ESM)vs require 函数(CJS)

1. package.json type 字段

// "type": "module" → .js 文件作为 ESM
{
  "type": "module"
}
// app.js → ESM

// "type": "commonjs" 或不设置 → .js 文件作为 CJS
{
  "type": "commonjs"
}
// app.js → CJS

2. 文件扩展名(覆盖 type 设置)

.mjs → 始终 ESM(不管 type)
.cjs → 始终 CJS(不管 type)
.js  → 取决于 type 字段

3. 代码中的判断

// ESM 的标志
import something from './module.js'
export const value = 1
export default {}
import.meta.url

// CJS 的标志
const something = require('./module')
module.exports = {}
__dirname
__filename

环境差异

// CJS 环境
console.log(typeof require)       // 'function'
console.log(typeof module)        // 'object'
console.log(__dirname)            // '/path/to/dir'
console.log(this)                  // module.exports

// ESM 环境
console.log(typeof require)       // 'undefined'
console.log(__dirname)            // ReferenceError!
console.log(import.meta.url)      // 'file:///path/to/file.mjs'
console.log(this)                  // undefined

ESM 中使用 __dirname

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

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

// 等价于 CJS 的 __dirname / __filename
const configPath = join(__dirname, 'config.json')

ESM 中使用 require

import { createRequire } from 'module'
const require = createRequire(import.meta.url)

// 可以在 ESM 中使用 require
const cjsModule = require('./legacy.cjs')

顶层 await

// ESM 支持顶层 await
const config = await fetch('/config').then(r => r.json())
export default config

// CJS 不支持顶层 await
// ❌ const config = await fetch(...)  // 语法错误

JSON 导入

// CJS
const config = require('./config.json')  // ✅ 原生支持

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

// 或用 fs 读取
import { readFileSync } from 'fs'
const config = JSON.parse(readFileSync('./config.json', 'utf-8'))

条件导出

// package.json
{
  "exports": {
    ".": {
      "node": {
        "import": "./dist/node/index.mjs",
        "require": "./dist/node/index.cjs"
      },
      "browser": {
        "import": "./dist/browser/index.mjs",
        "require": "./dist/browser/index.cjs"
      },
      "default": "./dist/index.cjs"
    }
  }
}

判断当前是 ESM 还是 CJS

// 方法 1:检查 import.meta
if (typeof import.meta !== 'undefined') {
  console.log('ESM')
}

// 方法 2:检查 require
if (typeof require !== 'undefined') {
  console.log('CJS')
}

// 方法 3:try-catch
let isESM = false
try {
  // @ts-ignore
  await import('data:text/javascript,')
  isESM = true
} catch {
  isESM = false
}

最佳实践

  1. 新项目用 ESM:"type": "module"
  2. 库提供双格式:exports 同时提供 import 和 require
  3. 混合项目用 .mjs/.cjs:明确区分
  4. 避免混用:同一项目尽量统一模块格式

同分类其他题目