Skip to content

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.exportsexports 对象暴露成员。
  • 导入:使用同步的 require() 函数加载模块。

示例代码:

javascript
// 导出模块(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() 异步加载模块,并在回调函数中使用加载好的模块。

示例代码

javascript
// 定义模块
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)。

示例代码

javascript
(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 关键字。

示例代码

javascript
// 导出模块
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. 规范对比与现代迁移指南

为了更直观地理解,我们将四种规范进行横向对比:

规范主要环境加载方式核心优势现状
CommonJSNode.js同步简单高效,生态极其丰富存量项目主流,正向 ESM 转型
AMD浏览器异步解决浏览器阻塞问题已淡出历史舞台
UMD跨平台兼容一份代码,到处运行仅在需要兼容老旧环境时使用
ES Modules全栈静态语言标准,支持 Tree Shaking现代开发的唯一推荐标准

迁移策略建议

  1. 全面拥抱 ESM:新项目应直接在 package.json 中设置 "type": "module"。
  2. 从 CommonJS 迁移到 ES Modules:可以将 .js 逐步重命名为 .mjs,或利用 esbuild 等工具进行平滑转译。
    • 使用 .mjs 扩展名或在 package.json 中设置 "type": "module"
    • require() 替换为 import
    • module.exports 替换为 exportexport default
  3. 从 AMD 迁移到 ES Modules
    • 使用现代构建工具(如 Webpack、Vite)处理模块加载
    • define() 替换为 ES Modules 语法
    • 利用构建工具的代码分割功能实现按需加载
    • 移除对 RequireJS 的依赖
  4. 构建工具的妙用:利用 ViteTurbopack,我们完全可以用 ESM 编写代码,由工具链自动处理老旧环境的兼容。

6. 结语:在标准之上构建未来

从 CommonJS 的探索到 ESM 的大一统,模块化规范的演进本质上是 JavaScript 从一种“脚本语言”向“工业级编程语言”蜕变的过程

理解这些规范不仅是为了应付面试或处理老旧代码,更是为了让我们在面对复杂的全栈架构时,能对代码的加载、执行和构建有更深层的掌控力。在 AI 辅助编程普及的今天,清晰的模块化思维更是构建可靠 Prompt 和 Agentic 流程的基石,希望这篇梳理能为你夯实全栈基础。