在JavaScript发展早期,所有代码都堆在全局作用域,命名冲突、依赖混乱、代码复用困难成为大型项目的"噩梦",模块化成为刚需。社区和标准组织先后推出了CommonJS、AMD、ESM、UMD四种主流规范,分别针对不同场景设计,本文就来拆解它们的核心逻辑、差异与适用场景。
一、模块化演进脉络
JavaScript模块化的发展是按需诞生的过程:
- 2009年:Node.js诞生,采纳CommonJS规范,首次在服务端实现了标准化的模块加载;
- 2011年:为解决浏览器端同步加载阻塞页面渲染的问题,社区提出AMD规范,由RequireJS推广落地;
- 2015年:ES6正式发布ESM规范,从语言层面统一了模块化的标准,成为现代前端的绝对主流;
- 兼容期:为解决一套代码在浏览器、Node、AMD加载器中通用的问题,UMD方案应运而生,成为跨平台库的标准打包格式。
二、四大规范核心拆解
CommonJS:服务端模块化的基石
CommonJS是Node.js的默认模块规范,核心设计围绕服务端本地文件读取速度快、无需考虑网络阻塞的特性打造,具备三个核心特征:
- 同步加载 :
require()会立即读取、执行对应模块文件,执行到require语句时才会加载依赖; - 值拷贝:导出的是值的副本,模块内部后续修改不会影响已导入的值(对象类型导出的是引用);
- 运行时动态解析 :
require()可以出现在代码任意位置,支持条件加载。
javascript
// math.js
function add(a, b) { return a + b; }
module.exports = { add };
// main.js
const math = require('./math.js'); // 同步加载
console.log(math.add(2, 3)); // 输出5
// 循环依赖示例:可能拿到未完成的模块导出
// a.js
exports.done = false;
const b = require('./b.js');
console.log('在a中,b.done =', b.done);
exports.done = true;
// b.js
exports.done = false;
const a = require('./a.js');
console.log('在b中,a.done =', a.done); // 输出false,因为a.js尚未执行完成
exports.done = true;
CommonJS的局限在于无法在编译时分析依赖关系,不支持Tree Shaking,无法用于浏览器端原生加载(需要打包工具转换)。
AMD:浏览器异步加载的早期方案
AMD(Asynchronous Module Definition)是专为浏览器端设计的异步模块规范,核心设计是依赖前置,在模块定义时提前声明所有依赖,异步加载完成后才执行模块逻辑,避免阻塞页面渲染。
javascript
// 定义模块,依赖jquery和math,依赖加载完成后才执行工厂函数
define('myModule', ['jquery', './math'], function($, math) {
$('body').css('background', '#eee');
return {
calculate: function(x) { return math.square(x); }
};
});
// 加载使用模块
require(['myModule'], function(myModule) {
console.log(myModule.calculate(5));
});
AMD的典型实现是RequireJS,在ESM普及前是浏览器端的主流方案,被Dojo、jQuery等项目采用,但语法繁琐、配置复杂,2020年RequireJS停止维护后,AMD已经基本退出历史舞台。
ESM:语言层面的统一标准
ESM(ECMAScript Modules)是ES6引入的官方模块规范,从语言层面统一了模块化的标准,核心优势是静态化设计,具备三个关键特性:
- 编译时静态分析 :所有
import/export语句必须位于模块顶层,依赖关系在编译阶段就能确定,与运行状态无关; - 值的实时引用:导出的是值的只读引用,模块内部修改会实时同步到所有导入方;
- 支持Tree Shaking:打包工具可以通过静态分析移除未使用的导出,减小产物体积。
javascript
// math.mjs
export const PI = 3.14;
export function circleArea(r) { return PI * r * r; }
export function unused() { console.log('unused'); } // 未使用的导出会被Tree Shaking移除
// app.mjs
import { circleArea } from './math.mjs';
console.log(circleArea(5)); // 输出78.5
// 动态导入,支持代码分割
const heavyModule = await import('./heavy.mjs');
ESM与CommonJS的核心差异:
| 特性 | CommonJS | ESM |
|---|---|---|
| 加载方式 | 同步运行时加载 | 编译时静态解析+异步预加载 |
| 导出值类型 | 值的拷贝 | 值的实时只读引用 |
| 严格模式 | 默认非严格模式 | 自动启用严格模式 |
| 顶层this指向 | 指向module.exports | undefined |
| Tree Shaking | 不支持 | 完美支持 |
ESM是浏览器原生支持的标准(通过<script type="module">加载),也是Node.js、Deno等运行时的默认规范,是现代前端项目的绝对主流。
UMD:跨环境兼容的"胶水"方案
UMD(Universal Module Definition)并非语言原生规范,而是一套基于IIFE的封装策略,核心逻辑是通过环境判断,让同一套代码可以兼容AMD、CommonJS、全局变量三种模式,实现"一次编写,多端运行"。
javascript
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD环境:使用define定义模块
define(['dep'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS环境:使用module.exports导出
module.exports = factory(require('dep'));
} else {
// 浏览器全局环境:挂载到全局对象
root.myModule = factory(root.dep);
}
}(this, function(dep) {
// 模块核心逻辑
return {
doSomething: function() { return dep.action(); }
};
}));
UMD是jQuery、Lodash、Moment.js等经典跨平台库的标准打包格式,在微前端、低代码平台、遗留系统集成等场景中,仍然是连接新旧技术栈的常用方案。
三、四大规范核心对比
| 维度 | CommonJS | AMD | ESM | UMD |
|---|---|---|---|---|
| 加载方式 | 同步加载 | 异步加载 | 编译时静态解析 | 根据环境自动适配 |
| 加载时机 | 运行时执行到require时 | 依赖加载完成后执行 | 预处理阶段解析所有import | 运行时判断环境后加载 |
| 静态分析 | 不支持 | 不支持 | 完美支持 | 不支持 |
| Tree Shaking | 不支持 | 不支持 | 支持 | 不支持 |
| 适用环境 | Node.js服务端 | 浏览器端(旧) | 全平台 | 全平台 |
| 当前现状 | Node.js主流,逐步迁移ESM | 基本淘汰 | 绝对主流 | 存量库兼容使用 |
四、选型建议
- 新项目/现代前端应用:全面采用ESM,浏览器和Node.js原生支持,配合Vite、Webpack等工具可以实现Tree Shaking、代码分割等优化;
- Node.js服务端项目 :存量项目继续使用CommonJS,新项目推荐使用ESM(通过
.mjs后缀或package.json配置"type": "module"启用); - 需要跨浏览器、Node、AMD加载器使用的第三方库:使用UMD格式打包,保证最大兼容性;
- 维护2015年前的老浏览器项目:可能需要继续使用AMD+RequireJS的方案。
ESM的出现已经终结了模块化规范的纷争,其他规范要么用于存量项目维护,要么用于特定兼容场景,理解它们的核心差异,能帮助我们更好地处理跨环境兼容、老项目迁移等问题。