从CommonJS到ES Modules:现代JavaScript模块化指南 1. 为什么我们需要告别require时代十年前我刚接触Node.js时require()是加载模块的唯一方式。那时候所有教程都这样写const fs require(fs) const _ require(lodash)但随着前端生态的演进这种CommonJS模块系统逐渐暴露出问题同步加载机制导致Tree Shaking困难运行时解析使得静态分析几乎不可能作用域污染难以避免类型支持薄弱让TS开发者头疼而ES Modules(ESM)作为JavaScript官方标准完美解决了这些问题。Node.js从12版开始实验性支持ESM到20版已经成熟。TypeScript 5.9更是全面优化了对ESM的支持。关键转折2023年Node.js官方宣布新项目应优先使用ESMCommonJS将进入维护模式2. 现代JS模块化核心概念解析2.1 ESM基础语法对比传统CommonJS写法// 导出 module.exports { foo: 1 } exports.bar 2 // 导入 const mod require(./mod)现代ESM写法// 导出 export const foo 1 export default { bar: 2 } // 导入 import mod, { foo } from ./mod.js关键差异文件扩展名必须显式声明(.js/.ts)import/export必须位于模块顶层默认严格模式支持Top-level await2.2 类型系统增强TypeScript 5.9对ESM的类型推导做了重大改进// 现在可以正确推断动态导入类型 const { readFile } await import(fs/promises) // 支持import type与值导入分离 import type { Router } from express import { createRouter } from express3. 项目迁移实战指南3.1 基础配置改造package.json新增{ type: module, exports: { .: { import: ./dist/index.js, types: ./dist/index.d.ts } } }tsconfig.json关键配置{ module: NodeNext, moduleResolution: NodeNext, outDir: ./dist, rootDir: ./src }所有文件扩展名改为.ts/.js3.2 常见模块改造模式情况1默认导出转换// 改造前 module.exports function() {} // 改造后 export default function() {}情况2解构导出转换// 改造前 exports.a 1 exports.b 2 // 改造后 export const a 1 export const b 2情况3动态加载改造// 改造前 const pkg require(../${path}) // 改造后 const pkg await import(../${path}.js)4. 深度互操作解决方案4.1 混合模块兼容方案当必须使用CommonJS模块时import { createRequire } from module const require createRequire(import.meta.url) // 传统模块使用 const legacyMod require(old-module)4.2 类型声明适配技巧对于没有类型定义的ESM模块// global.d.ts declare module esm-only-module { export function hello(): string }4.3 性能优化方案预加载优化// 启动时预加载 const dep await import(heavy-dependency) // 使用时直接引用 export function useDep() { return dep }编译缓存配置# tsconfig.json { tsBuildInfoFile: ./.cache/tsbuildinfo }5. 企业级项目迁移策略5.1 渐进式迁移路线新模块全部使用ESM旧模块按功能逐步改造设置过渡期兼容层5.2 自动化迁移工具链推荐工具组合ts-migrate基础语法转换cjs-to-esm模块规范转换jest测试保障5.3 监控与回滚方案性能基准测试node --test test/benchmark.js内存监控配置import { monitor } from node:diagnostics_channel monitor(memory, (data) { console.log(Memory usage:, data) })6. 疑难问题解决方案6.1 __dirname替代方案ESM中需要使用import { fileURLToPath } from url import { dirname } from path const __filename fileURLToPath(import.meta.url) const __dirname dirname(__filename)6.2 循环引用处理CommonJS// a.js exports.loaded false const b require(./b) exports.loaded trueESM解决方案// a.ts export let loaded false import { b } from ./b.js loaded true6.3 第三方模块兼容问题模块处理模式import { createRequire } from module const require createRequire(import.meta.url) const problematic require(problematic-module) export default problematic7. 前沿技术整合7.1 与WebAssembly互操作import wasm from ./module.wasm?init const { instance } await wasm() console.log(instance.exports.add(1, 2))7.2 模块联邦实践微前端场景应用// host-app/src/bootstrap.ts import(remote-app/component) .then(({ default: Component }) { mount(Component) })7.3 服务端组件支持Next.js风格实现// app/server-component.tsx import db from server-only-db export async function ServerComponent() { const data await db.query() return div{data}/div }迁移过程中最大的教训是不要试图一次性改造整个代码库。我在一个3万行代码的项目中采用新代码新规范老代码逐步迁移的策略配合自动化测试保障最终用6周时间完成了平滑过渡。特别要注意第三方库的兼容性问题建议在项目初期就建立模块兼容层。