背景
最近在研究前端构建工具的发展历史。AMD 作为前端模块化历史中的一部分,必然是绕不开的。所以把它拎出来,做成一个小笔记。
简介
AMD 全称 Asynchronous Module Definition,是一种用于前端的模块化方案。与之相对应的,就是用于后端的 CommonJs 模块化方案。
当我们尝试去学习理解一个东西的时候,首先要问的就是为什么有这个东西?它解决了什么问题?如果没有它会有怎么样呢?
所以为什么会有 AMD ?回答这个问题需要我们回到当时的历史场景中。JavaScript 从诞生之初就没有模块化方案,这对于一门编程语言来说,本身就是一件不可思议的事情。在早期,如果前端逻辑简单,所有的代码写到一个 JavaScript 文件里面就可以了,然后在 html
文件中通过 script
标签引入。随着业务开始变得复杂,我们自然而然就会把代码进行分块管理,比如下面这样。
html
<script src="libs/utils/add.js"></script>
<script src="libs/utils/subtract.js"></script>
<script src="libs/utils/log.js"></script>
<script src="libs/utils/main.js"></script>
但这种方式带来了两个问题,一个是全局变量,另一个是 script
标签顺序。
全局变量问题
JavaScript 在2015年引入语言级别的模块化之前,所有 script
文件中定义的变量都会被定义到全局。流行的解决方式就是使用 Module Pattern。这种方式比较取巧,它使用一个函数把当前模块包裹起来,返回模块需要导出的内容。这样的话,局部变量就被局限在了函数作用域。网上的相关内容很多,这里就不重复了。
标签顺序问题
Module Pattern 并没有解决标签的顺序问题。依然需要我们手动控制 script
标签的导入顺序,如果顺序出错,就会产生bug。我们可以想象一下,当项目里面的标签比较多的时候,在开发过程中,需要时刻关注标签顺序,造成严重的心智负担。
AMD
要解决上面提到的两个问题,就需要一种 JavaScript 的模块化方案。2009 年,node 作为 JavaScript 的服务器端运行环境发布了,获得了很大的关注。node 使用 CommonJs 规范,实现了 JavaScript 的模块化方案。但是 CommonJs 没法在浏览器使用。所以前端需要一个自己的模块化方案,这个方案就是 AMD。
2010年,实现了 AMD 规范的 requireJs 发布。
requireJs
我们简单看一下,如何使用 requireJs。 首先去 requireJs 官网将代码下载下来。保存到本地 libs
文件夹下面,项目的结构如下
js
|-- index.html
|-- libs
|-- main.js
|-- require.js
|-- utils
|-- add.js
|-- subtract.js
|-- log.js
项目内容如下
js
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script data-main="libs/main" src="libs/require.js"></script>
</body>
</html>
//add.js
define(() => {
return {
add: (a, b) => {
return a + b;
}
}
})
//subtract.js
define(() => {
return {
subtract: (a, b) => {
return a - b;
}
}
})
//log.js
define(() => {
return function(inputLog) {
console.log("customized log method: ", inputLog);
}
})
//main.js
require(['./utils/add', './utils/subtract', './utils/log'],
(addMod, subtractMod, log) => {
const sum = addMod.add(1, 2);
const sub = subtractMod.subtract(1, 2);
log(sum);
log(sub);
}
);
本地浏览器打开运行,可以在控制台看到下面的输出信息。
log
customized log method: 3
customized log method: -1
requireJs 是怎么做到的呢?检查 index.html
文件头可以看到,requireJs 自动添加了 script
标签,浏览器会下载对应 js 代码进行解析执行。
可以看到,requireJs 顺利帮我们解决了我们前面的两个问题。变量被定义在 module 里面,不会泄漏到全局。script
自动生成,不用我们再去手动维护。
但是也可以明显的感觉到 requireJs 的这种语法,写起来很繁琐。 另外一个问题是,一个复杂的前端项目,可能会有几百上千个模块,如果所有模块都通过上面这种 http 请求的方式,势必会把页面拖垮。requireJs 怎么解决这个问题呢?
optimize
有趣的是,收集前端构建工具发展历史的过程中,我无意中发现 jquery的这个版本 依然使用的是 requireJs。随便打开一个源码文件 都可以看到 requireJs 的语法。
但是我们使用 jQuery 的时候,只需要引入一个 js 文件就可以了,所以应该可以通过某种方式,把所有的模块代码合并在一起。
在 jQuery 的构建代码中,有这么一行,可以看出来 requireJs 提供了一种拼接代码的优化方案,可以将多个文件拼接到一起。 我们也可以对照着写一个简单的构建代码。添加一个 build.js
放在 scripts
文件夹下面。
js
|-- index.html
|-- libs
|-- main.js
|-- require.js
|-- utils
|-- add.js
|-- subtract.js
|-- log.js
|-- scripts
|-- build.js
js
//build.js
const requirejs = require("requirejs");
const fs = require("fs");
const config = {
baseUrl: './libs',
optimize: "none",
findNestedDependencies: true,
out: (compiled) => {
console.log('write compiled result');
fs.writeFileSync('bundle.js', compiled, "utf8");
},
name: 'main',
};
requirejs.optimize(config, function(response) {
console.log(response);
})
运行以后的输出结果
js
//bundle.js
define('utils/add',[],() => {
return {
add: (a, b) => {
return a + b;
}
}
});
define('utils/subtract',[],() => {
return {
subtract: (a, b) => {
return a - b;
}
}
});
define('utils/log',[],() => {
return function(inputLog) {
console.log("customized log method: ", inputLog);
}
});
require(['./utils/add', './utils/subtract', './utils/log'],
(addMod, subtractMod, log) => {
const sum = addMod.add(1, 2);
const sub = subtractMod.subtract(1, 2);
log(sum);
log(sub);
}
);
define("main", function(){});
requirejs.optimize
的主要作用就是将这些模块进行了拼接和部分代码转换。在 index.html
中修改引入的代码就可以了。
html
<script data-main="bundle" src="libs/require.js"></script>
通过这种方式,就不用每一个模块就发送一个请求。
一个题外话,我顺便构建了一下 jQuery,发现产物和上面的 bundle.js
有些区别。估计是 jQuery 使用 Grunt 做了一些其他的处理。
总结
我们主要介绍了 AMD 出现的历史背景,解决的问题,以及 requireJs 的简单使用。AMD 作为 JavaScript 的模块化方案,根源上是一种过渡方案。随着时间的推移,它会完成使命,然后退出历史舞台。所以没有必要花太多的时间在这上面,有一个感性的认识就好了。