參考資料
Modules/1.0——維基百科
CommonJS Modules/1.0——伯樂在線
js模塊化——博客園
Javascript模塊化編程系列——阮一峰
《ECMAScript 6 入門》——阮一峰
前言
本人菜鳥,入IT只為當(dāng)鼓勵(lì)師俊柔。本編文章意在簡單總結(jié)一下 什么是模塊化围详,模塊化的優(yōu)點(diǎn), js模塊化 的發(fā)展歷史辖佣,關(guān)于 js模塊化 的一些規(guī)范 等等。
一、什么是模塊化
根據(jù)百度百科說法:模塊化是指解決一個(gè)復(fù)雜問題時(shí)自頂向下逐層把系統(tǒng)劃分成若干模塊的過程铸鹰,有多種屬性年缎,分別反映其內(nèi)部特性悔捶。
暈了,這是什么嘛单芜。
簡單的說就是蜕该,我們實(shí)現(xiàn)一個(gè)應(yīng)用時(shí)(不管是web、桌面還是移動(dòng)端)洲鸠,通常都會(huì)按照不同的功能堂淡,分割成不同的模塊來編寫,編寫完之后按照某種方式組裝起來成為一個(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等等編程語言本身就擁有可以實(shí)現(xiàn)模塊化的指令或方法,有了這些指令或方法梗顺,就可以把子功能寫在另外的文件上泡孩,需要用到的時(shí)候直接引入即可。舉下例子:
- c使用 #include 包含.h文件
- php中使用 require_once 包含.php文件
- java使用 import 導(dǎo)入包
拋開C寺谤、C++仑鸥、Java、PHP這些不說变屁,就說前端領(lǐng)域眼俊,認(rèn)真想想,其實(shí) html css 也實(shí)現(xiàn)了模塊化粟关。
-
html 中的 <frame> <iframe> <frameset>(但好像不推薦使用)
- css 中有 @import " /.css " 指令可以導(dǎo)入其他css
那 JavaScript 呢疮胖?帶著疑問,下面會(huì)介紹js模塊化的發(fā)展歷程闷板。(大神請(qǐng)無視)
二澎灸、模塊化的優(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í)它只是作為一種客戶端腳本語言艺玲,目的是 將 不涉及后端數(shù)據(jù)的、簡單的 表單有效性驗(yàn)證 轉(zhuǎn)移到客戶端完成鞠抑,減少客戶端向服務(wù)端的請(qǐng)求數(shù)饭聚。那時(shí)的JavaScript只是服務(wù)端工程師在使用,他們或許只需在頁面上隨便寫幾句js代碼就能滿足需求搁拙。
if (xxx) {
// ......
} else {
// ......
}
element.onsubmit= function () {
//......
}
代碼可能像這樣子秒梳,從上到下執(zhí)行就行了法绵,沒有什么模塊的規(guī)范。
3.2 模塊萌芽
隨著ajax的概念被提出酪碘,前端有了主動(dòng)發(fā)起請(qǐng)求的能力朋譬,一些業(yè)務(wù)開始向客戶端方向偏移。網(wǎng)站逐漸變成“互聯(lián)網(wǎng)應(yīng)用程序”兴垦,嵌入網(wǎng)頁的Javascript代碼越來越龐大徙赢,越來越復(fù)雜。于是探越,一些問題就暴漏出來了:
-
依賴關(guān)系不好管理狡赐。如果一個(gè)文件需要依賴另外一些文件中定義的東西時(shí),這個(gè)文件依賴的所有文件都要在它之前導(dǎo)入钦幔。過于復(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中修改過a的值。于是if (a==0) { // ...... }
食茎。(出事了5倨啤)命名沖突
項(xiàng)目中通常會(huì)把一些通用的函數(shù)封裝成一個(gè)文件。
我定義了一個(gè)函數(shù):function func ( // ...... ) { }
你也想實(shí)現(xiàn)類似功能别渔,于是:function func2 ( // ...... ) { }
他又想實(shí)現(xiàn)類似功能附迷,于是:function func3 ( // ...... ) { }
要避免命名沖突,只能靠你我他之間的溝通協(xié)作哎媚。
如果放著這些問題不解決喇伯,團(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í)(幾十人甚至上千人一起開發(fā)同一個(gè)項(xiàng)目),溝通就變得非常困難且低效了买喧。
于是捻悯,前人創(chuàng)造了很多方法來避免這些問題匆赃,盡最大的努力實(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) {
// 通過給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)格雖然靈活了些唠倦,但并未解決根本問題:所需依賴還是得外部提前提供称鳞、還是增加了全局變量。
3.2.2 避免命名沖突的方法
java風(fēng)格的命名空間稠鼻,用多級(jí)命名空間來進(jìn)行管理冈止。于是編寫代碼和調(diào)用代碼就變得這么長了。
Myapp.utils.func1 = xxx;
Myapp.tools.func1 = xxx;
Myapp.tools.another.func1 = xxx;-
設(shè)置變量名的控制權(quán)讓渡函數(shù)候齿。
有時(shí)候我們可能不只用到一種函數(shù)庫或插件熙暴,當(dāng)用到多個(gè)函數(shù)庫時(shí),由于庫并不是一個(gè)人編寫的慌盯,全局變量的命名沖突不是總能避免周霉。如:jquery.js庫 和 Prototype.js庫,它們都用了$符號(hào)作為全局變量亚皂。同時(shí)導(dǎo)入兩個(gè)庫肯定會(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è)問題,這個(gè)后續(xù)再說禁漓。
3.2.4 推薦
想了解更多實(shí)現(xiàn)模塊化的方法跟衅,可以拜讀一下峰哥的文章:
Javascript模塊化編程(一):模塊的寫法
3.2.5 模塊化問題
當(dāng)人們覺得再這樣下去寫代碼槽糕透了的時(shí)候,他們就想運(yùn)用模塊化的思想璃饱,寫好一個(gè)模塊与斤,要用就導(dǎo)入肪康,導(dǎo)入后毫不影響原先的代碼荚恶。這樣就引發(fā)很多需要思考的問題:
- 怎樣安全地包裝一個(gè)模塊的代碼撩穿?
- 怎樣唯一地標(biāo)識(shí)一個(gè)模塊?
- 怎樣優(yōu)雅地把模塊的API暴漏出去谒撼?
- 怎樣方便地使用所依賴的模塊食寡?
四、服務(wù)端 js 的誕生
4.1 nodejs
2009年廓潜,nodejs誕生抵皱,我們可以用 js 編寫服務(wù)端的代碼了。
在瀏覽器環(huán)境下辩蛋,沒有模塊也不是特別大的問題呻畸,畢竟網(wǎng)頁程序的復(fù)雜性有限;但是在服務(wù)器端悼院,一定要有模塊伤为,與操作系統(tǒng)和其他應(yīng)用程序互動(dòng),否則根本沒法編程据途。
于是绞愚,CommonJS 社區(qū)制定了 Modules/1.0 規(guī)范(現(xiàn)在已經(jīng)被1.1取代)。nodejs 采用了該規(guī)范颖医,故以下用 nodejs 作為例子位衩。
4.2 Modules/1.0
總結(jié)起來,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。無線循環(huán)爹殊,這就產(chǎn)生了依賴閉環(huán)的問題)蜕乡,為了避免這個(gè)問題,規(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ì)象來作為輸出的唯一表示
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í)符來進(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)速的快慢乏苦。若要等很長時(shí)間,瀏覽器會(huì)處于"假死"狀態(tài)尤筐。例如:
// 第二行math.add(1, 1)汇荐,在第一行require('math')之后運(yùn)行,因此必須等math.js加載完成盆繁。
// 如果加載時(shí)間很長掀淘,整個(gè)應(yīng)用就會(huì)停在那里等。
var math = require('./math.js');
math.add(1, 1);
因此油昂,瀏覽器端的模塊革娄,不能采用 "同步加載"(Sync)倾贰,只能采用 "異步加載"(Async)。這就是 AMD規(guī)范(后面提及)誕生的背景拦惋。 - 若瀏覽器加載資源的方式外層沒有 function 包裹躁染,變量會(huì)暴漏在全局上;而全局污染這個(gè)問題在服務(wù)端編程不如瀏覽器要求嚴(yán)格架忌。例如:
// 變量math 和 math.js中定義在全局作用域上的變量、方法 都會(huì)污染到全局我衬。
var math = require('./math.js');
既然如此叹放,問題要怎么解決?于是乎挠羔,就像黨派斗爭一樣井仰,分裂了三種解決方案。
5.1 Modules/1.x
這一派人的意見是:
- 在現(xiàn)有基礎(chǔ)上改進(jìn)來滿足瀏覽器端的需要(function包裝不污染全局破加、異步加載)俱恶。所以,他們制定了 Modules/Transport規(guī)范范舀,提出:先通過工具合是,把現(xiàn)有模塊代碼轉(zhuǎn)化為瀏覽器上使用的模塊代碼,然后再使用的方案锭环。
典型的工具有:browserify聪全。Browserify 可以讓你使用類似于 node 的 require() 的方式來組織瀏覽器端的 Javascript 代碼,通過 預(yù)編譯 讓前端 Javascript 可以直接使用 Node NPM 安裝的一些庫辅辩。難懂难礼,那就直接看它的例子吧:
所以,若采用這一派的規(guī)范玫锋,我們就可以直接像服務(wù)端一樣編寫代碼了蛾茉,編寫完后,只需要用工具把它編譯成瀏覽器使用的代碼即可撩鹿。
5.2 Modules/2.0
這一派人的意見是:
- Modules/1.0固然不適合瀏覽器谦炬,但它里面的一些理念還是很好的,如:通過 require 來聲明依賴节沦。新的規(guī)范應(yīng)該兼容這些吧寺。
- AMD規(guī)范(請(qǐng)看 5.3) 也有它好的地方,如:模塊的預(yù)先加載散劫、通過
return 可暴漏任意類型的數(shù)據(jù)稚机,而不像 commonjs 那樣 exports 只能為
object。故 其中的一些觀點(diǎn) 也應(yīng)采納获搏。 - 最終他們制定了一個(gè) Modules/Wrappings規(guī)范赖条,此規(guī)范指出了一個(gè)模塊應(yīng)該如何"包裝"失乾,包含以下內(nèi)容:
① 全局有一個(gè) module 變量,用來定義模塊纬乍。
② 通過module.declare方法來定義一個(gè)模塊碱茁。
③ module.declare方法 只接收一個(gè)參數(shù),那就是模塊的 factory仿贬,它可以是函數(shù)纽竣,也可以是對(duì)象(如果是對(duì)象,那么模塊輸出就是此對(duì)象)茧泪。
④ 模塊的 factory函數(shù) 傳入三個(gè)參數(shù):require蜓氨、exports、module队伟,用來引入其他依賴和導(dǎo)出本模塊API穴吹。
⑤ 如果 factory函數(shù) 最后明確寫有return數(shù)據(jù),那么 return 的內(nèi)容即為模塊的輸出嗜侮;不寫 return 默認(rèn)返回undefined港令。
CMD/seajs
seajs 的作者 是 國內(nèi)大牛 淘寶前端步道者 玉伯。seajs 全面擁抱
Modules/Wrappings規(guī)范锈颗,不用 RequireJS 那樣回調(diào)的方式來編寫模塊顷霹。
它的特色和用法以后再來補(bǔ)充。(待續(xù))
5.3 Modules/Async
這一派人的意見是:
瀏覽器與服務(wù)器環(huán)境差別太大击吱,不能沿用舊的模塊標(biāo)準(zhǔn)泼返。
-
既然瀏覽器必須異步加載代碼,那么模塊在定義的時(shí)候就必須 指明所依賴的模塊姨拥,然后 把本模塊的代碼寫在回調(diào)函數(shù)里绅喉。模塊的加載也是通過 下載—>回調(diào) 這樣的過程來進(jìn)行,這個(gè)思想就是AMD的基礎(chǔ)叫乌。
// AMD也采用require()語句加載模塊柴罐,但是不同于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庫實(shí)現(xiàn)了AMD規(guī)范:require.js 和 curl.js寞奸。
AMD/RequireJs
這里主要介紹 RequireJs呛谜,若想了解其用法,可以看我的另一篇文章:AMD/RequireJS 使用入門枪萄。
六隐岛、ES6模塊化標(biāo)準(zhǔn)
既然模塊化開發(fā)的呼聲這么高,作為官方的ECMA必然要有所行動(dòng)瓷翻,js模塊化很早就列入草案聚凹,終于在2015年6月份發(fā)布了ES6正式版。
ES6只要增加了 export
齐帚、import
妒牙、module
等命令。具體用法以后再補(bǔ)充童谒。
想了解更多關(guān)于ES6的東西,推薦大家閱讀《ECMAScript 6 入門》沪羔,這是這本書的 網(wǎng)上教程饥伊。