为什么需要模块化

在有模块化以前

我们来看下面一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<script src="a.js"></script>
<script src="b.js"></script>
<script>
alert("Hello world!");
</script>
<body></body>
</html>

在这段 html 中,有 a.js、b.js 和之后一段嵌入的 JavaScript 代码。硬要说其是“模块”也不是不可以,只不过它们在未经特殊处理的前提下,是会互相污染的。比如,在 a.js 中写 window.alert = function() {} 是会实实在在影响到后面的 b.js 和最后一段脚本中的 alert 的。

模块化的意义

JavaScript 模块化就是将代码分解为独立小块,每个块都有自己的接口、依赖和功能,每个模块都是一份密闭空间,同时为了使代码更加可维护、可重用和可读性更高。

在有 CommonJS、 ES6、AMD 等模块化之前如何解决模块化

全局函数

通过将不同的功能封装成不同的全局函数

1
2
3
4
5
6
function m1() {
//...
}
function m2() {
//...
}

缺点:污染全局命名空间且无法保证命名冲突 、如果使用变量名很长,导致调用不方便,模块之间看不出直接关系

iife(自执行函数)

1
2
3
(function () {
// statements
})();

利用的是函数作用域特性。通过闭包来做到内部变量的隔离,然后通过立即执行该闭包来得到相应的结果。这样就可以很方便地通过执行一些复杂逻辑来得到一个所谓的“模块”,而把逻辑变成内部私有形式给隔离开来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// module.js文件
(function (window, $) {
let data = "xxxx";
//操作数据的函数
function foo() {
//用于暴露有函数
console.log(`foo() ${data}`);
$("body").css("background", "red");
}
function bar() {
//用于暴露有函数
console.log(`bar() ${data}`);
}
//暴露行为
window.useFn = { foo, bar };
})(window, jQuery);
1
2
3
4
5
6
7
// index.html文件
<!-- 引入的js必须有一定顺序 -->
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
useFn.foo();
</script>

这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显

现代常用的模块化方式

在 iife 之后,主要出现了 4 大体系:即 CommonJS、AMD 、CMD 、UMD。AMD/CMD/UMD,只是首字母不一样,后两个字母是 Module Defintion 的缩写。CommonJS ,也叫 cjs,是一种同步加载模块。前面几种都是第三方规范和实现,与 JavaScript 无关。随着 ECMAScript 规范的不断规范和完善,出现了 ECMAScript modules,即 ES Modules (缩写 ESM)模块规范,因其在 ECMAScript 6 中提出,所以也叫 ES6 Modules 模块化。相较于之前的几种方式都是通过三方实现函数和对象来模拟模块,ESM 只要宿主支持,该语法就能直接使用。
Node.js 的 ESM 最开始是在 v8.5.0 中,只不过当时还是 Experimental 特性,如果要使用需要加上 --experimental-modules参数才能正常启动程序;在 v12.17.0 版本后才移除了这个参数并使之成为了正常使用的方式。

AMD/CMD/UMD 简介

因为这些模块化历史久远,已不适用于近些年的开发习惯,但又是历史产物,可能在某些老项目或特定场景中还在使用,所以还是提一下其主要特征和基本概念。

  • AMD

AMD(Asynchronous Module Definition)模块化规范。采用异步加载模块,适用于浏览器端开发。可以在不影响后面代码执行的情况下加载模块。AMD 最开始在 require.js 中被使用,其首个提交在 2009 年出现,AMD 推荐依赖前置,提前执行。

AMD 规范中,一个模块可以通过 define 函数来定义。define 函数的第一个参数是一个数组,用于声明该模块的依赖项。第二个参数是一个函数,用于定义模块的功能。模块的输出通过 return 语句来实现。

在 AMD 规范中,通过 require 函数来引入模块。require 函数的第一个参数是依赖项的数组,第二个参数是一个回调函数,用于获取模块的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义模块
//utils.js
define([], function () {
return {
add: function (a, b) {
console.log(a + b);
},
};
});
// 加载模块
//main.js
require(["./utils"], function (utils) {
utils.add(1, 2);
});
  • CMD

CMD 是 Common Module Definition,即一般模块定义,产生于 Sea.js 中。虽然 Common 也含有通用的意思,与 AMD 不同的是, 模块的加载是异步的,在使用模块时才加载。CMD 推崇依赖就近、延迟执行,也是通过 define 定义模块,require 引入模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义模块
// math.js
define(function (require, exports, module) {
exports.add = function (x, y) {
return x + y;
};
exports.subtract = function (x, y) {
return x - y;
};
});

// 加载模块
// app.js
define(function (require) {
var math = require("math");
console.log(math.add(2, 3)); // 输出 5
console.log(math.subtract(5, 3)); // 输出 2
});
  • UMD

UMD 意为通用模块,内部会分析是 AMD 还是 CMD 还是 CommonJs 规范,都不是的话会把引入的模块内容挂载到全局上。从而使不同规范的代码都可以正常使用。主要是通过像 rollup 或 webpack 等打包工具配置输出 umd 的格式。最后打包结果中去做具体是运行环境支持哪种规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义模块
// math.js
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define([], factory);
} else if (typeof exports === "object") {
module.exports = factory();
} else {
root.math = factory();
}
})(this, function () {
return {
add: function (x, y) {
return x + y;
},
subtract: function (x, y) {
return x - y;
},
};
});

