前端构建工具进化史之AMD

背景

最近在研究前端构建工具的发展历史。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 的模块化方案,根源上是一种过渡方案。随着时间的推移,它会完成使命,然后退出历史舞台。所以没有必要花太多的时间在这上面,有一个感性的认识就好了。

相关推荐
酉鬼女又兒3 分钟前
零基础快速入门前端JavaScript四大核心内置对象:Math、Date、String、Array全解析(可用于备赛蓝桥杯Web应用开发)
前端·javascript·css·蓝桥杯·前端框架·js
__sgf__7 分钟前
ES11(ES2020)新特性
前端·javascript
__sgf__22 分钟前
ES8(ES2017)新特性
前端·javascript
__sgf__25 分钟前
ES9(ES2018)新特性
前端·javascript
送鱼的老默31 分钟前
学习笔记--vue3 watchEffect监听的各种姿势用法和总结
前端·vue.js
你挚爱的强哥31 分钟前
解决:动态文本和背景色一致导致文字看不清楚,用js获取背景图片主色调,并获取对比度最大的hex色值给文字
前端·javascript·github
英俊潇洒美少年34 分钟前
js 同步异步,宏任务微任务的关系
开发语言·javascript·ecmascript
用户693717500138439 分钟前
Android 手机终于能当电脑用了
android·前端
wooyoo40 分钟前
花了一周 vibe 了一个 OpenClaw 的 Agent 市场,聊聊过程中踩的坑
前端·后端·agent
angerdream43 分钟前
最新版vue3+TypeScript开发入门到实战教程之路由详解
前端·javascript·vue.js