第2章 前端模块化概述:CommonJS、ES Module、AMD
说到前端工程化,模块化是绕不开的一步。很多新人觉得模块化就是"import/export",其实没那么简单。我当年刚接触 Node.js 时,就被 CommonJS 和 ES Module 的差异坑过好几次。这一章,咱们把三种主流模块化方案掰开揉碎聊清楚。
2.1 为什么需要模块化?
说白了,没有模块化的时候,全局变量满天飞。你写个 var utils = {},别人也写个 var utils = {},后加载的就把前面的覆盖了。我在一个老项目中遇到过,就因为两个依赖定义了同名的全局函数,线上页面白屏了整整两小时……从那以后,我对模块化方案特别敏感。
模块化要解决三个核心问题:
- 作用域隔离:变量不互相污染
- 依赖管理:自动加载依赖,保证顺序正确
- 按需加载:只在需要时加载模块,减少首屏体积
2.2 CommonJS
CommonJS 是 Node.js 默认的模块规范。你写个 require('./util'),就是用它。它的特点是同步加载,因为 Node 环境里文件都在本地磁盘,读取很快。但放到浏览器里就不行了——网络请求是异步的,同步加载会卡住页面。
语法很简单:
// util.js
module.exports = {
add: (a, b) => a + b
};
// main.js
const util = require('./util');
console.log(util.add(1, 2)); // 3
关键点:
module.exports导出整体对象exports是module.exports的引用,不能重新赋值- 同步加载,适用于服务器端
- 值拷贝(基本类型)或引用拷贝(对象)
exports = { ... } 并不会生效,因为它打断了引用关系。必须用 module.exports = { ... }。那次排查了半小时才找到原因。
2.3 ES Module
ES Module(ESM)是 ECMAScript 官方标准,浏览器和 Node 现在都支持。它最核心的特点是静态分析——在编译阶段就知道你导出了什么、导入了什么,从而可以做 tree shaking(摇树优化)。
// util.js
export function add(a, b) {
return a + b;
}
// main.js
import { add } from './util.js';
console.log(add(1, 2)); // 3
语法上的区别:
export default导出默认值,导入时可以用任意变量名export { ... }导出具名列表import声明提升,必须在文件顶部- 异步加载,浏览器里通过
<script type="module">使用
2.4 AMD(异步模块定义)
AMD 是专门为浏览器设计的规范,典型实现是 RequireJS。它的核心是异步加载,通过回调函数拿到模块。你看这写法:
// 定义模块 util
define('util', ['dependency'], function(dep) {
return {
add: function(a, b) { return a + b; }
};
});
// 使用模块
require(['util'], function(util) {
console.log(util.add(1, 2));
});
为什么现在用得少了?因为 ES Module 已经原生支持异步加载,而且语法更简洁。不过在一些遗留系统里你还会遇到它。记得我刚工作时,接手一个基于 RequireJS 的老项目,配置了三十多个模块路径,光调试依赖顺序就花了一周。
2.5 三种方案对比
| 特性 | CommonJS | AMD | ES Module |
|---|---|---|---|
| 加载方式 | 同步 | 异步 | 异步(编译时静态) |
| 环境 | Node.js | 浏览器(需加载器) | 浏览器 & Node.js |
| 语法 | require / module.exports | define / require | import / export |
| 树摇优化 | 不支持 | 不支持 | 原生支持 |
| 循环依赖 | 有限支持(返回部分 exports) | 支持死循环检测 | 有限支持(绑定是只读引用) |
你想想看,如果浏览器环境能直接跑 CommonJS,那打包工具(比如 webpack 的 UMD 包装)就不需要存在了。但现实是,我们必须做兼容。
2.6 实际项目中的选择策略
我个人现在的建议是:
- 新项目用 ES Module,配合打包工具(webpack/vite)输出兼容格式。
- Node.js 库 可以同时提供 CommonJS 和 ESM 两种入口(通过 package.json 的 exports 字段)。
- 老项目维护 如果还在用 AMD,尽早迁移到统一方案,否则调试效率太低。
require() 导入 ESM 模块直接报错的情况。因为 Node 16+ 对 ESM 采用隔离加载,不支持 require 引用。解决办法是用 import() 动态导入,或者在 .cjs 文件里使用 createRequire。