// 加载模块
// app.js
var math = require("math");
console.log(math.add(2, 3)); // 输出 5
console.log(math.subtract(5, 3)); // 输出 2

CommonJS 规范

简介

CommonJS 模块规范发布于 2009 年,由 Mozilla 工程师 Kevin Dangoor 起草,他于当年 1 月发表了一篇文章《What Server-side JavaScript Needs》。最初的主要目的是为在浏览器环境之外的 JavaScript 环境建立模块生态系统公约。因而最初叫做 ServerJS。后来觉得显得太有局限性,就把名字改成了 CommonJS,又把浏览器包括了回来。在 CommonJS 的官网,是这么一句口号:

JavaScript: not just for browsers any more!

简单概括 CommonJS,即它是 JavaScript 语言的一种模块化规范。规定了模块的定义、引入和使用方式。 主要特点是同步加载,即只有加载完成后才能执行后面的代码。

模块定义

在 CommonJS 规范中,一个模块就是一个单独的文件。每个文件都是一个模块,文件内部的所有变量、函数和对象都属于该模块的私有作用域。要在其他模块中使用该模块的内部变量和方法,需要通过 module.exports 或 exports 对象进行导出。

exports 对象,是一个用于导出模块内容的通道

modlue 对象,是一个包含该模块元信息的执行结果,里面包含的有模块的 id,modlue,exports 等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个模块,计算圆的面积和周长
const PI = 3.14;

function calcArea(radius) {
return PI * radius * radius;
}

function calcCircumference(radius) {
return 2 * PI * radius;
}

// 批量导出模块
module.exports = {
calcArea,
calcCircumference,
};
//或一个一个导出
exports.calcArea = calcArea;
exports.calcCircumference = calcCircumference;

在上面的代码定义了一个模块,包含了计算圆的面积和周长的方法。最后通过 module.exports 对象将这些方法导出,以便其他模块可以调用。

问题 1:module.exports 和 exports 的关系是什么

exports = 值,不会改变 modlue.exports 的内容空间

但如果通过 exports.xxx = 值,会改变 modlue.exports 的内容

同时 module.exports === this 为 true

模块引入

在 CommonJS 规范中,通过 require 函数来引入模块。require 函数的参数是模块的路径,返回值是模块导出的对象。

1
2
3
4
//引入上面代码
const { calcArea, calcCircumference } = require("./moduleFile");
//或
const calcArea = require("./moduleFile").calcArea;

简介

ECMAScript Modules 模块化

ECMAScript Modules 又称 ES Modules,缩写 ESM。因为其首次在 ECMAScript 6 中被提出,也称其为 ES6 Modules。下面我们统称 ES6 Modules。ES6 Modules 规范中引入了一种新的模块化机制。它的设计非常“精简”与“官方”,从语法层面就完成了对模块的定义。像 CommonJS 也好,AMD、CMD 等也罢,都是通过三方实现函数和对象来模拟模块,而 ESM 则直接通过 import 与 export 语法来导入和导出模块。只要宿主支持,那么该语法就直接能用。

模块定义

ES6 模块化中,一个模块可以通过 export 关键字来导出变量、函数或对象,也可以通过 import 关键字来引入其他模块的内容。

1
2
3
4
5
6
7
8
9
10
// export.js
//默认导出
export default function a() {
console.log("foo");
}
//导出多个
export const name = "ESM";
export function greeting() {
console.log(`Hello, ${name}!`);
}

模块引入

ES6 模块化中,通过 import 关键字导入一个或多个模块,也可以使用 import * as 命令将所有导出的模块作为一个对象导入。import 关键字的参数是模块的路径,返回值是模块导出的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// module.js
export const name = "ESM";
export function greeting() {
console.log(`Hello, ${name}!`);
}
export default function () {
console.log(`This is a default export function.`);
}

// app.js
import * as module from "./module.js";
console.log(module.name); // 输出 'ESM'
module.greeting(); // 输出 'Hello, ESM!'
module.default(); // 输出 'This is a default export function.'

也可以在一个文件夹下的 index.js 中。批量导出所引入的模块。

1
2
3
//utils/index.js
export * from "./export.js";
export * from "./b.js";

在所需要的模块中就可以直接使用,而不需要每个文件都通过 import 来引入

1
2
//xxx.js
import { greeting } from "./utils";

ESM 还支持动态导入模块,这意味着我们可以在运行时动态地加载模块。

1
2
3
4
5
6
// app.js
async function loadModule() {
const module = await import("./module.js");
module.greeting(); // 输出 'Hello, ESM!'
}
loadModule();

问题 2:ES6 Modules 和 CommonJS 的区别

  • CommonJS 模块输出的是一个值的拷贝,ES6 Modules 模块输出的是值的引用,因而当模块内部发生变化,ES6 Modules 可以跟踪到变化,而 CommonJS 不能。
  • CommonJS 模块是运行时做的模块加载和运行,可以在代码执行一半的时候以动态的方式加载,这种方法在一些静态分析的时候会造成阻碍,ES6 Modules 模块是在模块顶部以语法的形式加载模块,完全可以做静态分析。