server.js CJS 到 ESM 转换指南
概述
本指南帮助你将 server.js 从 CommonJS (CJS) 转换为 ES Module (ESM) 格式。
转换步骤
1. 顶部静态 import(已完成部分)
文件顶部已经部分转换,确保包含以下 import 语句:
import dotenv from 'dotenv';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
dotenv.config({ path: __dirname + '/.env', override: true });
process.setMaxListeners(0);
import * as http from 'http';
import * as https from 'https';
import * as fs from 'fs';
import * as path from 'path';
import * as url from 'url';
import * as crypto from 'crypto';
// 加载统一日志工具
import logger from './server/utils/logger.js';
2. 转换所有 require() 调用
#### 2.1 静态 require()(顶层,不在 try-catch 或条件语句中)
转换前:
const moduleName = require('./path/to/module');
const { namedExport } = require('./path/to/module');
转换后:
import moduleName from './path/to/module.js';
import { namedExport } from './path/to/module.js';
注意:
- 所有本地模块路径必须添加
.js扩展名 - 内置模块使用
import as语法:import as http from 'http';
#### 2.2 动态 require()(在 try-catch 或条件语句中)
转换前:
let moduleName;
try {
moduleName = require('./path/to/module');
} catch (e) {
console.warn('模块加载失败:', e.message);
}
转换后:
let moduleName;
(async () => {
try {
const imported = await import('./path/to/module.js');
moduleName = imported.default || imported;
} catch (e) {
console.warn('模块加载失败:', e.message);
}
})();
注意: import() 返回的是 { default: ... } 结构,需要根据实际情况访问。
3. 转换 module.exports
转换前:
module.exports = { config };
module.exports = someVariable;
转换后:
export default { config };
export default someVariable;
命名导出:
// 转换前
module.exports = { foo, bar };
// 转换后
export { foo, bar };
4. 处理 __dirname 和 __filename
ESM 中没有 __dirname 和 __filename,需要手动定义:
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
5. 处理动态 import() 的特殊情况
#### 5.1 safeRequire 和 safeRequireAsync 函数
这些函数需要完全重构,因为它们内部使用 require()。
方案 1:转换为异步函数
async function safeImport(modulePath, label, options = {}) {
try {
const mod = await import(modulePath);
const module = mod.default || mod;
if (options.initFn) {
const result = await options.initFn(module);
if (result && typeof result.then === 'function') return result.then(() => { console.log(`✅ ${label}`); return module; });
}
console.log(`✅ ${label}`);
return module;
} catch (e) {
console.log(`❌ ${label}加载失败:`, e.message);
return null;
}
}
方案 2:使用顶层并行 import
如果模块不是真正可选的,最好在文件顶部使用静态 import。
6. 处理循环依赖
ESM 的静态分析可能导致循环依赖问题。如果遇到问题:
- 将循环依赖的模块改为动态
import() - 或者重构代码消除循环依赖
7. 验证转换结果
- 语法检查:
node --check server.js
- 运行测试:
npm test
- 手动测试主要功能
自动化转换脚本
使用以下脚本可以自动转换大部分内容(但可能需要手动修复):
# 安装转换工具
npm install --save-dev @babel/core @babel/cli jscodeshift
# 使用 jscodeshift 转换(需要 codemod 脚本)
npx jscodeshift --transform convert-cjs-to-esm.js server.js
# 或使用 Babel(需要配置)
npx babel server.js --out-file server.esm.js --presets @babel/preset-env
常见问题
Q1: import 路径错误
A: 确保所有本地模块路径都有 .js 扩展名,例如:
./config-loader→./config-loader.js./server/utils/logger→./server/utils/logger.js
Q2: 动态 import() 返回的对象结构
A: import() 返回 { default: ... },访问时需要注意:
const mod = await import('./module.js');
const actualModule = mod.default || mod;
Q3: __dirname 在 ESM 中不可用
A: 使用 import.meta.url 和 fileURLToPath 来获取等效值(参见第4节)。
手动转换检查清单
- [ ] 所有
require()已转换为import - [ ] 所有本地模块路径已添加
.js扩展名 - [ ]
module.exports已转换为export - [ ]
__dirname和__filename已正确定义 - [ ] 动态
import()已在async函数中使用 - [ ]
safeRequire和safeRequireAsync已重构 - [ ] 没有循环依赖问题
- [ ] 文件通过语法检查
- [ ] 所有测试通过
参考资料
- Node.js ESM 文档: https://nodejs.org/docs/latest-v22.x/api/esm.html
- MDN import 语句: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
- CJS 到 ESM 迁移指南: https://flaviocopes.com/es-modules/
BossAgents