
本文详解如何在 ES 模块(ESM)主导的 Node.js 项目中安全、可靠地加载并执行 CommonJS(.cjs)文件,解决 eval 作用域隔离导致函数未定义的问题,并提供可落地的测试集成方案。
本文详解如何在 es 模块(esm)主导的 node.js 项目中安全、可靠地加载并执行 commonjs(.cjs)文件,解决 `eval` 作用域隔离导致函数未定义的问题,并提供可落地的测试集成方案。
在现代 Node.js(v14+)项目中,若主项目采用 ES 模块(即 type: "module" 在 package.json 中),直接使用 eval() 加载并执行 .js 文件会失败——因为 eval() 在严格模式下运行于独立作用域,其内部声明的函数(如 function sarasa2(){})不会泄露到外部上下文,导致 ReferenceError: sarasa2 is not defined。
根本原因在于:eval() 不是模块加载机制,它无法模拟 require() 的导出绑定与作用域桥接能力。而真正的解决方案不是规避模块系统,而是尊重并桥接两种模块规范。
✅ 正确做法:用 createRequire 加载 CommonJS 文件
Node.js 提供了官方兼容方案:node:module 模块中的 createRequire()。它能为任意文件路径创建一个符合 CommonJS 语义的 require 函数,完美支持 module.exports 和 exports.xxx 导出。
根据 package.json 中的 engines 字段自动检测 Node.js 版本需求,未安装 nvm 时自动安装并切换至对应版本。支持 Windows、macOS、Linux。适用于 Node.js 项目开发、版本切换和环境配置。
1. 将源文件改为标准 CommonJS 格式(.cjs 扩展名推荐)
// testsjs/micro/sarasa.cjs
function sarasa2() {
console.log("aca toy!");
return true;
}
// ✅ 方式一:命名导出
module.exports.sarasa2 = sarasa2;
// ✅ 或方式二:默认导出对象(更清晰)
// module.exports = { sarasa2 };? 强烈建议使用
.cjs扩展名(而非.js),避免与 ESM 文件混淆,并确保 Node.js 总是以 CommonJS 模式解析——即使项目启用了"type": "module"。
2. 在 ESM 测试文件中安全加载与调用
import { Test } from "../../myjsunit/testSuite.js";
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
class so_Test extends Test {
test_load_and_run() {
// ✅ 获取当前目录的绝对路径(ESM 中不能直接用 __dirname)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const cjsFilePath = join(__dirname, '../testsjs/micro/sarasa.cjs');
// ✅ 创建专属 require 实例,指向该 .cjs 文件所在目录
const require = createRequire(cjsFilePath);
// ✅ 同步加载:返回 module.exports 对象
const sarasaModule = require('./sarasa.cjs'); // 注意:路径相对于 createRequire 的 base dir
// ✅ 调用导出的函数
const actual = sarasaModule.sarasa2();
this.assertTrue(actual);
this.done();
}
}
export { so_Test };? 关键细节说明:
-
createRequire(filePath)的参数必须是文件路径(非 URL),且该路径决定了require()解析相对路径的基准目录(类似require在该文件内执行); - 使用
join(__dirname, ...)构建路径,确保跨平台兼容性; -
import.meta.url是 ESM 中获取当前文件 URL 的唯一标准方式,需配合fileURLToPath()转换为文件系统路径; - 不要尝试
require('./sarasa.cjs')直接导入(ESM 不允许),必须通过createRequire创建的实例调用。
⚠️ 注意事项与最佳实践
-
禁止混用
eval()加载业务逻辑:eval()存在严重安全隐患(代码注入)、调试困难、无模块作用域、无法处理require/exports,仅适用于极简原型,绝不用于测试或生产; -
明确区分模块类型:在
package.json中设置"type": "module"后,所有.js默认为 ESM;.cjs则强制为 CommonJS —— 这是 Node.js 官方推荐的共存策略; -
导出需显式声明:CommonJS 文件中必须通过
module.exports或exports.xxx显式暴露函数,否则加载后为空对象; -
测试可复用性:此方案完全兼容 Jest、Vitest 等主流测试框架(只需将
createRequire逻辑封装为工具函数); -
Botmaker 等低代码平台场景适配:如答案中所述,最终上传至 Botmaker 的脚本仍可保持纯函数体(无需
exports),但本地开发/测试阶段务必用.cjs+createRequire实现 TDD 循环,大幅提升可维护性。
通过这一模式,你既能享受 ES 模块项目的现代工程体验(TypeScript、tree-shaking、IDE 支持),又能无缝集成遗留或平台限定的 CommonJS 逻辑,真正实现“写一次,测充分,发无忧”。

















