咱們再一次被計算機的名詞、概念籠罩。html
Backbone、Emberjs、Spinejs、Batmanjs 等MVC框架侵襲而來。
CommonJS、AMD、NodeJS、RequireJS、SeaJS、Curljs 等模塊化的JavaScript概念及庫撲面而來。前端
模塊化JavaScript的概念尤其突出,彷佛有趕超07年Ajax風潮之趨勢。node
寫函數(過程式)
2005年之前,JavaScript沒人重視,只做爲表單驗證等少許應用。那時一個網頁上寫不了幾行JS代碼,1000行算很複雜了。這時組織代碼的方式是過程式,幾十行的代碼甚至連一個函數都不用寫。稍多的須要提取抽象出一個函數,更復雜一些則須要更多函數,函數間互相調用。git
寫類(面向對象)
2006年,Ajax席捲全球。JavaScript被重視了,愈來愈多的後端邏輯放到了前端。網頁中的JS代碼量急劇增長。這時寫函數方式組織大量代碼顯得力不從心。有時調試一個小功能,從一個函數可能會跳到第N個函數去。這時寫類的方式出現了,Prototype 率先流行開來。用它組織代碼,寫出的都是一個個類,每一個類都是Class.create建立的。又有YUI、Ext等重量級框架。雖然它們的寫類方式各不一樣,但它們的設計思路卻都是要知足大量JavaScript代碼的開發。程序員
寫模塊(如今,將來?)
2009年,Nodejs誕生!這個服務器端的JavaScript採用模塊化的寫法很快征服了瀏覽器端的JSer。牛人們紛紛仿效,各類寫模塊的規範也是層出不窮。CommonJS想統一先後端的寫法,AMD則認爲本身是適合瀏覽器端的。好吧,不管寫模塊的風格是啥樣,寫模塊化的JavaScript卻已開始流行了。你,準備好了嗎?github
模塊化的JavaScript是神馬? 這是咱們發明了又一個銀彈嗎?不管是啥,就當學習吧。至於適不適合項目中使用,各自斟酌。json
寫到這也沒說什麼是「模塊」。其實在計算機領域,模塊化的概念被推崇了近四十年。軟件整體結構體現模塊化思想,即把軟件劃分爲一些獨立命名的部件,每一個部件稱爲一個模塊,當把全部模塊組裝在一塊兒的時候,即可得到問題的一個解。模塊化以分治法爲依據,可是否意味着咱們把軟件無限制的細分下去?事實上當分割過細,模塊總數增多,每一個模塊的成本確實減小了,但模塊接口所需代價隨之增長。後端
要確保模塊的合理分割則須瞭解信息隱藏,內聚度及耦合度。設計模式
信息隱藏
模塊應設計的使其所包含的信息(過程和數據)對於那些不須要用到它的模塊不可見。每一個模塊只完成一個獨立的功能,而後提供該功能的接口。模塊間經過接口訪問。JavaScript中能夠用函數去隱藏,封裝,然後返回接口對象。以下是一個提供事件管理的模塊event。跨域
1
2
3
4
5
6
7
8
|
Event = function() {
// do more
return {
bind: function() {},
unbind: function() {},
trigger: function() {}
};
}();
|
函數內爲了實現想要的接口bind、unbind、trigger可能須要寫不少不少代碼,但這些代碼(過程和數據)對於其它模塊來講沒必要公開,外部只要能訪問接口bind,unbind,trigger便可。
信息隱藏對於模塊設計好處十分明顯,它不只支持模塊的並行開發,並且還可減小測試或後期維護工做量。如往後要修改代碼,模塊的隱藏部分可隨意更改,前提是接口不變。如事件模塊開始實現時爲了兼容舊版本IE及標準瀏覽器,寫了不少IE Special代碼,有一天舊版本IE消失了(猴年馬月),只需從容刪去便可。
內聚度
內聚是來自結構化設計的一個概念,簡單說內聚測量了單個模塊內各個元素的聯繫程度。最不但願出現的內聚就是偶然性內聚,即將徹底無關的抽象塞進同一個模塊或類中。最但願出現的內聚是功能性內聚,即一個模塊或類的各元素一同工做,提供某種清晰界定的行爲。
內聚度指模塊內部實現,它是信息隱藏和局部化概念的天然擴展,它標誌着一個模塊內部各成分彼此結合的緊密程度。好處也很明顯,當把相關的任務分組後去閱讀就容易多了。設計時應該儘量的提升模塊內聚度,從而得到較高的模塊獨立性。
耦合度
耦合也是來自結構化設計,Stevens、Myers和Constantine將耦合定義爲「一個模塊與另外一個模塊之間創建起的關聯強度的測量。強耦合使系統變得複雜,由於若是模塊與其它模塊高度相連,它就難以獨立的被理解、變化和修正」
內聚度是指特定模塊內部實現的一種度量,耦合度則是指模塊之間的關聯程度的度量。耦合度取決於模塊之間接口的複雜性,進入或調用模塊的位置等。與內聚度相反,在設計時應儘可能追求鬆散耦合的系統。
在JavaScript模塊究竟是什麼,能用代碼具體展示一下嗎?其實上面已經寫了一段事件模塊代碼
這能表明「模塊」嗎?這就是一個JS對象啊,覺得有多麼深奧。
是的,JavaScript中模塊多數時候被實現爲一個對象。這麼看來,多數時候咱們都寫過「模塊」(但沒有在整個項目中應用模塊化思想)。或許每一個人寫模塊的方式(風格)還不一樣。好比上面的事件模塊是一個匿名函數執行,匿名函數中封裝了不少代碼,最後經過return返回給Event變量,這個Event就是事件模塊的接口。
又如jQuery,它也是一個匿名函數執行,但它並不返回接口對象。而是將本身暴露給window對象。
1
2
3
4
5
|
(function(window){
// ..
// exports
window.jQuery = window.$ = jQuery;
})(window);
|
再如SeaJS,它一開始就將接口公開了
1
2
3
4
|
/**
* Base namespace for the framework.
*/
this.seajs = { _seajs: this.seajs };
|
後續是不少的匿名函數執行給變量seajs添加不少工具方法。注意,這裏的this在瀏覽器環境指window對象,若是是定位在瀏覽器中,這個this也能夠去掉。就象Ext。
1
2
3
4
5
6
7
|
Ext = {
/**
* The version of the framework
* @type String
*/
version : '3.1.0'
};
|
咱們已經看到了四種方式寫模塊(把jQuery,SeaJS,Ext當作模塊,呃很大的模塊)。哪種更好呢? 哪種更適合在瀏覽器端呢?純從代碼風格上說,是蘿蔔白菜各有所愛。只要咱們運用了「模塊化」的思想來開發就好了。
但若是有一種統一的語法格式來寫模塊豈不是更好,就不會出現各用各的風格來寫模塊而使代碼亂糟糟。
這就是目前的現狀,開發者強烈須要一種統一的風格來寫模塊(最好是語言內置了)。這時一些組織出現了,最具表明的如CommonJS,AMD。此外ECMAScript也開始着手模塊的標準化寫法。
不管它們提供什麼樣的寫法,咱們須要的僅僅是:
服務器端的JSer是幸運的,它有Node.js,Node.js遵循了一個稱爲CommonJS的規範。CommonJS其中就有對寫模塊的標準化。固然模塊化只是其中的一部分而已。
具體來講Node.js實現了:
在模塊化方面,它實現了Modules/1.0(已經更新到1.1.1),如下是node中是寫模塊的一個示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
MATH.JS
exports.add = function() {
var sum = 0, i = 0, args = arguments, l = args.length;
while (i < l) {
sum += args[i++];
}
return sum;
};
INCREMENT.JS
var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
};
MAIN.JS
var inc = require('increment').increment;
var a = 1;
inc(a); // 2
|
這就寫了一個math、increment、main模塊。math提供了add方法來實現數字相加。increment模塊依賴於math模塊,它提供increment方法實現相加。main獲取到increment方法,執行相加操做。
以上代碼示例能夠看到:
CommonJS module基本要求以下:
Modules/1.1較1.0僅增長了標示符module,require函數增長了main和paths屬性。而仔細比對1.1與1.1.1後發現除了格式調整了下幾乎沒有變化。
前面提到Node.js有一套簡潔的格式寫模塊,它遵循的就是 Moudles。
瀏覽器裏的JavaScript呢? 儘管語言自己暫不支持模塊(ES6打算支持),但能夠用現有的API包裝一個寫法出來。
毫無疑問,首先想到的是Node.js的Modules格式,它是最好的效仿對象。由於先後端有一個統一的方式寫JS模塊豈不樂哉!
但一開始就碰到一些難題:
服務器端JS模塊文件就在本地,瀏覽器端則須要經過網絡請求。
服務器端能夠很容易的實現同步或異步請求模塊,瀏覽器端則問題多多。
以下。
1
2
3
4
|
var event = require("event");
event.bind(el, 'click', function() {
// todo
});
|
這段代碼中require若是是異步執行的,則event.bind的執行有可能會出錯。
那實現同步的require不就好了嗎?的確可使用 XHR 實現同步載入模塊JS文件。但XHR的缺點也是明顯的,它不能跨域,這點讓人很難接受,由於有些場景須要模塊部署在不一樣的服務器。
那隻能經過script tag來實現模塊加載了!但script tag默認就是異步的,要實現Node.js的如出一轍風格(Modules)很難,幾乎是不可能。
這時,「救世主」出現了:Modules/Wrappings ,顧名思義包裹的模塊。該規範約定以下:
描述有拗口,代碼卻很簡單,使用了一個function包裹模塊(Node.js模塊則無需包裹)。
1
2
3
4
5
6
7
8
9
10
11
|
一個基本的模塊定義
module.declare(function(require, exports, module)
{
exports.foo = "bar";
});
直接使用對象做爲模塊
module.declare(
{
foo: "bar"
});
|
Modules/Wrappings的出現使得瀏覽器中實現它變得可能,包裹的函數做爲回調。即便用script tag做爲模塊加載器,script徹底下載後去回調,回調中進行模塊定義。
好了,截止目前咱們已經看到了兩種風格的模塊定義:Modules 和 Modules/Wrappings。
CommonJS Modules有1.0、1.一、1.1.1三個版本:
Node.js、SproutCore實現了 Modules 1.0
SeaJS、AvocadoDB、CouchDB等實現了Modules 1.1.1
SeaJS、FlyScript實現了Modules/Wrappings
注意:
SeaJS未實現所有的 Modules 1.1.1,如require函數的main,paths屬性在SeaJS中沒有。但SeaJS給require添加了async、resolve、load、constructor。
SeaJS沒有使用 Modules/Wrappings 中的module.declare定義模塊,而是使用define函數(看起來象AMD中的define,實則否則)。
前面提到,爲實現與Node.js相同方式的模塊寫法,大牛們作了不少努力。
但瀏覽器環境不一樣於服務器端,它的模塊有一個HTTP請求過程(而Node.js的模塊文件就在本地),這個請求過程多數使用script tag,script 默認的異步性致使很難實現與Node.js如出一轍的模塊格式。
Modules/Wrappings 使得實現變爲現實。雖然和Node.js的模塊寫法不徹底一致,但也有不少類似之處,使得熟悉Node.js的程序員有一些親切感。
但Node.js終究是服務器端的JavaScript,沒有必要把這些條條框框放到瀏覽器JavaScript環境中。
這時AMD 誕生了,它的全稱爲異步模塊定義。從名稱上看便知它是適合script tag的。也能夠說AMD是專門爲瀏覽器中JavaScript環境設計的規範。它吸收了CommonJS的一些優勢,但又不照搬它的格式。開始AMD做爲CommonJS的transport format 存在,因沒法與CommonJS開發者達成一致而獨立出來。它有本身的wiki 和討論組 。
AMD設計出一個簡潔的寫模塊API:
define(id?, dependencies?, factory);
其中:
id: 模塊標識,能夠省略。
dependencies: 所依賴的模塊,能夠省略。
factory: 模塊的實現,或者一個JavaScript對象。
特別指出,id遵循CommonJS Module Identifiers 。dependencies元素的順序和factory參數一一對應。
如下是使用AMD模式開發的簡單三層結構(基礎庫/UI層/應用層),用於展現模塊的五種寫法:
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
|
BASE.JS
define(function() {
return {
mix: function(source, target) {
}
};
});
UI.JS
define(['base'], function(base) {
return {
show: function() {
// todo with module base
}
}
});
PAGE.JS
define(['data', 'ui'], function(data, ui) {
// init here
});
DATA.JS
define({
users: [],
members: []
});
|
以上同時演示了define的前三種用法。細心的會發現,還有兩種沒有出現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
具名模塊
define('index', ['data','base'], function(data, base) {
// todo
});
包裝模塊
define(function(require, exports, module) {
var base = require('base');
exports.show = function() {
// todo with module base
}
});
|
若是不考慮多了一層函數外,格式和Node.js是同樣的:使用require獲取依賴模塊,使用exports導出API。
除了define外,AMD還保留一個關鍵字require。require 做爲規範保留的全局標識符,能夠實現爲 module loader,也能夠不實現。
目前,實現AMD的庫有RequireJS 、curl 、Dojo 、bdLoad、JSLocalnet 、Nodules 等。也有不少庫支持AMD規範,即將本身做爲一個模塊存在,如MooTools 、jQuery 、qwery 、bonzo 甚至還有 firebug 。
UMD是AMD 和CommonJS的糅合,前面花了很長的篇幅介紹了兩大類模塊規範,CommonJS(Modules/Modules/Wrappings)及AMD。
咱們知道Modules/Wrappings是出於對Node.js模塊格式的偏好而包裝下使其在瀏覽器中得以實現。而Modules/Wrappings的格式經過某些工具(如r.js)也能運行在Node.js中。事實上,這兩種格式同時有效且都被普遍使用。
AMD以瀏覽器爲第一(browser-first)的原則發展,選擇異步加載模塊。它的模塊支持對象(objects)、函數(functions)、構造器(constructors)、字符串(strings)、JSON等各類類型的模塊。所以在瀏覽器中它很是靈活。
CommonJS module以服務器端爲第一(server-first)的原則發展,選擇同步加載模塊。它的模塊是無需包裝的(unwrapped modules)且貼近於ES.next/Harmony的模塊格式。但它僅支持對象類型(objects)模塊。這迫使一些人又想出另外一個更通用格式 UMD(Universal Module Definition)。但願提供一個先後端跨平臺的解決方案。
UMD的實現很簡單,先判斷是否支持Node.js模塊格式(exports是否存在),存在則使用Node.js模塊格式。接着判斷是否支持AMD(define是否存在),存在則使用AMD方式加載模塊。前兩個都不存在,則將模塊公開到全局(window或global)。下面是一個示例:
下面是一個示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
EVENTUTIL.JS
(function (root, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
root.eventUtil = factory();
}
})(this, function() {
// module
return {
addEvent: function(el, type, handle) {
//...
},
removeEvent: function(el, type, handle) {
},
};
});
|
雖然UMD八字尚未一撇,有些開源庫卻開始支持UMD了,如大名鼎鼎的《JavaScript設計模式》做者Dustin Diaz開發的qwery。代碼以下:
1
2
3
4
5
6
7
8
9
|
!function(name,definition){
if(typeof module != 'function') module.exports = definition()
else if (typeof define == 'function' && typeof define.amd == 'object') define(definition)
else this(name) = definition()
}('query',function(){
var doc =document
, html = doc.documentElement
// ...
})
|
ECMAScript的下一個版本Harmony已經考慮到了模塊化的需求。
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
35
36
37
38
39
40
41
42
43
|
使用
module關鍵字來定義一個模塊
module math {
export function sum(x, y) {
return x + y;
}
export var pi = 3.141593;
}
使用
import關鍵字來加載外部模塊
// we can import in script code, not just inside a module
import {sum, pi} from math;
alert("2π = " + sum(pi, pi));
引入全部
API
// import everything
import * from math;
alert("2π = " + sum(pi, pi));
局部重命名
import { draw: drawShape } from shape;
import { draw: drawGun } from cowboy;
嵌套模塊
module widgets {
export module button { ... }
export module alert { ... }
export module textarea { ... }
...
}
import { messageBox, confirmDialog } from widgets.alert;
…
從服務器上請求的模塊
<script type=」harmony」>
// loading from a URL
module JSON at 'http://json.org/modules/json2.js';
alert(JSON.stringify({'hi': ‘world'}));
動態載入一個模塊
Loader.load('http://json.org/modules/json2.js', function(JSON) {
alert(JSON.stringify([0, {a: true}]));
});
|
ES6 modules還須要很長時間來規範化,可謂任重而道遠。且它有個問題,即新的語法關鍵字不能向下兼容(如低版本IE瀏覽器)。