AMD、CMD 和 ES6 Module 的区别与演进

118 阅读5分钟

为什么需要模块化?

在复杂的前端项目中,如果不进行模块化,会面临诸多问题:

  • 命名冲突:多个文件中的变量容易覆盖,污染全局作用域。
  • 依赖混乱:手动管理<script>标签的加载顺序,依赖关系难以维护。
  • 代码复用困难:难以抽离和复用公共代码。

模块化就是为了解决这些问题,它允许我们将代码拆分成独立的、可复用的模块,并显式地声明彼此之间的依赖关系

演进历程:从“原始社会”到“现代文明”

  1. 原始阶段:全局函数模式,<script>标签堆砌。
  2. 命名空间模式:用对象封装模块,减少全局变量,但本质仍是全局对象。
  3. IIFE模式:使用立即执行函数创建私有作用域,是早期简单的模块化方案。
  4. 社区规范时代AMDCMD登场,提供了模块定义和加载的规范及库。
  5. 现代标准时代ES6语言层面引入了模块系统,一统江湖。

AMD (Asynchronous Module Definition)

核心理念“依赖前置,提前执行”

  • 代表库RequireJS
  • 出现背景:主要为浏览器环境设计,强调模块的异步加载

语法示例

// 1. 定义模块 (math.js)
define(['dependencyA', 'dependencyB'], function (depA, depB) {
  // 依赖项在数组中最前面声明
  const add = (a, b) => depA.round(a) + depB.round(b); // 假设depA是lodash

  // 返回模块的对外接口
  return {
    add: add
  };
});

// 2. 配置路径 (通常在主入口文件,如main.js)
require.config({
  paths: {
    'dependencyA': 'lib/lodash.min',
    'dependencyB': 'lib/my-depb.min',
    'math': 'modules/math'
  }
});

// 3. 使用模块
require(['math'], function (math) {
  console.log(math.add(10, 20));
});

特点

  • 优点:依赖关系清晰直观;适合浏览器异步环境;不阻塞页面渲染。
  • 缺点:即使不需要的依赖也会提前加载和执行。

CMD (Common Module Definition)

核心理念“依赖就近,延迟执行”

  • 代表库Sea.js (由阿里团队推出)
  • 出现背景:尝试吸收AMD和CommonJS的优点,更符合开发时的书写习惯。

语法示例

// 定义模块 (math.js)
define(function (require, exports, module) {
  // 在需要的地方,就近引入依赖
  const depA = require('dependencyA'); // 同步引入

  const add = (a, b) => {
    // 在函数内部需要时才引入(理论上,但很少这么用)
    // const depB = require('dependencyB');
    return depA.round(a) + depA.round(b);
  };

  // 方式1:通过 exports 对象添加对外属性
  exports.add = add;

  // 方式2:也可以通过 module.exports 整体赋值
  // module.exports = { add };
});

// 使用模块
seajs.use(['math'], function (math) {
  console.log(math.add(5, 15));
});

特点

  • 优点:依赖延迟加载,更节省资源;写法更接近Node.js的CommonJS风格。
  • 缺点:依赖关系不够直观,需要阅读整个模块代码才能确定。

ES6 Module (现代标准)

核心理念“语言层面支持,编译时确定”

  • 代表JavaScript语言标准 (ES2015)
  • 现状现代前端开发的绝对主流和首选。所有现代浏览器都原生支持,Node.js也正式支持。

核心语法importexport

语法示例

// 1. 定义并导出模块 (math.js)
// 命名导出 (每个模块多个)
export const PI = 3.14159;
export function multiply(x, y) {
  return x * y;
}

// 默认导出变量的引用列表 (每个模块一个)
const myMath = {
  PI,
  multiply
};
export default myMath;


// 2. 导入模块 (main.js)
// 导入默认导出的内容,名称可自定义
import myMath from './math.js';

// 导入命名导出的内容,名称必须匹配,可用 as 重命名
import { PI, multiply as mul } from './math.js';

// 整体导入所有命名导出到一个对象
import * as mathUtils from './math.js';

console.log(myMath.PI);
console.log(mul(2, mathUtils.PI));


// 3. 动态导入 (按需加载)
// import() 返回一个Promise
button.addEventListener('click', async () => {
  const module = await import('./path/to/module.js');
  module.doSomething();
});

特点

  • 静态化:依赖关系在代码编译阶段就能确定,这使得Tree Shaking(摇树优化)成为可能,可以移除未使用的代码,极大优化打包体积。
  • 只读引用import导入的是值的只读引用,而不是拷贝。修改原始模块中的值,所有引入的地方都会看到变化。
  • 异步加载:通过import()函数实现动态导入,完美替代AMD/CMD的异步功能。
  • 官方标准:是语言的一部分,无需引入第三方库。

对比总结

方面AMD (RequireJS)CMD (Sea.js)ES6 Module
核心理念依赖前置,提前执行依赖就近,延迟执行依赖前置,编译时确定
执行时机提前加载并执行所有依赖延迟加载,执行到require语句时才加载和执行依赖编译时加载,运行时只读引用
代表库RequireJSSea.js语言原生支持
语法关键词define, requiredefine, requireimport, export, export default
异步加载原生支持原生支持通过 import() 函数支持
静态分析难,依赖是动态字符串难,依赖是动态字符串容易,依赖是静态路径,利于Tree Shaking和优化
现状历史产物,基本淘汰历史产物,基本淘汰绝对主流,现代开发标准

最佳实践与学习建议

  1. 专注现代标准彻底学习和使用 ES6 Module。这是现在和未来的方向。
  2. 使用构建工具:在实际项目中,我们使用 WebpackViteRollup 等工具将ES6模块代码打包、转换、优化,使其兼容所有浏览器。
  3. 理解历史:只需了解AMD和CMD是模块化演进中的重要一环即可,无需深究其细节,除非维护老项目。
  4. 掌握关键特性
    • export / export default
    • import / import * as / as rename
    • 动态导入 import()
  1. 利用优化:享受ES6 Module带来的Tree Shaking作用域提升等优化红利,写出更高效、更精简的代码。