JavaScript 模块化规范全景图:从 CJS 到 ESM
摘要总结: 本文横向对比 JavaScript 四大模块化规范:CommonJS、AMD、UMD、ES Modules。通过代码示例展示各规范的核心语法,并提供从 CommonJS 迁移到 ESM 的具体步骤,以及从 AMD 向 ESM 迁移的构建工具策略。最后以对比表格收尾,阐明 ESM 作为现代开发唯一推荐标准的地位。
在 JavaScript 进化史上,“模块化”始终是一个核心命题。从早期 script 标签满天飞的“混沌时代”,到各路规范群雄逐鹿的“战国时代”,再到如今 ES Modules 定于一尊,开发者们在解决依赖管理、作用域污染和异步加载等问题上付出了巨大的努力。
本文将带你梳理 JavaScript 历史上最重要的几种模块化规范,便于构建起全栈开发的底层知识图谱。
1. CommonJS 规范:Node.js 生态的基石
CommonJS 是伴随 Node.js 诞生的规范,由 Kevin Dangoor 提出,初衷是让 JavaScript 脱离浏览器也能构建复杂的系统应用。它是 Node.js 成功的关键因素之一,在它出现之前,JavaScript 缺乏在服务器端运行所需的组织结构。它规定每个文件都是一个独立的模块,拥有私有的作用域,不会污染全局空间。
核心逻辑:
- 导出:通过
module.exports或exports对象暴露成员。 - 导入:使用同步的
require()函数加载模块。
示例代码:
// 导出模块(module.exports)
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
module.exports = {
add,
subtract
};
// 或使用 exports 快捷方式
exports.multiply = (a, b) => a * b;
exports.divide = (a, b) => a / b;
// 导入模块
const math = require('./math');
console.log(math.add(1, 2)); // 输出:3
console.log(math.subtract(3, 1)); // 输出:2| 关键特性 | 说明 |
|---|---|
| 同步加载 | 由于 Node.js 运行在服务器端,模块文件存放在磁盘上,读取速度极快,因此同步加载不会造成明显阻塞。 |
| 运行时加载 | 模块在代码执行到 require 时才被解析和加载。 |
| 缓存机制 | 模块在首次加载后会被缓存,后续的 require 直接返回缓存结果,确保了性能。 |
2. AMD 规范:浏览器异步加载方案
当开发者尝试在浏览器端使用 CommonJS 时,由于网络环境的延迟,同步加载会导致页面卡死。于是,AMD (Asynchronous Module Definition) 应运而生, 由 RequireJS 团队提出,旨在解决浏览器端模块加载的问题,特别是针对大型 Web 应用的性能优化。AMD(Asynchronous Module Definition)是专门为浏览器环境设计的模块化规范,支持异步加载模块,避免阻塞浏览器主线程。
核心逻辑:
- 定义:使用
define()函数,显式声明依赖。 - 加载:使用
require()异步加载模块,并在回调函数中使用加载好的模块。
示例代码:
// 定义模块
define(['dependency1', 'dependency2'], function(dep1, dep2) {
const result = dep1 + dep2;
return {
getResult: () => result
};
});
// 加载模块
require(['module1', 'module2'], function(mod1, mod2) {
// 模块加载完成后执行
console.log(mod1.getResult());
console.log(mod2.getResult());
});| 关键特性 | 说明 |
|---|---|
| 异步加载 | 通过回调机制解决浏览器端网络延迟问题。 |
| 依赖前置 | 在模块运行前,必须先下载并执行所有的依赖模块。 |
| 代表工具 | RequireJS 是该规范最知名的实现。 |
3. UMD 规范:跨环境兼容方案
UMD(Universal Module Definition)是一种跨环境的模块化规范,同时支持 CommonJS 和 AMD,还能在浏览器全局作用域下使用。
在很长一段时间内,库作者面临一个尴尬的局面:需要同时支持 Node.js 环境和浏览器环境。UMD (Universal Module Definition) 并非一种全新的技术,而是一套精巧的兼容性封装。
实现原理: 它通过一个立即执行函数(IIFE),在运行时侦测当前环境:
- 如果存在 define.amd,则按 AMD 方式注册。
- 如果存在 module.exports,则按 CommonJS 方式导出。
- 如果都不存在,则直接挂载到全局对象(如 window 或 self)。
示例代码:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['exports'], factory); // AMD
} else if (typeof exports === 'object') {
factory(exports); // CommonJS
} else {
factory((root.MyLib = {})); // 浏览器全局
}
}(this, function (exports) {
exports.hello = () => 'Hello from UMD!';
}));在 ES Modules 普及之前,UMD 是编写通用开源库(如 jQuery、Lodash)的标准姿势。虽然它增加了代码体积且逻辑复杂,但其兼容性无与伦比。
4. ES Modules:现代开发的终极标准
2015 年,ECMAScript 6 正式发布,带来了语言层面的模块化方案 —— ES Modules (ESM)。它不仅统一了全栈开发的模块标准,还为现代构建工具(如 Vite、Webpack)提供了深度优化的可能。作为 JavaScript 的官方规范,ESM 终于彻底结束了长达十年的规范之争。
核心逻辑:
- 导出:使用
export(具名导出)或export default(默认导出)。 - 导入:使用
import关键字。
示例代码:
// 导出模块
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export default {
multiply: (a, b) => a * b,
divide: (a, b) => a / b
};
// 导入模块
import mathDefault, { add, subtract } from './math.js';
console.log(add(1, 2)); // 输出:3
console.log(mathDefault.multiply(2, 3)); // 输出:6| 关键特性 | 说明 |
|---|---|
| 静态分析 | 与 CommonJS 的运行时加载不同,ESM 在代码编译阶段就能确定依赖关系。这使得 Tree Shaking(剔除未使用的代码)成为可能。 |
| 异步加载支持 | 天然适配浏览器。 |
| 官方标准 | 现代浏览器及 Node.js(v12+)均已原生支持。 |
5. 规范对比与现代迁移指南
为了更直观地理解,我们将四种规范进行横向对比:
| 规范 | 主要环境 | 加载方式 | 核心优势 | 现状 |
|---|---|---|---|---|
| CommonJS | Node.js | 同步 | 简单高效,生态极其丰富 | 存量项目主流,正向 ESM 转型 |
| AMD | 浏览器 | 异步 | 解决浏览器阻塞问题 | 已淡出历史舞台 |
| UMD | 跨平台 | 兼容 | 一份代码,到处运行 | 仅在需要兼容老旧环境时使用 |
| ES Modules | 全栈 | 静态 | 语言标准,支持 Tree Shaking | 现代开发的唯一推荐标准 |
迁移策略建议:
- 全面拥抱 ESM:新项目应直接在 package.json 中设置 "type": "module"。
- 从 CommonJS 迁移到 ES Modules:可以将 .js 逐步重命名为 .mjs,或利用 esbuild 等工具进行平滑转译。
- 使用
.mjs扩展名或在package.json中设置"type": "module" - 将
require()替换为import - 将
module.exports替换为export或export default
- 使用
- 从 AMD 迁移到 ES Modules:
- 使用现代构建工具(如 Webpack、Vite)处理模块加载
- 将
define()替换为 ES Modules 语法 - 利用构建工具的代码分割功能实现按需加载
- 移除对 RequireJS 的依赖
- 构建工具的妙用:利用
Vite或Turbopack,我们完全可以用 ESM 编写代码,由工具链自动处理老旧环境的兼容。
6. 结语:在标准之上构建未来
从 CommonJS 的探索到 ESM 的大一统,模块化规范的演进本质上是 JavaScript 从一种“脚本语言”向“工业级编程语言”蜕变的过程。
理解这些规范不仅是为了应付面试或处理老旧代码,更是为了让我们在面对复杂的全栈架构时,能对代码的加载、执行和构建有更深层的掌控力。在 AI 辅助编程普及的今天,清晰的模块化思维更是构建可靠 Prompt 和 Agentic 流程的基石,希望这篇梳理能为你夯实全栈基础。