从CommonJS到ES Modules:现代JavaScript模块化指南

发布时间:2026/11/4 8:05:06
从CommonJS到ES Modules:现代JavaScript模块化指南

1. 为什么我们需要告别require时代

十年前我刚接触Node.js时,require()是加载模块的唯一方式。那时候所有教程都这样写:

const fs = require('fs') const _ = require('lodash')

但随着前端生态的演进,这种CommonJS模块系统逐渐暴露出问题:

  1. 同步加载机制导致Tree Shaking困难
  2. 运行时解析使得静态分析几乎不可能
  3. 作用域污染难以避免
  4. 类型支持薄弱让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 基础配置改造

  1. package.json新增:
{ "type": "module", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } } }
  1. tsconfig.json关键配置:
{ "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src" }
  1. 所有文件扩展名改为.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 性能优化方案

  1. 预加载优化:
// 启动时预加载 const dep = await import('heavy-dependency') // 使用时直接引用 export function useDep() { return dep }
  1. 编译缓存配置:
# tsconfig.json { "tsBuildInfoFile": "./.cache/tsbuildinfo" }

5. 企业级项目迁移策略

5.1 渐进式迁移路线

  1. 新模块全部使用ESM
  2. 旧模块按功能逐步改造
  3. 设置过渡期兼容层

5.2 自动化迁移工具链

推荐工具组合:

  • ts-migrate基础语法转换
  • cjs-to-esm模块规范转换
  • jest测试保障

5.3 监控与回滚方案

  1. 性能基准测试:
node --test test/benchmark.js
  1. 内存监控配置:
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 = true

ESM解决方案:

// a.ts export let loaded = false import { b } from './b.js' loaded = true

6.3 第三方模块兼容

问题模块处理模式:

import { createRequire } from 'module' const require = createRequire(import.meta.url) const problematic = require('problematic-module') export default problematic

7. 前沿技术整合

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周时间完成了平滑过渡。特别要注意第三方库的兼容性问题,建议在项目初期就建立模块兼容层。