第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 的引用,不能重新赋值
  • 同步加载,适用于服务器端
  • 值拷贝(基本类型)或引用拷贝(对象)
我曾经踩过的坑: 在 CommonJS 里,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"> 使用
我个人习惯: 在新项目中全用 ES Module,除非需要兼容老 Node 版本。因为它的静态特性让 IDE 自动补全、类型检查都更好用。

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 的老项目,配置了三十多个模块路径,光调试依赖顺序就花了一周。

一句话总结: CommonJS 是同步的,适合服务端;AMD 是异步的,适合老浏览器;ES Module 是现代标准,统一各方。

2.5 三种方案对比

特性 CommonJS AMD ES Module
加载方式 同步 异步 异步(编译时静态)
环境 Node.js 浏览器(需加载器) 浏览器 & Node.js
语法 require / module.exports define / require import / export
树摇优化 不支持 不支持 原生支持
循环依赖 有限支持(返回部分 exports) 支持死循环检测 有限支持(绑定是只读引用)

你想想看,如果浏览器环境能直接跑 CommonJS,那打包工具(比如 webpack 的 UMD 包装)就不需要存在了。但现实是,我们必须做兼容。

前端模块化方案核心架构 CommonJS AMD ES Module 同步加载 require / module.exports 异步加载 define / require 静态分析 import / export 现代最佳实践:ES Module 为主,打包工具降级兼容 说明:CommonJS 和 AMD 属于 "运行时加载",ES Module 属于 "编译时加载" 箭头表示方案特征与演变方向,虚线表示各方案差异关系

2.6 实际项目中的选择策略

我个人现在的建议是:

  1. 新项目用 ES Module,配合打包工具(webpack/vite)输出兼容格式。
  2. Node.js 库 可以同时提供 CommonJS 和 ESM 两种入口(通过 package.json 的 exports 字段)。
  3. 老项目维护 如果还在用 AMD,尽早迁移到统一方案,否则调试效率太低。
避坑指南: 我曾经在一个混合使用 CommonJS 和 ESM 的项目里,遇到 require() 导入 ESM 模块直接报错的情况。因为 Node 16+ 对 ESM 采用隔离加载,不支持 require 引用。解决办法是用 import() 动态导入,或者在 .cjs 文件里使用 createRequire。