模块化模式

总访客数量

模块是闭包最强大的一种应用,在团队开发中是迫切需要的,可以更好的划分团队人员职能,做到精细化管理而互不干扰。

模块化的发展

初学者的代码 - 原始写法

作为一名初学者,写代码可能是一堆一堆的,所以为了好看一点,就把这一段代码包含在一个函数里面以供调用。

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

function m2(){
//...
}

上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就行了。

缺点是污染了全局变量,容易照成变量名冲突,各模块直接关系混乱。

如果代码都是大段大段的,最后导致写成了一堆无人能看懂的代码。

高级一点的代码 - 对象写法

所以就有人使用对象的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
var module1 = {

_count : 0,

m1 : function (){
//...
},

m2 : function (){
//...
}

};

上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。

但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。

1
module1._count = 5;

初具样子的代码 - 闭包写法

所以闭包来了,可以做到变量的私有化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];

function doSomething() {
console.log( something );
}

function doAnother() {
console.log( another.join( " ! " ) );
}

return {
doSomething: doSomething,
doAnother: doAnother
};
}

var foo = CoolModule();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

使用上面的写法,外部代码无法读取内部的something、another变量。

首先,CoolModule() 只是一个函数,但它 必须被调用 才能成为一个被创建的‘模块实例’(我们暂且叫foo为‘模块实例’或者‘实例’)。

第二,CoolModule() 函数返回一个对象。这个返回的对象拥有指向内部函数的引用,但是 没有 指向内部数据变量的引用;也可以看做是模块的公有API。我们也可以仅仅返回一个内部函数,jQuery 就是一个很好地例子。

这个返回值对象最终被赋值给外部变量 foo,然后我们可以在这个API上访问那些属性,比如 foo.doSomething()

doSomething()doAnother() 函数拥有模块“实例”内部作用域的闭包,也就是说foo拥有了访问模块词法作用域的能力。

这种方式可以可以重复创建多个‘实例’,如果想仅仅创建一个‘实例’,就要使用IIFE

再次升级 - IIFE写法

所以立即调用函数表达式来了,做到了在代码加载成功之后自动创建一个‘实例’,并且不会再次创建一个新的‘实例’;因为没有同样的‘实例’,所以这个‘实例’是‘单例’的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];

function doSomething() {
console.log( something );
}

function doAnother() {
console.log( another.join( " ! " ) );
}

return {
doSomething: doSomething,
doAnother: doAnother
};
})();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

模块的继承

如果一个模块很大要分成几个部分,或者要继承另一个模块,这是就要采用下面这种方式,同时最后一行的module1 || {}是必须的,防止浏览器异步加载导致的引用错误。

而且这样的好处是和外界完全隔离,也不需要在作用域链上进行疯狂的查找。

1
2
3
4
5
6
7
8
9
var module1 = (function (mod){

mod.m3 = function () {
//...
};

return mod;

})(module1 || {});

模块间的相互引用

模块间的相互引用也是没有问题的,只是写法有点特殊。

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
27
28
29
30
31
32
33
34
// 1. 首先注册模块实例
var module1 = {};
var module2 = {};
// 2. 实现模块实例
module1 = (function (mod1, mod2){

mod1.m1 = function(){
console.log('--module1--');
};

mod1.m2 = function() {
return mod2.m1()
}

return mod1;

})(module1 || {}, module2 || {});

module2 = (function (mod1, mod2){

mod2.m1 = function(){
console.log('--module2--');
};

mod2.m2 = function() {
return mod1.m1()
}

return mod2;

})(module1 || {}, module2 || {});
// 3. 调用模块实例方法
module1.m2() // --module2--
module2.m2() // --module1--

这里要注意的是要先注册模块实例,然后再实现各模块实例,最后要在全部模块加载完代码之后再执行即可。

ES6模块

ES6 引入了模块化,其设计思想是在编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块必须被定义在一个分离的文件中(每个模块一个)。浏览器/引擎拥有一个默认的“模块加载器”,它在模块被导入时同步地加载模块文件。

ES6 的模块化分为导出(export) @与导入(import)两个模块。

