參考資料
Modules/1.0——維基百科
CommonJS Modules/1.0——伯樂(lè)在線
js模塊化——博客園
Javascript模塊化編程系列——阮一峰
《ECMAScript 6 入門》——阮一峰
前言
本人菜鳥(niǎo),入IT只為當(dāng)鼓勵(lì)師。本編文章意在簡(jiǎn)單總結(jié)一下 什么是模塊化,模塊化的優(yōu)點(diǎn), js模塊化 的發(fā)展歷史,關(guān)于 js模塊化 的一些規(guī)范 等等。
一、什么是模塊化
根據(jù)百度百科說(shuō)法:模塊化是指解決一個(gè)復(fù)雜問(wèn)題時(shí)自頂向下逐層把系統(tǒng)劃分成若干模塊的過(guò)程,有多種屬性,分別反映其內(nèi)部特性。
暈了,這是什么嘛。
簡(jiǎn)單的說(shuō)就是,我們實(shí)現(xiàn)一個(gè)應(yīng)用時(shí)(不管是web、桌面還是移動(dòng)端),通常都會(huì)按照不同的功能,分割成不同的模塊來(lái)編寫,編寫完之后按照某種方式組裝起來(lái)成為一個(gè)整體,最終實(shí)現(xiàn)整個(gè)系統(tǒng)的功能。
所以,如果一個(gè)團(tuán)隊(duì)一起做一個(gè)復(fù)雜的應(yīng)用,肯定要分模塊分工合作(一個(gè)人戰(zhàn)斗不太現(xiàn)實(shí))。這時(shí),有很多需要注意的點(diǎn)就出現(xiàn)了:
- 模塊中定義的資源不應(yīng)該污染全局環(huán)境,否則多人協(xié)作困難且容易出錯(cuò)。
- 各個(gè)模塊可獨(dú)立工作,即便單組模塊出現(xiàn)故障也不影響整個(gè)系統(tǒng)工作。
- 各模塊不能全部預(yù)先加載,應(yīng)該實(shí)現(xiàn)按需自動(dòng)加載。確保每個(gè)模塊高效運(yùn)行,又能節(jié)約資源,提高效率。
C、C++、Java、PHP等等編程語(yǔ)言本身就擁有可以實(shí)現(xiàn)模塊化的指令或方法,有了這些指令或方法,就可以把子功能寫在另外的文件上,需要用到的時(shí)候直接引入即可。舉下例子:
- c使用 #include 包含.h文件
- php中使用 require_once 包含.php文件
- java使用 import 導(dǎo)入包
拋開(kāi)C、C++、Java、PHP這些不說(shuō),就說(shuō)前端領(lǐng)域,認(rèn)真想想,其實(shí) html css 也實(shí)現(xiàn)了模塊化。
-
html 中的 <frame> <iframe> <frameset>(但好像不推薦使用)
- css 中有 @import " /.css " 指令可以導(dǎo)入其他css
那 JavaScript 呢?帶著疑問(wèn),下面會(huì)介紹js模塊化的發(fā)展歷程。(大神請(qǐng)無(wú)視)
二、模塊化的優(yōu)點(diǎn)
可維護(hù)性:
- 多人協(xié)作互不干擾
- 靈活架構(gòu),焦點(diǎn)分離
- 方便模塊間組合、分解 、解耦
- 方便單個(gè)模塊功能調(diào)試、升級(jí)
可測(cè)試性:
- 可分單元測(cè)試
三、前端的模塊化思想的發(fā)展
3.1 那年的誕生——1995
1995年,JavaScript正式發(fā)布,當(dāng)時(shí)它只是作為一種客戶端腳本語(yǔ)言,目的是 將 不涉及后端數(shù)據(jù)的、簡(jiǎn)單的 表單有效性驗(yàn)證 轉(zhuǎn)移到客戶端完成,減少客戶端向服務(wù)端的請(qǐng)求數(shù)。那時(shí)的JavaScript只是服務(wù)端工程師在使用,他們或許只需在頁(yè)面上隨便寫幾句js代碼就能滿足需求。
if (xxx) {
// ......
} else {
// ......
}
element.onsubmit= function () {
//......
}
代碼可能像這樣子,從上到下執(zhí)行就行了,沒(méi)有什么模塊的規(guī)范。
3.2 模塊萌芽
隨著ajax的概念被提出,前端有了主動(dòng)發(fā)起請(qǐng)求的能力,一些業(yè)務(wù)開(kāi)始向客戶端方向偏移。網(wǎng)站逐漸變成“互聯(lián)網(wǎng)應(yīng)用程序”,嵌入網(wǎng)頁(yè)的Javascript代碼越來(lái)越龐大,越來(lái)越復(fù)雜。于是,一些問(wèn)題就暴漏出來(lái)了:
-
依賴關(guān)系不好管理。如果一個(gè)文件需要依賴另外一些文件中定義的東西時(shí),這個(gè)文件依賴的所有文件都要在它之前導(dǎo)入。過(guò)于復(fù)雜的系統(tǒng),依賴關(guān)系可能出現(xiàn)相互交叉的情況,依賴關(guān)系的管理就更加難了。
// 如果main.js中要用到gameBg.js中定義的屬性、方法或者對(duì)象時(shí)// 正確,gameBg.js要在main.js之前導(dǎo)入 <script src="scripts/views/gameBg.js" type="text/javascript"> <script src="scripts/main.js" type="text/javascript"> // 報(bào)錯(cuò),cannot find xxx of undefined <script src="scripts/views/gameBg.js" type="text/javascript"> <script src="scripts/main.js" type="text/javascript"> // 如果js文件很多呢?
全局環(huán)境的污染。
我在a.js中定義了一個(gè)全局變量var a = 0
,相當(dāng)于定義在window上。
你在b.js中用了我定義的全局變量,給它賦值a = 1
。
我又在c.js中用了這個(gè)全局變量,但我不知道你在b.js中修改過(guò)a的值。于是if (a==0) { // ...... }
。(出事了!)命名沖突
項(xiàng)目中通常會(huì)把一些通用的函數(shù)封裝成一個(gè)文件。
我定義了一個(gè)函數(shù):function func ( // ...... ) { }
你也想實(shí)現(xiàn)類似功能,于是:function func2 ( // ...... ) { }
他又想實(shí)現(xiàn)類似功能,于是:function func3 ( // ...... ) { }
要避免命名沖突,只能靠你我他之間的溝通協(xié)作。
如果放著這些問(wèn)題不解決,團(tuán)隊(duì)的工作重點(diǎn)與關(guān)注點(diǎn)就不只是系統(tǒng)的業(yè)務(wù)邏輯,還包括隊(duì)內(nèi)的溝通,這會(huì)阻礙著項(xiàng)目進(jìn)度。而且當(dāng)人數(shù)一多時(shí)(幾十人甚至上千人一起開(kāi)發(fā)同一個(gè)項(xiàng)目),溝通就變得非常困難且低效了。
于是,前人創(chuàng)造了很多方法來(lái)避免這些問(wèn)題,盡最大的努力實(shí)現(xiàn)模塊化:
3.2.1 避免全局環(huán)境污染的方法
只創(chuàng)建一個(gè)全局變量作為當(dāng)前應(yīng)用的容器,把其他變量、方法加到該命名空間下。
var Myapp = {};
Myapp.location = "login";
Myapp.info = {
name: "flappybird",
creator: "Dong Nguyen"
};
Myapp.startGame = function () {
// ......
};將代碼寫在一個(gè)匿名函數(shù)內(nèi)部
( function () {
// 局部變量和方法
var variable1 = "I'm a variable in part";
var func1 = function () {
// ......
};
// 全局變量和方法
window.variable2 = "I'm a variable in global";
window.func2 = function () {
// ......
};
})();-
jquery風(fēng)格匿名函數(shù)
( function (window) {
// 通過(guò)給window添加屬性而暴漏到全局
window.jQuery = window.$ = jQuery;// 定義全局對(duì)象jQuery($)的相關(guān)內(nèi)容 })(window);
jQuery的封裝風(fēng)格曾被很多框架模仿。
這種方式用到了匿名函數(shù)包裝代碼(即第二種方法)。多出的點(diǎn)是,所依賴的外部變量可以傳給這個(gè)函數(shù),在函數(shù)內(nèi)部就可以使用這些依賴了,然后把模塊自身暴漏給window。
如果需要添加擴(kuò)展,則可以作為jQuery的插件,把它掛載到$上。例如:fullpage.js插件。
這種風(fēng)格雖然靈活了些,但并未解決根本問(wèn)題:所需依賴還是得外部提前提供、還是增加了全局變量。
3.2.2 避免命名沖突的方法
java風(fēng)格的命名空間,用多級(jí)命名空間來(lái)進(jìn)行管理。于是編寫代碼和調(diào)用代碼就變得這么長(zhǎng)了。
Myapp.utils.func1 = xxx;
Myapp.tools.func1 = xxx;
Myapp.tools.another.func1 = xxx;-
設(shè)置變量名的控制權(quán)讓渡函數(shù)。
有時(shí)候我們可能不只用到一種函數(shù)庫(kù)或插件,當(dāng)用到多個(gè)函數(shù)庫(kù)時(shí),由于庫(kù)并不是一個(gè)人編寫的,全局變量的命名沖突不是總能避免。如:jquery.js庫(kù) 和 Prototype.js庫(kù),它們都用了$符號(hào)作為全局變量。同時(shí)導(dǎo)入兩個(gè)庫(kù)肯定會(huì)產(chǎn)生影響。
但是jquery提供了noConflict()方法,可以讓渡變量名的控制權(quán)。
// 將變量$的控制權(quán)讓渡給prototype.js
jQuery.noConflict();
// 使用jQuery
jQuery("h1").text("我是標(biāo)題");// 自定義一個(gè)更短的命名 var jq = jQuery.noConflict(); jq("p").text("我是段落");
3.2.3 完善依賴關(guān)系的管理
后面提到的 require.js、sea.js 等 可以解決這個(gè)問(wèn)題,這個(gè)后續(xù)再說(shuō)。
3.2.4 推薦
想了解更多實(shí)現(xiàn)模塊化的方法,可以拜讀一下峰哥的文章:
Javascript模塊化編程(一):模塊的寫法
3.2.5 模塊化問(wèn)題
當(dāng)人們覺(jué)得再這樣下去寫代碼槽糕透了的時(shí)候,他們就想運(yùn)用模塊化的思想,寫好一個(gè)模塊,要用就導(dǎo)入,導(dǎo)入后毫不影響原先的代碼。這樣就引發(fā)很多需要思考的問(wèn)題:
- 怎樣安全地包裝一個(gè)模塊的代碼?
- 怎樣唯一地標(biāo)識(shí)一個(gè)模塊?
- 怎樣優(yōu)雅地把模塊的API暴漏出去?
- 怎樣方便地使用所依賴的模塊?
四、服務(wù)端 js 的誕生
4.1 nodejs
2009年,nodejs誕生,我們可以用 js 編寫服務(wù)端的代碼了。
在瀏覽器環(huán)境下,沒(méi)有模塊也不是特別大的問(wèn)題,畢竟網(wǎng)頁(yè)程序的復(fù)雜性有限;但是在服務(wù)器端,一定要有模塊,與操作系統(tǒng)和其他應(yīng)用程序互動(dòng),否則根本沒(méi)法編程。
于是,CommonJS 社區(qū)制定了 Modules/1.0 規(guī)范(現(xiàn)在已經(jīng)被1.1取代)。nodejs 采用了該規(guī)范,故以下用 nodejs 作為例子。
4.2 Modules/1.0
總結(jié)起來(lái),Modules/1.0規(guī)范指出:
- 模塊需要提供頂級(jí)作用域的私有性。
- 提供從其他模板導(dǎo)入單例對(duì)象到自身的能力
- 提供導(dǎo)出自身API的能力
Modules/1.0規(guī)范的內(nèi)容如下:
4.2.1 模塊上下文
-
在模塊中存在一個(gè)自由變量"require",它是一個(gè)函數(shù)。這個(gè)"require"函數(shù):
① 接收參數(shù)為:一個(gè)模塊標(biāo)識(shí)符。
var example = require('./example.js');
② 返回:外部模塊輸出的API。
// 變量example即為外部模塊example.js輸出的內(nèi)容
③ 如果出現(xiàn)依賴閉環(huán)(正常情況,加載main.js時(shí),遇到var a = require(./a.js);
則去加載a.js;加載a.js時(shí),遇到var b = require(./b.js);
則去加載b.js;加載b.js時(shí),遇到var a = require(./a.js);
則去加載a.js。無(wú)線循環(huán),這就產(chǎn)生了依賴閉環(huán)的問(wèn)題),為了避免這個(gè)問(wèn)題,規(guī)定每個(gè)模塊只會(huì)被加載執(zhí)行一次。
// main.js
console.log("main start");
var a = require(./a.js);
var b = require(./b.js);
console.log("main end");// a.js console.log("a start"); var b = require(./b.js); console.log("a end"); // b.js console.log("b start"); var a = require(./a.js); console.log("b end"); /* 輸出結(jié)果為: main start a start b start b end a end */
④ 如果請(qǐng)求模塊失敗,require函數(shù)應(yīng)拋出一個(gè)錯(cuò)誤。
- 模塊中存在一個(gè)名為"exports"的自由變量,它是一個(gè)對(duì)象,模板可把自身API加到其中。
// 暴露message變量
exports.message = "hi";
// 暴露hello方法
exports.say= function () {
console.log("hello!");
}; - 模塊必須使用"exports"對(duì)象來(lái)作為輸出的唯一表示
4.2.2 模塊標(biāo)識(shí)符
- 模塊標(biāo)識(shí)符是一個(gè)以正斜杠分隔的多個(gè)”term”組成的字符串。
- 一個(gè)term必須是一個(gè) 駝峰格式的標(biāo)識(shí)符,
.
字符(表示當(dāng)前目錄) 或者..
字符串(表示上一級(jí)目錄)。 - 模塊標(biāo)識(shí)符可以不加文件擴(kuò)展名,比如”.js”。
var a = require(./a);
// 相當(dāng)于 var a = require(./a.js); - 模塊標(biāo)識(shí)符可以是 相對(duì)的 或者 頂級(jí)的 (top-level)。如果一個(gè)模塊標(biāo)識(shí)符的第一個(gè)term是
.
字符(表示當(dāng)前目錄)或者..
字符串(表示上一級(jí)目錄),那么它是 相對(duì)的 。 - 頂級(jí)標(biāo)識(shí)符是概念上的模塊命名空間的根。
- 相對(duì)標(biāo)識(shí)符是相對(duì)于在其內(nèi)部調(diào)用了
require()
的模塊的標(biāo)識(shí)符來(lái)進(jìn)行解析的。
五、服務(wù)端的模塊化在前端領(lǐng)域的應(yīng)用
既然服務(wù)端出了模塊化方案 Modules/1.0 ,那么是不是可以把這個(gè)規(guī)范直接用在客戶端啊?
只可惜,不能。出于以下原因:
- 資源的加載方式與服務(wù)端完全不同。
① 服務(wù)端require
一個(gè)模塊,是直接從 硬盤 或 內(nèi)存 中讀取的。可以同步加載完成,等待時(shí)間就是硬盤的讀取時(shí)間,那速度是很快的。
② 客戶端,瀏覽器需要從服務(wù)端下載資源,花費(fèi)的是請(qǐng)求所花的時(shí)間,取決于網(wǎng)速的快慢。若要等很長(zhǎng)時(shí)間,瀏覽器會(huì)處于"假死"狀態(tài)。例如:
// 第二行math.add(1, 1),在第一行require('math')之后運(yùn)行,因此必須等math.js加載完成。
// 如果加載時(shí)間很長(zhǎng),整個(gè)應(yīng)用就會(huì)停在那里等。
var math = require('./math.js');
math.add(1, 1);
因此,瀏覽器端的模塊,不能采用 "同步加載"(Sync),只能采用 "異步加載"(Async)。這就是 AMD規(guī)范(后面提及)誕生的背景。 - 若瀏覽器加載資源的方式外層沒(méi)有 function 包裹,變量會(huì)暴漏在全局上;而全局污染這個(gè)問(wèn)題在服務(wù)端編程不如瀏覽器要求嚴(yán)格。例如:
// 變量math 和 math.js中定義在全局作用域上的變量、方法 都會(huì)污染到全局。
var math = require('./math.js');
既然如此,問(wèn)題要怎么解決?于是乎,就像黨派斗爭(zhēng)一樣,分裂了三種解決方案。
5.1 Modules/1.x
這一派人的意見(jiàn)是:
- 在現(xiàn)有基礎(chǔ)上改進(jìn)來(lái)滿足瀏覽器端的需要(function包裝不污染全局、異步加載)。所以,他們制定了 Modules/Transport規(guī)范,提出:先通過(guò)工具,把現(xiàn)有模塊代碼轉(zhuǎn)化為瀏覽器上使用的模塊代碼,然后再使用的方案。
典型的工具有:browserify。Browserify 可以讓你使用類似于 node 的 require() 的方式來(lái)組織瀏覽器端的 Javascript 代碼,通過(guò) 預(yù)編譯 讓前端 Javascript 可以直接使用 Node NPM 安裝的一些庫(kù)。難懂,那就直接看它的例子吧:
所以,若采用這一派的規(guī)范,我們就可以直接像服務(wù)端一樣編寫代碼了,編寫完后,只需要用工具把它編譯成瀏覽器使用的代碼即可。
5.2 Modules/2.0
這一派人的意見(jiàn)是:
- Modules/1.0固然不適合瀏覽器,但它里面的一些理念還是很好的,如:通過(guò) require 來(lái)聲明依賴。新的規(guī)范應(yīng)該兼容這些。
- AMD規(guī)范(請(qǐng)看 5.3) 也有它好的地方,如:模塊的預(yù)先加載、通過(guò)
return 可暴漏任意類型的數(shù)據(jù),而不像 commonjs 那樣 exports 只能為
object。故 其中的一些觀點(diǎn) 也應(yīng)采納。 - 最終他們制定了一個(gè) Modules/Wrappings規(guī)范,此規(guī)范指出了一個(gè)模塊應(yīng)該如何"包裝",包含以下內(nèi)容:
① 全局有一個(gè) module 變量,用來(lái)定義模塊。
② 通過(guò)module.declare方法來(lái)定義一個(gè)模塊。
③ module.declare方法 只接收一個(gè)參數(shù),那就是模塊的 factory,它可以是函數(shù),也可以是對(duì)象(如果是對(duì)象,那么模塊輸出就是此對(duì)象)。
④ 模塊的 factory函數(shù) 傳入三個(gè)參數(shù):require、exports、module,用來(lái)引入其他依賴和導(dǎo)出本模塊API。
⑤ 如果 factory函數(shù) 最后明確寫有return數(shù)據(jù),那么 return 的內(nèi)容即為模塊的輸出;不寫 return 默認(rèn)返回undefined。
CMD/seajs
seajs 的作者 是 國(guó)內(nèi)大牛 淘寶前端步道者 玉伯。seajs 全面擁抱
Modules/Wrappings規(guī)范,不用 RequireJS 那樣回調(diào)的方式來(lái)編寫模塊。
它的特色和用法以后再來(lái)補(bǔ)充。(待續(xù))
5.3 Modules/Async
這一派人的意見(jiàn)是:
瀏覽器與服務(wù)器環(huán)境差別太大,不能沿用舊的模塊標(biāo)準(zhǔn)。
-
既然瀏覽器必須異步加載代碼,那么模塊在定義的時(shí)候就必須 指明所依賴的模塊,然后 把本模塊的代碼寫在回調(diào)函數(shù)里。模塊的加載也是通過(guò) 下載—>回調(diào) 這樣的過(guò)程來(lái)進(jìn)行,這個(gè)思想就是AMD的基礎(chǔ)。
// AMD也采用require()語(yǔ)句加載模塊,但是不同于CommonJS,它要求兩個(gè)參數(shù)
// 第一個(gè)參數(shù)[module],是一個(gè)數(shù)組,里面的成員就是要加載的模塊
// 第二個(gè)參數(shù)callback,則是加載成功之后的回調(diào)函數(shù)
require([module], callback);// math.add()與math模塊加載不是同步的,瀏覽器不會(huì)發(fā)生假死。AMD比較適合瀏覽器環(huán)境。 require(['math'], function (math) { math.add(2, 3); });
由于與原規(guī)范不合,最終從 CommonJs 中分裂了出去,獨(dú)立制定了瀏覽器端的js模塊化規(guī)范 AMD(Asynchronous Module Definition)。
目前,主要有兩個(gè)Javascript庫(kù)實(shí)現(xiàn)了AMD規(guī)范:require.js 和 curl.js。
AMD/RequireJs
這里主要介紹 RequireJs,若想了解其用法,可以看我的另一篇文章:AMD/RequireJS 使用入門。
六、ES6模塊化標(biāo)準(zhǔn)
既然模塊化開(kāi)發(fā)的呼聲這么高,作為官方的ECMA必然要有所行動(dòng),js模塊化很早就列入草案,終于在2015年6月份發(fā)布了ES6正式版。
ES6只要增加了 export
、import
、module
等命令。具體用法以后再補(bǔ)充。
想了解更多關(guān)于ES6的東西,推薦大家閱讀《ECMAScript 6 入門》,這是這本書的 網(wǎng)上教程。