JavaScript模块化

JavaScript模块化

模块化的理解

1、什么是模块化

  • 将一个复杂的程序,依据一定的规则(规范)封装成一个或多个块(文件), 并进行组合在一起。
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。

2、模块化的进化过程

无模块时代

在 ajax 还未提出之前,js 还只是用来在网页上进行表单校验、提交,对 DOM 渲染操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var str, num;
//......
function submit() {
str = document.getElementById("xx").value;
if (str) {
//......
} else {
//......
}
num = 1;
for (var i = 0; i < 10; i++) {
num++;
//......
}
//......
form.submit();
}
1
2
3
4
<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>
<script type="text/javascript" src="main.js"></script>

缺点:

  • 全局变量污染
  • 函数命名冲突
  • 文件依赖顺序

模块雏形时代

2006 年,ajax 的概念被提出,前端拥有了主动向服务端发送请求并操作返回数据的能力,传统的网页向“富客户端”发展,出现了简单的功能对象封装。

  • namespace 模式
1
2
3
4
5
6
7
8
//模块js
var myModule = {
first_name: "www.",
second_name: "baidu.com",
getFullName: function () {
return this.first_name + this.second_name;
},
};
1
2
3
4
//调用js
console.log(myModule.getFullName());
myModule.first_name = "img.";
console.log(myModule.getFullName());

优点: 减少了全局变量,解决命名冲突
缺点: 数据不安全(外部可以直接修改模块内部的数据),模块名称会暴露在全局,存在命名冲突,依赖顺序问题

  • 自执行匿名函数(闭包)模式
1
2
3
4
5
6
7
8
9
10
11
//模块js
(function (window) {
let _moduleName = "module";
function setModuleName(name) {
_moduleName = name;
}
function getModuleName() {
return _moduleName;
}
window.moduleA = { setModuleName, getModuleName };
})(window);
1
2
3
4
//调用js
moduleA.setModuleName("html-module");
console.log(moduleA.getModuleName());
console.log(moduleA._moduleName); //模块不暴露,无法访问模块内属性方法

优点:变量、方法全局隐藏,模块私有化
缺点:模块名称会暴露在全局,存在命名冲突,依赖顺序问题

3、面临的问题

从以上的尝试中,可以归纳出 js 模块化需要解决那些问题:

  • 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
  • 如何唯一标识一个模块?
  • 如何优雅的把模块的 API 暴漏出去?(不能增加全局变量)
  • 如何方便的使用所依赖的模块?

模块化的规范

1、CommonJS

2009 年 Nodejs 发布,采用 CommonJS 模块规范。

特点:

  • 每个文件都是一个模块实例,代码运行在模块作用域,不会污染全局作用域。
  • 文件内通过 require 对象引入指定模块,通过 exports 对象来向往暴漏 API,文件内定义的变量、函数,都是私有的,对其他文件不可见。
  • 每个模块加载一次之后就会被缓存。
  • 所有文件加载均是同步完成,加载的顺序,按照其在代码中出现的顺序。
  • 模块输出的是一个值的拷贝,模块内部的变化不会影响该值。
1
2
3
4
5
6
7
8
9
//模块js
let _moduleName = "module";
function setModuleName(name) {
_moduleName = name;
}
function getModuleName() {
return _moduleName;
}
module.exports = { setModuleName, getModuleName };
1
2
3
4
//调用js
import { getModuleName, setModuleName } from "./es6.module";
setModuleName("es6 Module");
console.log(getModuleName());

缺点:模块同步加载,资源消耗和等待时间,适用于服务器编程

2、AMD/RequireJS

Commonjs 局限性很明显:

基于 Node 原生 api 在服务端可以实现模块同步加载,但是仅仅局限于服务端,客户端如果同步加载依赖的话时间消耗非常大,所以需要一个在客户端上基于 Commonjs 但是对于加载模块做改进的方案,于是 AMD 规范诞生了。

AMD 是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到所有依赖加载完成之后(依赖前置),这个回调函数才会运行。

RequireJS 是一个工具库,主要用于客户端的模块管理。它的模块管理遵守 AMD 规范,RequireJS 的基本思想是,通过 define 方法将代码定义为模块,通过 require 方法实现代码的模块加载。