ES6 的模块自动开启严格模式,不管你有没有在模块头部加上 use strict;

模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等。

每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域。

每一个模块只加载一次(是单例的), 若再去加载同目录下同文件,直接从内存中读取。

应用

bar.js

1
2
3
4
5
function hello(who) {
return "Let me introduce: " + who;
}

export hello;

foo.js

1
2
3
4
5
6
7
8
9
10
11
12
// 导入“bar”模块中的`hello()`
import hello from "bar";

var hungry = "hippo";

function awesome() {
console.log(
hello( hungry ).toUpperCase()
);
}

export awesome;

app.js

1
2
3
4
5
6
7
8
9
// 导入`foo`和`bar`整个模块
module foo from "foo";
module bar from "bar";

console.log(
bar.hello( "rhino" )
); // Let me introduce: rhino

foo.awesome(); // LET ME INTRODUCE: HIPPO

在浏览器上直接使用ES6模块

module “my-module.js”

1
2
3
4
5
function cube(x) {
return x * x * x;
}

export cube;

script demo.js

1
2
import { cube } from 'my-module.js';
console.log(cube(3)); // 27

html

1
<script type="module" src="./demo.js"></script>

过渡阶段的模块

在ES6模块未出现之前出现了一些大家公认的模块规范,首先来看一个简单的例子,并了解实现原理。然后再了解其他的模块规范。

目前,通行的js模块规范主要有两种:CommonJS和AMD。

实现原理

模块依赖加载器/消息机制实质上都是将这种模块定义包装进一个友好的API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var MyModules = (function Manager() {
var modules = {};

function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}

function get(name) {
return modules[name];
}

return {
define: define,
get: get
};
})();

这段代码的关键部分是 modules[name] = impl.apply(impl, deps)。这为一个模块调用了它的定义的包装函数(传入所有依赖),并将返回值,也就是模块的API,存储到一个用名称追踪的内部模块列表中。

这里是我可能如何使用它来定义一个模块:

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
27
28
29
30
MyModules.define( "bar", [], function(){
function hello(who) {
return "Let me introduce: " + who;
}

return {
hello: hello
};
});

MyModules.define( "foo", ["bar"], function(bar){
var hungry = "hippo";

function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}

return {
awesome: awesome
};
} );

var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );

console.log(
bar.hello( "hippo" )
); // Let me introduce: hippo

foo.awesome(); // LET ME INTRODUCE: HIPPO

模块“foo”和“bar”都使用一个返回公有API的函数来定义。“foo”甚至接收一个“bar”的实例作为依赖参数,并且可以因此使用它。

它们只是满足了模块模式的两个性质:调用一个函数定义包装器,并将它的返回值作为这个模块的API保存下来。

Commonjs

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。

img

这标志”Javascript模块化编程”正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。

1
var math = require('math');

然后,就可以调用模块提供的方法:

1
2
var math = require('math');
math.add(2,3); // 5

因为这个系列主要针对浏览器编程,不涉及node.js,所以对CommonJS就不多做介绍了。我们在这里只要知道,require()用于加载模块就行了。

AMD

CommonJS规范不适用于浏览器环境,如果在浏览器中运行,会有一个很大的问题。

服务器端因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态,导致后面的代码不能继续执行。

因此采用”异步加载”(asynchronous),这就是AMD规范诞生的背景。

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

define() 函数

本规范只定义了一个函数 “define”,它是全局变量。函数的描述为:

1
define(id?, dependencies?, factory);
  1. 第一个参数,id - 名字:
    是个字符串。它指的是定义中模块的名字,这个参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。
  2. 第二个参数,dependencies - 依赖:
    是个定义中模块所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。
    本规范定义了三种特殊的依赖关键字。如果”require”,”exports”, 或 “module”出现在依赖列表中,参数应该按照CommonJS模块规范自由变量去解析。
  3. 第三个参数,factory - 工厂方法:
    为模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

define.amd 属性

为了清晰的标识全局函数(为浏览器加载script必须的)遵从AMD编程接口,任何全局函数应该有一个”amd”的属性,它的值为一个对象。这样可以防止与现有的定义了define函数但不遵从AMD编程接口的代码相冲突。

