从 CommonJS 到 ESM:你的 Node.js 项目正在“掉队”吗?

从 CommonJS 到 ESM:你的 Node.js 项目正在“掉队”吗?

还记得上次你部署一个新功能,结果整个服务突然崩溃,日志里全是 require is not defined 吗?或者,你花了一整天升级依赖包,却发现模块加载顺序完全乱套,连 __dirname 都找不到了?如果你是个 Node.js 开发者,这些场景肯定不陌生。

今天,越来越多的企业项目开始拥抱 ES Module(ESM)——这是 JavaScript 官方标准模块系统,也是现代 Node.js 生态的默认选择。但迁移过程,就像翻新一栋老房子:看起来简单,拆开墙才发现到处都是“雷”。特别是那些从 CommonJS(CJS)时代延续下来的老项目,require()module.exports__dirname 就像老房子的砖瓦,稍有不慎就会塌方。

如果你正在为这些问题头疼,别慌。这篇文章会带你一步步走过 CJS 到 ESM 的迁移之路,把那些最坑的细节都讲清楚。读完你会发现,这不仅是“升级”,更是让你的代码更健壮、更现代化的必经之路。

为什么你的“老代码”突然不能用了?

核心问题:CJS 和 ESM 是两套完全不同的模块系统

CommonJS(CJS)是 Node.js 诞生时就有的模块方案,它的核心是 require()module.exports。你写的每一行 const x = require('y'),都是同步加载模块——这在服务器端没问题,但在浏览器端或者需要异步加载的场景下,就成了瓶颈。

ES Module(ESM)是 ECMAScript 官方标准,用 importexport 关键字。它支持静态分析(编译时就能知道依赖关系)、异步加载、还有更好的 tree-shaking(消除无用代码)。简单说,ESM 更安全、更高效、也更符合现代 JavaScript 的规范。

但问题来了:如果你在 package.json 里设置了 "type": "module",或者你的文件用了 .mjs 后缀,Node.js 就会强制使用 ESM 解析规则。这时候,你原本的 require() 调用会直接报错,__dirname 也会消失——因为 ESM 没有这些全局变量。

第一步:从 requireimport,没那么简单

静态导入:最基础的转换

如果你只是在文件顶部用 require() 加载模块,比如:

// 老代码 const dotenv = require('dotenv'); const { dirname } = require('path'); 

直接改成:

// 新代码 import dotenv from 'dotenv'; import { dirname } from 'path'; 

但这里有个坑:本地模块必须加上 .js 扩展名。CJS 里你可以写 require('./config'),Node.js 会自动补全 .js。但在 ESM 里,你必须明确写 import config from './config.js'。少写一个 .js,就会报 ERR_MODULE_NOT_FOUND

动态导入:if 语句里的 require 怎么办?

有些代码会在 try-catch 或条件语句里动态加载模块,比如:

let moduleName; try {   moduleName = require('./optional-module'); } catch (e) {   console.warn('模块加载失败:', e.message); } 

这种场景,你必须用 import() 函数(注意,这是异步的):

let moduleName; (async () => {   try {     const imported = await import('./optional-module.js');     moduleName = imported.default || imported;   } catch (e) {     console.warn('模块加载失败:', e.message);   } })(); 

关键点import() 返回的是一个 Promise,解析后得到的是一个模块对象,而不是直接的值。你通常需要访问 .default 属性才能获取到默认导出。

第二步:告别 module.exports,拥抱 export

CJS 里你经常这么写:

module.exports = { config, helper }; module.exports = someVariable; 

在 ESM 里,这变成了:

export default { config, helper }; export default someVariable; 

如果你有多个命名导出,比如:

// CJS module.exports = { foo, bar };

// ESM export { foo, bar };

注意:ESM 的 export default 只能有一个。如果你之前用 module.exports 导出了一个对象,迁移时很可能需要拆分成多个命名导出,或者只保留一个默认导出。

第三步:__dirname__filename 消失了?

这是迁移中最容易踩的坑。在 ESM 中,__dirname__filename 不再存在。你需要自己从 import.meta.url 中提取:

import { dirname } from 'path'; import { fileURLToPath } from 'url';

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

这个代码看起来简单,但如果你在代码里大量使用了 __dirname(比如拼接路径、读取配置文件),你就得一个一个地方改。建议把这段定义放在文件顶部,然后全局替换 __dirname 的引用。

第四步:那些“聪明”的函数,比如 safeRequire

很多项目里都有这样的工具函数,用来安全地加载可选模块:

function safeRequire(modulePath, label) {   try {     const module = require(modulePath);     console.log(✅ ${label});     return module;   } catch (e) {     console.log(❌ ${label}加载失败:, e.message);     return null;   } } 

在 ESM 里,这个函数必须完全重构。因为 require() 被禁用了,你只能用异步的 import()

async function safeImport(modulePath, label, options = {}) {   try {     const mod = await import(modulePath);     const module = mod.default || mod;     if (options.initFn) {       await options.initFn(module);     }     console.log(✅ ${label});     return module;   } catch (e) {     console.log(❌ ${label}加载失败:, e.message);     return null;   } } 

注意:这个函数现在是异步的,所有调用它的地方也要改成 await 或者 .then()

第五步:循环依赖——ESM 的“隐形杀手”

CJS 对循环依赖的处理比较“宽容”,因为它是同步加载的,遇到循环依赖时,会返回一个未完全初始化的对象。ESM 则严格得多,它会静态分析所有依赖,循环依赖会导致死锁或报错。

解决方案

  • 将循环依赖的模块改为动态
import()(见上面的异步加载)
  • 或者重构代码,消除循环依赖。比如把公共部分提取出来,或者用事件订阅模式代替直接引用

第六步:验证你的迁移是否成功

改完所有代码后,别急着上线。先做这三件事:

  1. 1. 语法检查
node --check server.js,确保没有语法错误
  1. 2. 运行测试
npm test,看看现有测试是否全部通过
  1. 3. 手动测试
:跑一遍主要功能,特别是那些涉及动态加载、路径拼接、配置文件读取的地方

如果遇到问题,最常见的错误就是:

  • 路径忘记加
.js 扩展名
  • import()
返回的对象结构理解错了(记得加 .default
  • 异步代码没有用
await.then() 处理

迁移不是终点,而是起点

从 CJS 到 ESM 的迁移,表面上是改几个关键字,实际上是对整个项目模块化思维的重塑。这个过程可能会遇到各种“坑”,但每填一个坑,你的代码就会变得更健壮、更现代化。

但说实话,对于很多企业来说,手动迁移几百个文件的风险太高了。 一个疏忽,就可能导致线上服务中断。

这时候,你需要的是一支专业的团队来帮你做这件事。

让 BossAgents(左帮右臂)帮你搞定这一切

我们是一家专注于企业级 Node.js 智能体开发和现代化改造的公司。我们的“数字员工”可以:

  • 自动化迁移
:扫描你的整个项目,自动识别所有 CJS 代码,并生成 ESM 版本的代码
  • 智能适配
:处理那些复杂的动态导入、循环依赖、__dirname 路径拼接等坑
  • 全面测试
:迁移后自动运行测试,确保功能 100% 一致
  • 持续集成
:帮你把迁移流程集成到 CI/CD 管道中,以后每次更新都自动保持 ESM 兼容

别再让你的项目“掉队”了。 点击下方链接,预约一次免费的项目评估。我们会在 24 小时内给你一份详细的迁移方案,包括风险点、工作量和预期收益。

BossAgents(左帮右臂)——让你的代码,跑得更快、更稳、更现代。

← 返回案例列表
分享:
🤖 Try Now →
🤖
🎁