从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官方宣布,新项目应优先使用ESM,CommonJS将进入维护模式
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 await
2.2 类型系统增强
TypeScript 5.9对ESM的类型推导做了重大改进:
// 现在可以正确推断动态导入类型 const { readFile } = await import('fs/promises') // 支持import type与值导入分离 import type { Router } from 'express' import { createRouter } from 'express'3. 项目迁移实战指南
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/.js
3.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周时间完成了平滑过渡。特别要注意第三方库的兼容性问题,建议在项目初期就建立模块兼容层。