举例说明

创建一个名为”alpha”的模块,使用了require,exports,和名为”beta”的模块:

1
2
3
4
5
6
7
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
});

一个使用了简单CommonJS转换的模块定义:

1
2
3
4
5
6
define(function (require, exports, module) {
var a = require('a'),
b = require('b');

exports.action = function () {};
});

RequireJS

有人说 AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出,这一点没有深入的探究是否正确,但是根据AMD出现的原因,个人感觉是正确的。

RequireJS 是实现AMD规范的一个非常流行的库。使用前先去 官方网站下载 最新版本。

1
2
3
4
<!-- async 属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上 -->
<script src="js/require.js" defer async="true" ></script>
<!-- data-main 属性的作用是,指定网页程序的主模块 -->
<script src="js/require.js" data-main="js/main"></script>
主模块写法
1
2
3
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
// some code here
});

or

1
2
3
define(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
// some code here
});

子模块的写法也是一样的。

自定义模块加载行为

自定义模块加载行为通过 require.config() 实现,写在主模块的头部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require.config({
// 改变基目录
baseUrl: "js/lib",
// 指定各个模块的加载路径
paths: {
// 直接指定网址
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
"underscore": "underscore.min",
"backbone": "backbone.min",
},
// 加载非规范的模块。underscore和backbone这两个库,都没有采用AMD规范编写。
shim: {
'underscore':{
exports: '_', // 输出的变量名
},

'backbone': {
deps: ['underscore', 'jquery'], // 模块的依赖性
exports: 'Backbone', // 输出的变量名
}
}
});

require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。

插件

RequireJS 插件 实现了一些特定的功能。

domready插件,可以让回调函数在页面DOM结构加载完成后再运行。

1
2
3
require(['domready!'], function (doc){
// called once the DOM is ready
});

text和image插件,则是允许require.js加载文本和图片文件。

1
2
3
4
5
6
7
8
9
10
define([
'text!review.txt',
'image!cat.jpg'
],

function(review,cat){
console.log(review);
document.body.appendChild(cat);
}
);

类似的插件还有json和mdown,用于加载json文件和markdown文件。

CMD

CMD 模块规范是 seajs 推广的产物。

区别于 AMD 的异步加载,CMD 规范是懒加载,在用的时候才加载,一个文件是一个模块

defined() 函数

模块定义使用 define 关键字的方法,并接收一个factory的方法

1
2
3
define(function(require, exports, module) {
// 模块代码
});
  • define函数接受单个参数模块工厂。
  • factory可以是一个函数或其他有效的值。
  • 如果factory是函数,则该函数的前三个参数(如果已指定)必须依次为require exportsmodule
  • 如果factory不是函数,则将模块的导出设置为该对象。

具体的用法可以查看 Common Module Definition / draft CMD 模块定义规范

seajs

seajs 使用的是CMD模块规范,具体的使用方法请查看文档

到底哪个好用

RequireJs 和 seajs 有什么区别,哪一个更好用,这个在现在已经9012了,很多人都在用ES6的模块,还讨论这个话题就是为了六个念想。

前端为了减少请求次数都会打包到一个文件中,所以这两个就没什么区别。

谁说不是呢。

鉴于 RequireJs 应用更广泛,工程化更好,解决方案更多,推荐使用 RequireJs 。

在下方参考中也有很多的讨论,可自行查看

参考

你不懂JS:作用域与闭包
Javascript模块化编程(一):模块的写法
Javascript模块化编程(二):AMD规范
Javascript模块化编程(三):require.js的用法
export
import
详解JavaScript模块化开发
CommonJS
AMD (中文版))
require (中文版))
RequireJS
config-shim
【requireJS源码学习02】data-main加载的实现
RequireJS 插件
CMD 模块定义规范 –感觉说地不对,最好看接下来的英文版
CMD
seajs
与 RequireJS 的异同
SeaJS与RequireJS最大的区别
LABjs、RequireJS、SeaJS 哪个最好用?为什么?
AMD 和 CMD 的区别有哪些?