模块化的前世今生

对javascript模块化的一些总结…

原始写法

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

不同的函数简单放在一起,组成了一个模块,这样做污染了全局变量,模块成员之间看不出任何关系。

对象写法

1
2
3
4
5
6
7
var module1 = new Object({
_count: 0,
m1: function () {       //...
},
m2: function () {       //...
}
});

这样的写法会暴露所有模块成员,内部状态可以被外部改写。

立即执行函数

1
2
3
4
5
6
7
8
9
10
11
var module1 = (function () {
var _count = 0;
var m1 = function () {       //...
};
var m2 = function () {       //...
};
return {      
m1: m1,
m2: m2    
};
})();

这样可以达到不暴露私有成员的目的。

放大模式

如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用”放大模式”

1
2
3
4
5
var module1 = (function (mod) {
mod.m3 = function () {       //...
};
return mod;
})(module1);

上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。

宽放大模式

在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用”宽放大模式”。

1
2
3
4
5
var module1 = (function (mod) {
mod.m3 = function () {       //...
};
return mod;
})(module1 || {});

与”放大模式”相比,"宽放大模式"就是”立即执行函数”的参数可以是空对象。

输入全局变量

独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。

为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

1
2
3
var module1 = (function ($, YAHOO) {
  //...
})(jQuery, YAHOO);

模块化需要解决的问题

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

    CommonJS

    CommonJS模块可以很方便得将某个对象导出,让他们能够被其他模块通过 require 语句来引入。通过CommonJS,每个JS文件独立地存储它模块的内容(就像一个被括起来的闭包一样)。在这种作用域中,通过 module.exports 语句来导出对象为模块,再通过 require 语句来引入。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function myModule() {
    this.hello = function() {
    return 'hello!';
    }
    this.goodbye = function() {
    return 'goodbye!';
    }
    }
    module.exports = myModule;
1
2
3
4
5
var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

有两个好处:

  • 避免了全局命名空间污染
  • 明确了代码之间的依赖关系

需要注意的一点是,CommonJS以服务器优先的方式来同步载入模块,假使我们引入三个模块的话,他们会一个个地被载入。

它在服务器端用起来很爽,可是在浏览器里就不会那么高效了。毕竟读取网络的文件要比本地耗费更多时间。只要它还在读取模块,浏览器载入的页面就会一直卡着不动。

AMD

假使我们想要实现异步加载模块该怎么办?答案就是Asynchronous Module Definition(异步模块定义规范),简称AMD.

1
2
3
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});

这里使用 define 方法,第一个参数是依赖的模块,这些模块都会在后台无阻塞地加载,第二个参数则作为加载完毕的回调函数。

UMD

在一些同时需要AMD和CommonJS功能的项目中,你需要使用另一种规范:Universal Module Definition(通用模块定义规范)。

UMD创造了一种同时使用两种规范的方法,并且也支持全局变量定义。所以UMD的模块可以同时在客户端和服务端使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['myModule', 'myOtherModule'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('myModule'), require('myOtherModule'));
} else {
// Browser globals (Note: root is window)
root.returnExports = factory(root.myModule, root.myOtherModule);
}
}(this, function (myModule, myOtherModule) {
// Methods
function notHelloOrGoodbye(){}; // A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)
// Exposed public methods
return {
hello: hello,
goodbye: goodbye
}
}));

ES6模块

ES6 模块功能的特性是,导入是实时只读的。(CommonJS 只是相当于把导出的代码复制过来)。

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
// lib/counter.js
var counter = 1;
function increment() {
counter++;
}
function decrement() {
counter--;
}
module.exports = {
counter: counter,
increment: increment,
decrement: decrement
};
// src/main.js
var counter = require('../../lib/counter');
counter.increment();
console.log(counter.counter); // 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// lib/counter.js
export let counter = 1;
export function increment() {
counter++;
}
export function decrement() {
counter--;
}
// src/main.js
import * as counter from '../../counter';
console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2