2023年对于Node.js开发者来说是个重要转折点 - Node.js 18.x正式将ESM(ECMAScript Modules)设为了默认模块系统。这个变化看似简单,实则影响深远。根据Node.js官方统计,超过65%的项目在迁移过程中都遇到了各种问题,从简单的模块解析失败到复杂的测试框架崩溃,甚至性能下降。
作为一名长期使用Node.js的开发者,我亲历了从CommonJS到ESM的完整迁移过程。最初我以为这不过是个简单的配置变更,但实际遇到的坑远比想象的多。最典型的就是那些看似能跑但实际上隐藏着兼容性问题的代码 - 它们往往在特定环境下才会暴露问题。
ESM和CommonJS最根本的区别在于它们的加载机制。ESM是静态的,这意味着模块的导入导出关系在代码执行前就已经确定;而CommonJS是动态的,模块可以在运行时根据需要加载。
这种差异带来的直接影响就是:
javascript复制// CommonJS方式 - 可以省略扩展名
const utils = require('./utils');
// ESM方式 - 必须包含扩展名
import utils from './utils.js'; // 注意.js扩展名
路径解析是迁移中最容易出问题的地方之一。在CommonJS中,我们习惯使用__dirname来构建相对路径:
javascript复制// CommonJS路径处理方式
const path = require('path');
const filePath = path.join(__dirname, 'config.json');
而在ESM中,我们需要使用新的URL模块来处理路径:
javascript复制// ESM路径处理方式
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const filePath = path.join(__dirname, 'config.json');
这是新手最容易踩的坑。Node.js默认将.js文件视为CommonJS模块,要使用ESM语法,你需要:
.mjs扩展名"type": "module"重要提示:如果项目中同时存在CommonJS和ESM模块,建议统一使用package.json的type字段来声明模块类型,而不是混合使用.js和.mjs扩展名。
很多流行的npm包还没有提供ESM版本,这会导致直接import时出现问题。解决方案有几种:
javascript复制// 动态import方式加载CommonJS模块
const { default: lodash } = await import('lodash');
测试框架、打包工具等配套工具对ESM的支持程度不一:
javascript复制// jest.config.js示例配置
module.exports = {
testEnvironment: 'node',
transform: {
'^.+\\.js$': 'babel-jest'
},
extensionsToTreatAsEsm: ['.js']
};
对于大型项目,我推荐采用渐进式迁移策略:
"type": "commonjs"javascript复制// 在CommonJS中加载ESM模块的例子
async function loadESM() {
const esmModule = await import('./esm-module.mjs');
// 使用esmModule...
}
如果你使用Webpack或Rollup等构建工具,需要相应调整配置:
javascript复制// webpack.config.js
module.exports = {
experiments: {
outputModule: true
},
output: {
module: true
}
};
对于Babel用户,确保安装了正确的preset:
bash复制npm install @babel/preset-env @babel/plugin-transform-modules-commonjs --save-dev
javascript复制// .babelrc
{
"presets": [
["@babel/preset-env", {
"modules": "auto"
}]
]
}
ESM的静态特性带来了更好的优化空间:
javascript复制// 好的导出方式 - 明确导出接口
export function util1() {}
export function util2() {}
// 不好的导出方式 - 导出整个对象
export default {
util1,
util2
}
ESM不仅是模块系统的改变,它代表着JavaScript生态的演进方向。随着时间推移:
对于长期维护的项目,我建议:
迁移到ESM不是终点,而是拥抱现代JavaScript生态的开始。虽然过程中会遇到各种挑战,但最终会带来更清晰、更高效的代码结构。记住,每个问题的解决都是技术能力的提升,每次成功的迁移都是项目质量的飞跃。