# 模块化
模块化
指在解决某一个复杂问题或者一系列问题时,依照一种分类的思维把问题进行系统性的分解加以处理。模块化是一种处理复杂系统分解为代码结构更为合理、可维护性更高的可管理的模块的方式。
重点:
- 信息隐藏:内部的处理过程和具体数据在外部调用时不可见,每个模块只完成独立的功能,提供该功能的结构,模块间通过接口访问
- 内聚度:内聚指的是模块内各个元素的联系程度,最不希望的就是出现偶尔性内聚,也就是没有关系的抽象放在同一模块中,最希望的就是功能性内聚,也就是一个模块提供一系列相互关联的功能。内聚是同一模块内部的实现,是信息隐藏和局部化概念的扩展,标志一个模块内部各个成分之间结合的紧密程度。设计时应尽可能提高模块内聚度,从而获得较高的模块独立性
- 耦合度:强耦合使得系统变得复杂,模块之间难以独立理解、修改。耦合度是指模块之间的关联程度,耦合度取决于模块之间接口的复杂程度,进入或调用模块的位置等。设计模块时,应该尽量追求松散耦合的系统
# 服务器端
# CommonJs
Commonjs
作为 Node
中模块化规范
特点:
- 原生
Module
对象,每个文件都是一个Module
实例 - 文件内通过
require
对象引入指定模块 - 所有文件加载均是同步完成
- 通过
module
关键字暴露内容 - 每个模块加载一次之后就会被缓存
- 模块编译本质上是沙箱编译
- 由于使用了
Node
的api
,只能在服务端环境上运行
优点:
- 强大的查找模块功能,开发十分方便
- 标准化的输入输出,非常统一
- 每个文件引入自己的依赖,最终形成文件依赖树
- 模块缓存机制,提高编译效率
- 利用
node
实现文件同步读取 - 依靠注入变量的沙箱编译实现模块化
注意:
exports = module.exports
require()
返回的是module.exports
module.exports
的初始值为一个空对象{}- 模块只有一个导出使用
module.exports=xxx
,多个使用export.a=a; export.b=b
参考:
- CommonJS 规范 (opens new window)
- require()源码解读 (opens new window)
- Node 中的 Module 源码分析 (opens new window)
原理 :
// 闭包 + 匿名立即执行函数
(function(module, exports, require) {
// b.js
var a = require("a.js");
console.log("a.name=", a.name);
console.log("a.age=", a.getAge());
var name = "lilei";
var age = 15;
exports.name = name;
exports.getAge = function() {
return age;
};
return module.exports;
})(module, module.exports, require);
// bundle.js
(function(modules) {
// 模块管理的实现
var installedModules = {};
/**
* 加载模块的业务逻辑实现
* @param {String} moduleName 要加载的模块名
*/
var require = function(moduleName) {
// 如果已经加载过,就直接返回
if (installedModules[moduleName])
return installedModules[moduleName].exports;
// 如果没有加载,就生成一个 module,并放到 installedModules
var module = (installedModules[moduleName] = {
moduleName: moduleName,
exports: {}
});
// 执行要加载的模块
modules[moduleName].call(modules.exports, module, module.exports, require);
return module.exports;
};
return require("index.js");
})({
"a.js": function(module, exports, require) {
// a.js 文件内容
},
"b.js": function(module, exports, require) {
// b.js 文件内容
},
"index.js": function(module, exports, require) {
// index.js 文件内容
}
});
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 浏览器端
# AMD 和 RequireJS
Commonjs
局限性很明显:基于 Node
原生 api
在服务端可以实现模块同步加载,但是仅仅局限于服务端,客户端如果同步加载依赖的话时间消耗非常大,所以需要一个在客户端上基于 Commonjs
但是对于加载模块做改进的方案,于是 AMD
规范诞生了。
AMD
是"Asynchronous Module Definition
"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到所有依赖加载完成之后(前置依赖),这个回调函数才会运行。
RequireJs
是 js
模块化的工具框架,是 AMD
规范的具体实现
特点:
- 依赖前置:动态创建
<script>
引入依赖,在<script>
标签的onload
事件监听文件加载完毕;一个模块的回调函数必须得等到所有依赖都加载完毕之后,才可执行,类似Promise.all
。 - 配置文件:有一个
main
文件,配置不同模块的路径,以及shim
不满足AMD
规范的js
文件。
# CMD 和 SeaJs
同样是受到 Commonjs
的启发,国内(阿里)诞生了一个 CMD(Common Module Definition)
规范。该规范借鉴了 Commonjs
的规范与 AMD
规范,在两者基础上做了改进。
特点:
define
定义模块,require
加载模块,exports
暴露变量。- 不同于
AMD
的依赖前置,CMD
推崇依赖就近(需要的时候再加载) - 推崇
api
功能单一,一个模块干一件事。
SeaJs 是 CMD 规范的实现:
- 需要配置模块对应的
url
- 入口文件执行之后,根据文件内的依赖关系整理出依赖树,然后通过插入
<script>
标签加载依赖。 - 依赖加载完毕之后,执行根
factory
- 在
factory
中遇到require
,则去执行对应模块的factory
,实现就近依赖 - 类似
Commonjs
,对所有模块进行缓存(模块的url
就是id
)。 - 类似
Commonjs
,可以使用相对路径加载模块。 - 可以向
RequireJs
一样前置依赖,但是推崇就近依赖。 exports
和return
都可以暴露变量
# ES6
ES6
模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
ES6
的模块自动采用严格模式,不管你有没有在模块头部加上"use strict
"
模块功能主要由两个命令构成:
export
命令用于规定模块的对外接口import
命令用于输入其他模块提供的功能
# export
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export
关键字输出该变量。下面使用 export
命令输出变量:
export var firstName = "Michael";
export var lastName = "Jackson";
export var year = 1958;
2
3
除了像上面这样,还有另外一种:
var firstName = "Michael";
var lastName = "Jackson";
var year = 1958;
export { firstName, lastName, year };
2
3
4
5
应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
除了输出变量,还可以输出函数或类(class
):
export function multiply(x, y) {
return x * y;
}
2
3
通常情况下,export
输出的变量就是本来的名字,但是可以使用 as
关键字重命名:
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
2
3
4
5
6
7
8
需要特别注意的是,export
命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系:
// 报错
export 1;
// 报错
var m = 1;
export m;
// 正确写法
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
同样的,function
和 class
的输出,也必须遵守这样的写法:
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
2
3
4
5
6
7
8
9
10
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,import
命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6
模块的设计初衷
# import
使用 export
命令定义了模块的对外接口以后,其他文件就可以通过 import
命令加载这个模块:
// main.js
import { firstName, lastName, year } from "./profile.js";
function setName(element) {
element.textContent = firstName + " " + lastName;
}
2
3
4
5
6
import
命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js
)对外接口的名称相同
如果想为输入的变量重新取一个名字,要使用 as 关键字,将输入的变量重命名:
import { lastName as surname } from "./profile.js";
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口:
import { a } from "./xxx.js";
a = {}; // Syntax Error : 'a' is read-only;
2
3
如果 a
是一个对象,改写 a
的属性是允许的:
import { a } from "./xxx.js";
a.foo = "hello"; // 合法操作
2
3
属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。
from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js 后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
注:import
命令具有提升效果,会提升到整个模块的头部,首先执行
由于 import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构:
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
2
3
4
5
6
7
8
9
10
11
12
13
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值都加载在这个对象上面:
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
// 整体加载
import * as circle from "./circle";
console.log("圆面积:" + circle.area(4));
console.log("圆周长:" + circle.circumference(14));
// 不允许运行时改变
// 下面两行都是不允许的
circle.foo = "hello";
circle.area = function() {};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# export default
使用 import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到 export default
命令,为模块指定默认输出:
// export-default.js
export default function() {
console.log("foo");
}
// 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
// import-default.js
import customName from "./export-default";
customName(); // 'foo'
// export default命令用在非匿名函数前,也是可以的
function foo() {
console.log("foo");
}
// foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载
export default foo;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default
的本质是将后面的值,赋给 default
变量,所以可以直接将一个值写在 export default
之后
也可以用来输出类:
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
2
3
4
5
6
# export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与 export
语句写在一起:
export { foo, bar } from "my_module";
// 可以简单理解为
import { foo, bar } from "my_module";
export { foo, bar };
2
3
4
5
← 工程化 原生 JavaScript 模块化 →