4、AMD规范与RequireJS:异步模块定义

好,咱们进入第四章节。前面我们聊了CommonJS,那是服务端的神器。但到了浏览器端,情况就变了——文件加载是异步的,你不能等一个模块加载完再执行下一个。这时候,AMD规范就派上用场了。

AMD,全称是Asynchronous Module Definition,异步模块定义。说白了,它就是为浏览器量身定做的模块加载方案。我最早接触AMD是在2013年,那时候jQuery还是主流,RequireJS几乎是前端项目的标配。

define与require:核心用法

AMD规范里有两个核心方法:define和require。一个用来定义模块,一个用来加载模块。

define 的签名是这样的:

define(id?, dependencies?, factory);
  • id:模块标识,可选。我个人习惯不写,让文件名决定。
  • dependencies:依赖数组,可选。
  • factory:工厂函数,返回模块的导出内容。

来看个例子:

// 定义一个加法模块
define('calculator/add', ['jquery'], function($) {
  return function(a, b) {
    return a + b;
  };
});

require 的用法更直接:

require(['calculator/add'], function(add) {
  console.log(add(1, 2)); // 3
});

嗯,这里要注意:require是异步的。它不会阻塞后续代码的执行。我在项目中遇到过有人把require当import用,结果拿到的值是undefined——回调函数还没执行呢。

配置文件详解

RequireJS的配置文件叫require.config。它就像项目的总开关,所有模块的路径、依赖关系都在这里定义。

require.config({
  baseUrl: 'js/lib',
  paths: {
    'jquery': 'https://cdn.example.com/jquery-3.6.0.min',
    'calculator': '../app/calculator'
  },
  shim: {
    'backbone': {
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'
    }
  }
});

配置项说明:

配置项 作用 我的建议
baseUrl 所有模块的根路径 统一管理,避免路径混乱
paths 模块别名映射 CDN资源用这个,方便切换
shim 非AMD模块的兼容 老项目迁移时必备
map 模块版本映射 多版本共存时用
小技巧:paths配置里不要加.js后缀,RequireJS会自动补全。我曾经手滑加了后缀,结果加载了两次,排查了半天。

实战:用RequireJS重构计算器

好,理论说完了,咱们动手。还记得第三章那个计算器吗?现在用AMD规范重构它。

先看目录结构:

calculator/
├── index.html
├── js/
│   ├── main.js          # 入口文件
│   ├── app/
│   │   ├── calculator.js # 主模块
│   │   ├── add.js        # 加法
│   │   ├── subtract.js   # 减法
│   │   ├── multiply.js   # 乘法
│   │   └── divide.js     # 除法
│   └── lib/
│       └── require.js    # RequireJS库

第一步:定义各个运算模块

// js/app/add.js
define(function() {
  return function(a, b) {
    return a + b;
  };
});

// js/app/subtract.js
define(function() {
  return function(a, b) {
    return a - b;
  };
});

// 其他运算模块类似,不赘述

第二步:定义主模块

// js/app/calculator.js
define(['./add', './subtract', './multiply', './divide'], 
  function(add, subtract, multiply, divide) {
    return {
      add: add,
      subtract: subtract,
      multiply: multiply,
      divide: divide
    };
  }
);

第三步:配置入口文件

// js/main.js
require.config({
  baseUrl: 'js/app',
  paths: {
    'calculator': 'calculator'
  }
});

require(['calculator'], function(calc) {
  console.log('1 + 2 =', calc.add(1, 2));
  console.log('5 - 3 =', calc.subtract(5, 3));
  console.log('4 * 6 =', calc.multiply(4, 6));
  console.log('10 / 2 =', calc.divide(10, 2));
});

第四步:在HTML中引入

<!DOCTYPE html>
<html>
<head>
  <title>AMD计算器</title>
</head>
<body>
  <script data-main="js/main" src="js/lib/require.js"></script>
</body>
</html>
关键点:data-main属性告诉RequireJS入口文件是哪个。它会自动加载这个文件,然后执行里面的require.config和require调用。

避坑指南

我曾经在重构一个老项目时踩过几个坑,分享给你:

  • 循环依赖:A依赖B,B又依赖A。RequireJS会报错。解决办法是提取公共模块。
  • 路径问题:baseUrl和paths的优先级容易搞混。记住:paths会覆盖baseUrl。
  • 缓存问题:开发时经常遇到改了代码但浏览器还是加载旧版本。可以在urlArgs配置里加个时间戳。
require.config({
  urlArgs: 'bust=' + new Date().getTime()
});

嗯,这个技巧在调试时特别有用。但上线前记得去掉,不然用户每次访问都重新下载,性能就崩了。

AMD vs CommonJS:怎么选?

你可能会问:到底用哪个?我的经验是:

  • 浏览器端项目:用AMD(RequireJS)或ES Module
  • Node.js项目:用CommonJS
  • 新项目:直接用ES Module,打包工具会处理

说白了,AMD是特定历史时期的产物。现在ES Module已经普及,但理解AMD能帮你更好地理解前端模块化的演进脉络。而且,很多老项目还在用RequireJS,你总会遇到的。

好,这一章就到这里。下一章我们聊聊CMD规范与Sea.js,看看另一种异步加载的思路。