一文搞懂AMD、CMD、UMD、ESM和CommonJS

685 阅读8分钟

在前端开发的成长路上,模块化绝对是绕不开的关键革新。早年间,JavaScript 只用来做些简单的表单验证、按钮交互,代码量少得可怜,全堆在一个文件里也没啥问题。可随着技术迭代,前端能干的活儿越来越多 —— 复杂数据处理、页面渲染、单页应用(SPA)开发,代码量像坐了火箭一样暴涨。

这时候,把所有代码揉在一起的弊端就暴露无遗了:全局变量满天飞,一不小心就重名冲突,好好的程序突然就崩了;模块间的依赖关系乱得像一团麻,想加个功能、改个 bug,得在成千上百行代码里翻来翻去,维护成本高得吓人。

模块化的出现,就像给混乱的代码库来了场 “大扫除”。它把庞大的代码拆成一个个功能单一、独立封装的模块,每个模块都有自己的专属作用域,对外只暴露需要的接口,其他模块按需调用就行。这样一来,代码的可读性、可维护性和复用性都提了好几个档次。而前端领域里的 AMD、CMD、UMD、ESM 和 CommonJS 这五种模块化规范,就像是五种不同的 “整理方案”,各自有自己的适用场景和特点。下面咱们就一个个掰开揉碎了说。

一、CommonJS 规范:Node.js 的 “原生搭档”

1.1 核心概念

CommonJS 打从一开始就是为服务器端 JavaScript 设计的,尤其是 Node.js 环境。在这个规范里,一个文件就是一个模块,每个文件都有自己的独立作用域 —— 就像每个房间都有自己的门,房间里的东西(变量、函数)不会随便跑到其他房间去捣乱。

模块之间想互相调用,全靠两个 “关键工具”:module.exportsrequiremodule.exports相当于房间的 “对外窗口”,把模块里的函数、数据通过这个窗口暴露出去;require则是 “开门的钥匙”,其他模块用它就能拿到需要的资源,轻松实现复用。

1.2 关键特点

  • 同步加载:执行到require语句时,程序会停下来等模块加载完、执行完,才继续往下走。这在服务器端完全没问题 —— 模块文件都存在本地硬盘,读取速度极快,这点等待时间几乎可以忽略不计。 比如有个math.js模块:

    • function add(a, b) { return a + b; }
      function subtract(a, b) { return a - b; }
      module.exports = { add, subtract };
      
    •   在app.js里调用:
    • const math = require('./math');
      console.log(math.add(2, 3)); // 输出 5
      
  • 模块作用域隔离:每个模块都是 “独立王国”,内部变量不会和其他模块冲突。哪怕两个模块里都有name变量,也互不影响,只能通过模块暴露的接口访问。

  • 加载缓存:模块第一次被加载时,运行结果会被缓存起来。之后再用require调用,直接返回缓存结果,不用重复执行代码,省了不少资源。

  • 值拷贝导出module.exports导出的是值的拷贝,模块内部后续修改这个值,不会影响外部已经获取到的拷贝版本。比如修改配置文件的端口号,重新导入后还是原来的值。

二、AMD 规范:浏览器的 “异步救星”

2.1 核心定位

AMD 全称 Asynchronous Module Definition(异步模块定义),专门解决浏览器端的模块加载问题。浏览器里加载 JavaScript 文件是从网络获取的,如果像 CommonJS 那样同步加载,文件多、体积大的时候,浏览器会被卡住,页面 “假死”,用户体验太差。

AMD 的核心实现是 RequireJS 库,它支持异步并行加载模块—— 加载模块的同时,浏览器还能处理其他事情,不用一直等着,大大提升了页面响应速度。

2.2 核心特点:依赖前置

AMD 最鲜明的特点就是 “依赖前置”—— 定义模块的时候,必须把所有依赖的模块都提前声明好,就像出门前先把要用的东西都准备齐。

比如定义一个依赖dataServiceuiUtilsuserModule

define('userModule', ['dataService', 'uiUtils'], function (dataService, uiUtils) {
  function displayUserInfo() {
    const userData = dataService.fetchUserData();
    uiUtils.showMessage(userData.username);
  }
  return { displayUserInfo };
});

使用时先在 HTML 里引入 RequireJS,指定主入口文件:

<script src="require.js" data-main="main.js"></script>

main.js里配置路径并加载模块:

require.config({
  baseUrl: 'js/',
  paths: {
    'dataService': 'services/dataService',
    'uiUtils': 'utils/uiUtils',
    'userModule': 'modules/userModule'
  }
});
require(['userModule'], function (userModule) {
  userModule.displayUserInfo();
});

所有依赖加载完,回调函数才会执行,确保模块运行时资源都已就位。

三、CMD 规范:前端的 “灵活派”

3.1 核心定位

CMD 全称 Common Module Definition(通用模块定义),是国内开发者主导的规范,依托 SeaJS 实现,同样面向浏览器端。它和 AMD 的目标一致,但风格更灵活,主打 “按需加载”。

3.2 核心特点:就近依赖

