核心是重建模块解析逻辑和运行约束:先在package.json中设"type": "module",再将require/module.exports改为import/export、__dirname改为import.meta.url、动态require改为await import,并确保所有导入路径含.js扩展名。

直接把 CommonJS 项目改成纯 ES Modules,核心不是“改语法”,而是重建模块解析逻辑和运行约束。Node.js 从 v14.13 起已稳定支持 ESM,v16+ 是推荐基线,无需 polyfill 或构建工具即可原生运行。
第一步:明确项目入口与模块类型边界
在 package.json 中添加 "type": "module"。这会让所有 .js 文件默认按 ESM 解析——这是最干净的起点。注意:加了这一行后,require()、module.exports、__dirname、__filename 就全部失效,不能再混用。
- 如果项目里有大量 __dirname 用途(比如读取本地 JSON 配置),要换成 import.meta.url + new URL('./config.json', import.meta.url)
- 动态 require('xxx') 场景,必须改用 await import('xxx')(顶层 await 在 Node.js v14.8+ 支持)
- 原来用 fs.readFileSync(__dirname + '/data.txt') 的地方,需同步改为 fs.readFileSync(new URL('./data.txt', import.meta.url), 'utf8')
第二步:逐文件迁移导出/导入语法
CommonJS 的 module.exports 和 require 不是简单替换成 export 和 import 就能跑通的,关键在语义对齐:
- 原来
module.exports = { foo, bar }→ 改为export { foo, bar } - 原来
module.exports = function() {}(默认导出单个值)→ 改为export default function() {} - 原来
const utils = require('./utils')→ 改为import utils from './utils.js'(注意:ESM 要求路径带扩展名 .js) - 原来
const { a, b } = require('./lib')→ 若 lib.js 用 export default 导出对象,需写成import lib from './lib.js'; const { a, b } = lib
第三步:处理第三方依赖兼容性
不是所有 npm 包都提供 ESM 入口。遇到报错如 “The requested module does not provide an export named 'default'”,说明该包仍是 CommonJS 格式:
- 优先查包文档,看是否提供 ESM 版本(如 crypto-js@4.x 默认不支持 ESM,需用 import('crypto-js').then(...) 动态加载)
- 若包只支持 CommonJS,且你又必须静态 import,可用 Node.js 内置的 --experimental-loader 配合 esm-loader 等过渡方案,但不推荐长期使用
- 更稳妥的做法:保留部分 .cjs 文件(如专门封装 legacy 包调用),并在 package.json 中用 exports 字段精确控制入口
第四步:验证与清理残留
迁移完成后,检查三类典型问题:
- 启动时报 SyntaxError: Cannot use import statement outside a module → 检查是否漏删了某个 .js 文件里的 require,或 package.json 缺少 "type": "module"
- 运行时报 ReferenceError: __dirname is not defined → 所有 __dirname 都需替换为 import.meta.url 方案
- 导入后得到 undefined → 多半是 export/default 不匹配,用 console.log(Object.keys(module)) 查看实际导出结构