1
2
3
4
5
6
7
8
// module1.js 定义没有依赖的模块
define(function () {
let _moduleName = "module";
function getName() {
return _moduleName;
}
return { getName }; // 暴露模块
});
1
2
3
4
5
6
7
8
9
10
11
12
// module2.js 定义有依赖的模块
define(["module1"], function (module1) {
let _firstName = "AMD";
function getFullName() {
return _firstName + " " + module1.getName();
}
function setFirstName(name) {
_firstName = name;
}
// 暴露模块
return { _firstName, getFullName, setFirstName };
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//mian.js
require.config({
paths: {
module1: "./modules/module1",
module2: "./modules/module2",
// 第三方库模块
jquery: "./libs/jquery.min",
},
});
require(["module2", "jquery"], function (module2, jquery) {
console.log(module2.getFullName());
module2.setFirstName("AMD-AMD");
console.log(module2.getFullName());
console.log(module2._firstName);
jquery("#moduleId").html("<i>My name is jquery-module</i>");
});
1
2
//html中引入工具库,并定义js主文件
<script data-main="./main" src="./libs/require.js"></script>

特点:浏览器直接运行无需编译,异步加载,依赖关系清晰

3、CMD/SeaJS

CMD 规范专门用于浏览器端,同样是受到 Commonjs 的启发,国内(阿里)诞生了一个 CMD(Common Module Definition)规范。该规范借鉴了 Commonjs 的规范与 AMD 规范,在两者基础上做了改进。

与 AMD 相比非常类似,CMD 规范(2011)具有以下特点:

  • define 定义模块,require 加载模块,exports 暴露变量。
  • 不同于 AMD 的依赖前置,CMD 推崇依赖就近(需要的时候再加载)
  • 推崇 api 功能单一,一个模块干一件事。

SeaJs 是 CMD 规范的实现,跟 RequireJs 类似,CMD 是 SeaJs 推广过程中诞生的规范。CMD 借鉴了很多 AMD 和 Commonjs 优点。

1
2
3
4
5
6
//module.1
define(function (require, exports, module) {
module.exports = {
msg: "I am module1",
};
});
1
2
3
4
5
6
7
8
//module.2
define(function (require, exports, module) {
var module2 = require("./module1");
function show() {
console.log("同步引入依赖模块1 " + module2.msg);
}
exports.showModule = show;
});
1
2
3
4
5
//main.js
define(function (require) {
var m2 = require("./modules/module2");
m2.showModule();
});
1
2
3
4
5
//html中引入工具库,并定义js主文件
<script type="text/javascript" src="./libs/sea.js"></script>
<script type="text/javascript">
seajs.use('./main')
</script>

AMD、CMD 区别

  • AMD 推崇依赖前置
  • CMD 推崇依赖就近
1
2
3
4
5
6
7
8
//AMD
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
...
// 此处略去 100 行
b.doSomething()
...
})
1
2
3
4
5
6
7
8
9
10
//CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
...
// 此处略去 100 行
var b = require('./b') // 依赖可以就近书写
b.doSomething()
...
}

4、ES6

2015 年,ES6 规范中,终于将模块化纳入 JavaScript 标准,从此 js 模块化被 ECMA 官方扶正,也是后来 js 的标准。
ES6 中的模块化在 CommonJS 的基础上有所不同,关键字有 import,export,default,as,from。

1
2
3
4
5
6
7
8
9
//模块js
let _moduleName = "module";
function setModuleName(name) {
_moduleName = name;
}
function getModuleName() {
return _moduleName;
}
export { setModuleName, getModuleName };
1
2
3
4
//调用js
import { getModuleName, setModuleName } from "./es6.module";
setModuleName("es6 Module");
console.log(getModuleName());

CommonJS 和 ES6 区别

  • CommonJS 模块输出的是一个值的拷贝,即原来模块中的值改变不会影响已经加载的该值。
    ES6 模块输出的是值的只读引用,模块内值改变,引用也改变。
  • CommonJS 模块是运行时加载,加载的是整个模块,即将所有的接口全部加载进来。
    ES6 模块是编译时输出接口,可以单独加载其中的某个接口。

总结

  • CommonJS 规范主要用于服务端编程,加载模块是同步的,不适合在浏览器环境,存在阻塞加载,浏览器资源是异步加载的,因此有了 AMD、CMD 解决方案。
  • AMD 规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。
  • CMD 规范与 AMD 规范很相似,都用于浏览器编程,依赖就近,代码更简单。
  • ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
作者

刘剑

发布于

2020-05-29

更新于

2021-08-04

许可协议

CC BY-NC-SA 4.0

评论