深入浅出 AMD 模块化使用

一. 什么是 AMD

CommonJS 的缺点之一是它是同步的,AMD 旨在通过规范中定义的 API 异步加载模块及其依赖项来解决这个问题。AMD 全称为 Asynchronous Module Definition,即异步模块加载机制。它规定了如何定义模块,如何对外输出,如何引入依赖。

AMD规范重要特性就是异步加载。所谓异步加载,就是指同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。这种加载方式和浏览器环境的性能需求刚好吻合。

1. 语法

AMD 规范定义了一个全局函数 define,通过它就可以定义和引用模块,它有 3 个参数:

define(id?, dependencies?, factory);

其包含三个参数:

  • id:可选,指模块路径。如果没有提供该参数,模块名称默认为模块加载器请求的指定脚本的路径。

  • dependencies:可选,指模块数组。它定义了所依赖的模块。依赖模块必须根据模块的工厂函数优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入工厂函数中。

  • factory:为模块初始化要执行的函数或对象。如果是函数,那么该函数是单例模式,只会被执行一次;如果是对象,此对象应该为模块的输出值。

除此之外,要想使用此模块,就需要使用规范中定义的 require 函数:

require(dependencies?, callback);

其包含两个参数:

  • dependencies:依赖项数组;

  • callback:加载模块时执行的回调函数。

有关 AMD API 的更详细说明,可以查看 GitHub 上的 AMD API 规范:https://github.com/amdjs/amdjs-api/blob/master/AMD.mdicon-default.png?t=O83Ahttps://github.com/amdjs/amdjs-api/blob/master/AMD.md

2. 兼容性

该规范的浏览器兼容性如下:

fileOf7174.png

3. 优缺点

AMD 的优点

  • 异步加载导致更好的启动时间;

  • 能够将模块拆分为多个文件;

  • 支持构造函数;

  • 无需额外工具即可在浏览器中工作。

AMD 的缺点

  • 语法很复杂,学习成本高;

  • 需要一个像 RequireJS 这样的加载器库来使用 AMD。

二. 使用

当然,上面只是 AMD 规范的理论,要想理解这个理论在代码中是如何工作的,就需要来看看 AMD 的实际实现。RequireJS 就是 AMD 规范的一种实现,它被描述为“JavaScript 文件和模块加载器”。下面就来看看 RequireJS 是如何使用的。

1. 引入RequireJS

可以通过 npm 来安装 RequireJS:

npm i requirejs

也可以在 html 文件引入 require.js 文件:

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

这里 script标签有两个属性:

  • data-main="js/config":这是 RequireJS 的入口,也是配置它的地方;

  • src="js/require.js":加载脚本的正常方式,会加载 require.js 文件。

在 script 标签下添加以下代码来初始化 RequireJS:

<script>
    require(['config'], function() {
        //...
    })
</script>

当页面加载完配置文件之后, require() 中的代码就会运行。这个 script 标签是一个异步调用,这意味着当 RequireJS 通过 src="js/require.js 加载时,它将异步加载 data-main 属性中指定的配置文件。因此,该标签下的任何 JavaScript 代码都可以在 RequireJS 获取时执行配置文件。

那 AMD 中的 require() 和 CommonJS 中的 require() 有什么区别呢?

  • AMD require() 接受一个依赖数组和一个回调函数,CommonJS require() 接受一个模块 ID;

  • AMD require() 是异步的,而 CommonJS require() 是同步的。

2. 定义 AMD 模块

下面是 AMD 中的一个基本模块定义:

define(['dependency1', 'dependency2'], function() {
  // 模块内容
});

这个模块定义清楚地显示了其包含两个依赖项和一个函数。

下面来定义一个名为addition.js的文件,其包含一个执行加法操作的函数,但是没有依赖项:

// addition.js
define(function() {
    return function(a, b) {
        alert(a + b);
    }
});

再来定义一个名为 calculator.js 的文件:

define(['addition'], function(addition) {
    addition(7, 9);
});

当 RequireJS 看到上面的代码块时,它会去寻找依赖项,并通过将它们作为参数传递给函数来自动将其注入到模块中。

RequireJS 会自动为 addition.js 和 calculator.js 文件创建一个 <script> 标签,并将其放在HTML <head> 元素中,等待它们加载,然后运行函数,这类似于 require() 的行为。

fileOf7174.png

下面来更新一下 index.html 文件:

// index.html
require(['config'], function() {
    require(['calculator']);
});

当浏览器加载 index.html 文件时,RequireJS 会尝试查找 calculator.js 模块,但是没有找到,所以浏览器也不会有任何反应。那该如何解决这个问题呢?我们必须提供配置文件来告诉 RequireJS 在哪里可以找到 calculator.js(和其他模块),因为它是引用的入口。

下面是配置文件的基本结构:

requirejs.config({
    baseURL: "string",
    paths: {},
    shim: {},
});

这里有三个属性值:

  • baseURL:告诉 RequireJS 在哪里可以找到模块;

  • path:这些是与 define() 一起使用的模块的名称。在路径中,可以使用文件的 CDN,这时 RequireJS 将尝试在本地可用的模块之前加载模块的 CDN 版本;

  • shim:允许加载未编写为 AMD 模块的库,并允许以正确的顺序加载它们

我们的配置文件如下:

requirejs.config({
    baseURL: "js",
    paths: {
        // 这种情况下,模块位于 customScripts 文件中
        addition: "customScripts/addition",
        calculator: "customScripts/calculator",
    },
});

配置完成之后,重新加载浏览器,就会收到浏览器的弹窗:

fileOf7174.png

这就是在 AMD 中使用 RequireJS 定义模块的方法之一。我们还可以通过指定其路径名来定义模块,该路径名是模块文件在项目目录中的位置。下面给出一个例子:

define("path/to/module", function() {
    // 模块内容
})

当然,RequireJS 并不鼓励这种方法,因为当我们将模块移动到项目中的另一个位置时,就需要手动更改模块中的路径名。

在使用 AMD 定义模块时需要注意:

  • 在依赖项数组中列出的任何内容都必须与工厂函数中的分配相匹配;

  • 尽量不要将异步代码与同步代码混用。当在 index.html 上编写其他 JavaScript 代码时就是这种情况。