如何解决 Node.js ESM 环境下 ReferenceError: require is not defined 错误
在现代 AI 基础设施和模型部署工具链的开发中,我们越来越依赖于高性能、模块化的 JavaScript/TypeScript 后端服务(例如使用 Node.js 构建 API Gateway 或 Serverless Functions)。然而,在从传统的 CommonJS (CJS) 迁移到 ECMAScript Modules (ESM) 的过程中,开发者经常遇到一个棘手的错误:ReferenceError: require is not defined in ES module scope, you can use import instead。
本文将深入分析这个错误产生的原因,并提供三种实操性极强的解决方案,确保你的 Node.js 项目能够平稳地拥抱 ESM。
1. 错误根源分析:CJS 与 ESM 的代际差异
什么是 CommonJS (CJS)?
CJS 是 Node.js 历史上使用的模块系统,它使用同步的 require() 函数来导入模块,并使用 module.exports 或 exports 来导出。
什么是 ECMAScript Modules (ESM)?
ESM 是 JavaScript 官方的模块标准,使用 import 和 export 关键字。ESM 具有静态分析、异步加载和 Tree Shaking 等优势,是现代 Node.js 和浏览器环境的首选。
当 Node.js 运行时将你的文件识别为 ESM 模块时(例如,因为你使用了 .mjs 扩展名或在 package.json 中设置了 “type”: “module”),它会禁用全局的 require 函数。因此,当你在一个 ESM 文件中尝试调用 require() 时,就会触发 ReferenceError。
2. 解决方案一:代码完全迁移到 ESM (推荐)
解决此问题的最直接和推荐方法是将所有 CJS 语法替换为 ESM 语法。这适用于你的代码库中所有逻辑都可以更新的情况。
CommonJS 示例:
1
2
3
4
5
6
7
8 // commonjs_example.js
const path = require('path');
const utils = require('./utils');
function logPath() {
console.log(path.resolve('data'));
}
module.exports = logPath;
迁移到 ESM:
1
2
3
4
5
6
7
8
9
10
11 // esm_example.js
import path from 'path';
// 注意:对于本地文件,必须包含文件扩展名
import { utilityFunction } from './utils.js';
function logPath() {
console.log(path.resolve('data'));
}
// 导出方式也需要改变
export default logPath;
3. 解决方案二:配置 package.json 文件
Node.js 运行时根据文件的扩展名和项目的 package.json 中的 type 字段来确定模块类型。
选项 A: 显式设置项目为 ESM
如果整个项目都应被视为 ESM,请在 package.json 中添加以下配置。这样,所有 .js 文件都会被解析为 ESM。
1
2
3
4
5 {
"name": "ai-tooling",
"version": "1.0.0",
"type": "module"
}
选项 B: 混合使用 (通过文件扩展名)
如果项目需要同时支持 CJS 和 ESM,则应删除或不设置 “type” 字段(默认为 CJS),然后使用文件扩展名来区分:
- 使用 .js 或 .cjs 扩展名:文件被视为 CommonJS,可以使用 require()。
- 使用 .mjs 扩展名:文件被视为 ESM,必须使用 import。
对于新的 AI 基础设施项目,最佳实践是设置 “type”: “module” 并全面使用 ESM。
4. 解决方案三:在 ESM 模块中兼容 CJS (高级用法)
在某些情况下,你可能运行在一个 ESM 模块中,但需要导入一个仍然只提供 CJS 模块的旧版第三方库。直接使用 import 可能会失败,而 require 又不可用。
Node.js 提供了一个内置的 module 模块中的 createRequire 函数,允许你在 ESM 作用域中创建一个局部的 require 函数来加载 CJS 模块。
使用 **createRequire 的实操代码:**
假设我们正在构建一个 ESM 部署脚本,但需要加载一个旧版的 CJS 配置库 legacy-config-parser。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 // deploy_script.mjs (这是一个 ESM 文件)
import { createRequire } from 'module';
// 1. 创建 require 函数,它基于当前文件的 URL 进行路径解析
const require = createRequire(import.meta.url);
// 2. 现在可以使用这个局部的 require 来导入 CJS 模块
const legacyConfig = require('legacy-config-parser');
console.log('Successfully loaded legacy CJS config:', legacyConfig.version);
// 3. 其他新模块继续使用 ESM 导入
import { promisify } from 'util';
// ... 脚本逻辑
通过使用 createRequire,我们成功地桥接了 ESM 和 CJS 模块,解决了 ReferenceError: require is not defined 的问题,同时保持了主文件采用现代的 ESM 结构。
汤不热吧