JavaScript 诞生之初,设计初衷仅仅是为了在浏览器端做一些简单的表单验证和动态效果交互。那时的代码往往全部堆砌在全局作用域中,变量名冲突频发,代码耦合度极高,维护起来如同噩梦。随着 Web 应用日益复杂,前端工程化成为必然趋势,而“模块化”正是解决代码组织、依赖管理和作用域隔离的核心钥匙。从早期的手工拼接到如今标准化的 ES Module,JS 模块化方案经历了一场漫长的进化史。理解 CommonJS、AMD、CMD 和 ES Module 这四种主流方案的差异,不仅是掌握历史脉络,更是为了在现代开发中做出正确的技术选型和架构决策。

一、服务端霸权:CommonJS 的同步加载机制
CommonJS 规范诞生于 Node.js 崛起之时,其核心目标是为 JavaScript 在服务端运行提供一套标准的模块系统。在服务器环境中,模块文件通常存储在本地磁盘,读取速度极快且网络延迟几乎为零,因此 CommonJS 采用了同步加载的策略。
在 CommonJS 规范中,每个文件都是一个独立的模块,拥有自己的作用域,内部定义的变量、函数默认是私有的,不会污染全局环境。模块通过 module.exports 或 exports 对象向外暴露接口,而其他模块则使用 require() 函数进行引入。require() 是一个同步操作,当代码执行到这一行时,程序会立即停止,直到被引用的模块完全加载并执行完毕,才会继续向下执行。这种阻塞式的加载方式在服务端非常高效,因为它保证了依赖的顺序执行和确定性。
然而,将 CommonJS 直接搬到浏览器端却是一场灾难。浏览器环境依赖网络请求获取资源,如果采用同步加载,一旦某个模块体积较大或网络波动,整个页面的渲染线程就会被阻塞,导致用户界面“假死”,体验极差。尽管后来出现了 Browserify、Webpack 等打包工具,通过在构建阶段将所有模块合并成一个文件来模拟 CommonJS 在浏览器的运行,但这本质上是一种“妥协”,而非原生支持。CommonJS 的动态加载特性(require 可以写在条件语句或函数内部)虽然灵活,但也使得静态分析变得困难,不利于编译优化。
二、浏览器先行者:AMD 的异步依赖前置
为了解决浏览器端的异步加载问题,RequireJS 的作者提出了 AMD(Asynchronous Module Definition)规范。AMD 的核心思想是异步加载和依赖前置。它允许模块在加载时不阻塞页面渲染,只有当所有依赖都加载完成后,才会执行回调函数。
AMD 使用 define() 函数来定义模块,其典型语法结构要求开发者在数组中显式声明当前模块所依赖的所有其他模块,并在随后的回调函数中接收这些依赖的引用。这种“依赖前置”的写法意味着,模块在执行之前,必须明确知道它需要谁。一旦依赖准备就绪,回调函数立即执行。这种方式完美契合了浏览器的网络特性,实现了非阻塞的资源加载。
此外,AMD 还提供了 require() 函数用于按需加载模块,支持在运行时动态获取资源。虽然 AMD 解决了性能问题,但其语法相对繁琐,尤其是当依赖项较多时,代码的可读性会大幅下降。嵌套的回调结构也容易导致“回调地狱”,增加维护成本。尽管如此,在 ES6 模块普及之前,AMD 曾是大型前端项目(尤其是早期单页应用)的首选方案,RequireJS 也因此风靡一时。
三、本土化创新:CMD 的就近依赖与延迟执行
CMD(Common Module Definition)是由国内阿里云团队提出的模块化规范,代表实现是 SeaJS。CMD 在设计上借鉴了 CommonJS 的书写风格,同时保留了浏览器端的异步加载特性。它与 AMD 最大的区别在于依赖的加载时机和书写方式。
在 CMD 中,依赖的声明不再是前置的,而是“就近依赖”。开发者可以在代码的任何位置使用 require() 引入依赖,且这个 require() 是同步的语法(但在底层实现上是异步加载的)。关键在于,CMD 遵循“依赖就近,延迟执行”的原则。当一个模块被加载时,其内部的 require() 语句并不会立即执行对应的依赖加载,只有当代码逻辑真正运行到那一行时,才会去加载并执行该依赖。
这种机制带来了极大的灵活性。首先,代码书写更加自然,符合程序员的思维习惯,不需要在一开始就罗列所有依赖。其次,它支持按需加载,只有真正用到的模块才会被下载和执行,有效减少了首屏资源的浪费。相比之下,AMD 会在定义模块时就立即下载所有依赖,无论后续逻辑是否真的用到了它们。CMD 的这种“懒加载”特性在移动端网络环境下尤为宝贵。不过,由于过度依赖运行时分析,CMD 在构建优化和静态分析方面略逊于后来的 ES Module。
四、终极标准:ES Module 的静态化与树摇优化
随着 ECMAScript 2015(ES6)的发布,JavaScript 终于拥有了原生的模块化标准——ES Module(ESM)。这不仅仅是一个新的语法糖,更是语言层面的根本性变革。ESM 旨在统一此前混乱的模块格局,成为浏览器和 Node.js 通用的官方标准。
ESM 最显著的特征是静态化。它使用 import 和 export 关键字,且这些语句必须位于模块的顶层,不能出现在条件语句、循环或函数内部。这意味着模块的依赖关系在代码编译阶段(静态分析阶段)就能被完全确定。这种静态结构带来了巨大的优势:构建工具(如 Webpack、Rollup、Vite)可以轻松地进行**树摇(Tree Shaking)**优化,自动剔除那些被导入但从未使用的代码,显著减小打包体积。这是动态加载的 CommonJS 难以做到的。
在执行机制上,ESM 采用异步加载,天然适合浏览器环境。同时,它引入了“模块连接”的概念:import 引入的不是值的副本,而是对原始值的实时只读引用(Live Binding)。如果源模块导出的变量发生了变化,导入方会自动感知到这一变化。这与 CommonJS 输出值的拷贝(一旦输出,源模块的变化不影响导入方)有着本质区别。
现代浏览器已原生支持 ESM,只需在 <script> 标签中添加 type="module" 即可直接使用,无需任何打包工具。Node.js 也从较新版本开始支持 ESM(需配置 .mjs 后缀或 package.json 中的 type: "module")。尽管目前仍存在 CommonJS 与 ESM 混用的互操作性挑战,但 ESM 无疑是未来的唯一方向。
五、方案对比与现代化架构选型
回顾这四种方案,我们可以清晰地看到一条从“动态、同步、服务端优先”向“静态、异步、全平台统一”演进的路线。
| 特性 | CommonJS | AMD | CMD | ES Module |
|---|---|---|---|---|
| 运行环境 | 服务端 (Node) | 浏览器 | 浏览器 | 全平台 (原生) |
| 加载方式 | 同步加载 | 异步加载 | 异步加载 (延迟执行) | 异步加载 |
| 依赖声明 | 动态 (require 任意位置) |
前置 (define 数组) |
就近 (require 任意位置) |
静态 (import 顶层) |
| 输出值 | 值的拷贝 | 值的导出 | 值的导出 | 值的实时引用 |
| 优化能力 | 弱 (难静态分析) | 中 | 中 | 强 (支持 Tree Shaking) |
在现代前端工程体系中,ES Module 已成为绝对的主流。无论是使用 Vue、React 还是 Angular,底层均基于 ESM 构建。对于新项目,应无条件首选 ESM 语法。对于遗留的 CommonJS 包,现代打包工具(如 Vite、Rollup)能够自动将其转换并兼容。
值得注意的是,在淘宝客推广或大型电商后台系统中,模块依赖极其复杂。采用 ESM 配合 Vite 等新一代构建工具,不仅能利用静态分析实现极致的代码分割(Code Splitting),还能通过 Tree Shaking 剔除未使用的组件库代码,大幅提升首屏加载速度。而对于一些特殊的动态场景(如根据用户权限动态加载插件),虽然 ESM 支持动态 import() 语法,但在某些极端灵活的运行时需求下,理解 CMD 的“延迟执行”思想依然具有指导意义。
模块化不仅仅是语法的变迁,更是软件工程思想的升华。从各自为战到统一标准,JS 模块化方案的演进见证了前端从“脚本小子”到“工程专家”的蜕变。掌握这些原理,能让我们在面對复杂的依赖图谱时,依然保持清晰的架构视野。