CMD 最大的亮点是 “就近依赖”—— 不用在模块开头就声明所有依赖,而是用到哪个模块,再在哪个位置用require加载,就像走到半路需要什么再临时取一样。

比如这样定义模块:

define(function(require, exports, module) {
  // 用到时再加载依赖
  const dataService = require('./services/dataService');
  const uiUtils = require('./utils/uiUtils');
  
  function displayUserInfo() {
    const userData = dataService.fetchUserData();
    uiUtils.showMessage(userData.username);
  }
  
  exports.displayUserInfo = displayUserInfo;
});

这种方式的好处很明显:不用提前加载用不上的模块,减少初始加载资源;代码逻辑和依赖引入就近,读起来更顺,后续修改时改动范围也小,维护起来更轻松。

四、UMD 规范:跨平台的 “万能适配者”

4.1 核心定位

UMD 全称 Universal Module Definition(通用模块定义),是个 “全能选手”。之前 CommonJS 主打服务器端,AMD、CMD 主打浏览器端,跨平台开发时,代码很难无缝复用。UMD 就是为了解决这个问题,一套代码能适配多种环境。

4.2 实现原理

UMD 的核心是 “环境判断”—— 加载模块时,先判断当前运行环境,再选择对应的模块化规范:

  • 如果是 Node.js 环境,就用 CommonJS 的module.exportsrequire
  • 如果是支持 AMD 的浏览器环境,就用define异步加载;
  • 如果是普通浏览器,就把模块挂载到window全局对象上。

比如 jQuery 早期版本就用了 UMD 规范,核心逻辑如下:

(function (global, factory) {
  if (typeof exports === 'object' && typeof module !== undefined) {
    // Node.js环境(CommonJS)
    module.exports = factory(require('jquery'));
  } else if (typeof define === 'function' && define.amd) {
    // AMD环境(如RequireJS)
    define('toggler', ['jquery'], factory);
  } else {
    // 普通浏览器,挂载到window
    global.toggler = factory(global.jQuery);
  }
})(this, function ($) {
  function init() { /* 模块核心逻辑 */ }
  return { init };
});

这样一来,同一套代码既能在 Node.js 里运行,又能在浏览器里使用,完美实现 “一次编写,到处运行”。

五、ESM 规范:JavaScript 的 “官方标准”

5.1 核心定位

ESM 全称 ECMAScript Modules,是 JavaScript 官方推出的模块化标准,目标是统一浏览器和服务器端的模块化方案。现在主流浏览器(Chrome、Firefox、Safari)都支持,Node.js 从 13.2 版本开始也支持(需在package.json中指定"type": "module"),是未来的主流趋势。

使用起来很简单,浏览器端只需给script标签加type="module"

<script type="module" src="main.js"></script>

模块文件里用import导入、export导出:

// math.js
export function add(a, b) { return a + b; }

// main.js
import { add } from './math.js';
console.log(add(2, 3)); // 输出 5

5.2 核心特点:静态化

ESM 最核心的特点是 “静态化”—— 模块的依赖关系和导入导出的变量,在编译阶段就确定了,而不是像 CommonJS 那样要到运行时才知道。

这种静态化带来了很多好处:

  • 导入的变量是只读的,不能随意修改,保证了模块的稳定性;
  • 编译时就能优化加载,比如并行加载依赖模块;
  • 静态分析工具(如 TypeScript)能精准做类型检查、自动补全,提前发现代码错误,提升开发效率。

六、五大规范对比总结

6.1 核心差异表

规范加载方式依赖处理适用场景核心语法示例
CommonJS同步加载运行时确定,require同步获取Node.js 服务器端开发const mod = require('./mod'); module.exports = {}
AMD异步加载依赖前置,定义时声明依赖浏览器端开发,需兼容复杂依赖场景define('mod', ['dep'], (dep) => {}); require(['mod'], (mod) => {})
CMD异步加载就近依赖,用到时require加载浏览器端开发,追求代码灵活简洁define((require) => { const mod = require('./mod'); })
UMD环境判断,按需同步 / 异步 / 挂载全局融合 CommonJS、AMD 特点跨平台库 / 框架(浏览器、Node.js 通用)见 jQuery 示例的环境判断逻辑
ESM静态加载(编译阶段确定)import/export静态声明,只读现代浏览器、Node.js 新项目,未来主流import { mod } from './mod.js'; export const x = 1;

6.2 实际应用建议

  • 开发 Node.js 后端应用:优先选CommonJS,和 Node.js 生态完美契合,开发高效,社区资源丰富。

  • 开发前端应用:

    • 兼容老旧浏览器:选AMD(大型应用,依赖可控)或CMD(代码灵活);
    • 现代项目:优先ESM,语法简洁、性能优,支持静态分析,搭配打包工具(如 Vite、Webpack)更高效。
  • 开发跨平台库 / 框架:选UMD,一套代码适配多环境,不用单独做环境兼容。

掌握这五种模块化规范,面对不同的开发场景时,就能精准选对 “工具”,写出结构清晰、易维护的代码,让项目开发少走弯路。