淺談JavaScript 模塊化

參考資料

Modules/1.0——維基百科
CommonJS Modules/1.0——伯樂在線
js模塊化——博客園
Javascript模塊化編程系列——阮一峰
《ECMAScript 6 入門》——阮一峰

前言

本人菜鳥,入IT只為當鼓勵師。本編文章意在簡單總結一下 什么是模塊化,模塊化的優點, js模塊化 的發展歷史,關于 js模塊化 的一些規范 等等。

一、什么是模塊化

根據百度百科說法:模塊化是指解決一個復雜問題時自頂向下逐層把系統劃分成若干模塊的過程,有多種屬性,分別反映其內部特性。

暈了,這是什么嘛。

簡單的說就是,我們實現一個應用時(不管是web、桌面還是移動端),通常都會按照不同的功能,分割成不同的模塊來編寫,編寫完之后按照某種方式組裝起來成為一個整體,最終實現整個系統的功能。

所以,如果一個團隊一起做一個復雜的應用,肯定要分模塊分工合作(一個人戰斗不太現實)。這時,有很多需要注意的點就出現了:

  • 模塊中定義的資源不應該污染全局環境,否則多人協作困難且容易出錯。
  • 各個模塊可獨立工作,即便單組模塊出現故障也不影響整個系統工作。
  • 各模塊不能全部預先加載,應該實現按需自動加載。確保每個模塊高效運行,又能節約資源,提高效率。

C、C++、Java、PHP等等編程語言本身就擁有可以實現模塊化的指令或方法,有了這些指令或方法,就可以把子功能寫在另外的文件上,需要用到的時候直接引入即可。舉下例子:

  • c使用 #include 包含.h文件
  • php中使用 require_once 包含.php文件
  • java使用 import 導入包

拋開C、C++、Java、PHP這些不說,就說前端領域,認真想想,其實 html css 也實現了模塊化。

  • html 中的 <frame> <iframe> <frameset>(但好像不推薦使用)


  • css 中有 @import " /.css " 指令可以導入其他css

那 JavaScript 呢?帶著疑問,下面會介紹js模塊化的發展歷程。(大神請無視)

二、模塊化的優點

可維護性:

  • 多人協作互不干擾
  • 靈活架構,焦點分離
  • 方便模塊間組合、分解 、解耦
  • 方便單個模塊功能調試、升級

可測試性:

  • 可分單元測試

三、前端的模塊化思想的發展

3.1 那年的誕生——1995

1995年,JavaScript正式發布,當時它只是作為一種客戶端腳本語言,目的是 將 不涉及后端數據的、簡單的 表單有效性驗證 轉移到客戶端完成,減少客戶端向服務端的請求數。那時的JavaScript只是服務端工程師在使用,他們或許只需在頁面上隨便寫幾句js代碼就能滿足需求。

if (xxx) {
  // ......
} else {
  // ......
}
element.onsubmit= function () {
  //......
}

代碼可能像這樣子,從上到下執行就行了,沒有什么模塊的規范。

3.2 模塊萌芽

隨著ajax的概念被提出,前端有了主動發起請求的能力,一些業務開始向客戶端方向偏移。網站逐漸變成“互聯網應用程序”,嵌入網頁的Javascript代碼越來越龐大,越來越復雜。于是,一些問題就暴漏出來了:

  • 依賴關系不好管理。如果一個文件需要依賴另外一些文件中定義的東西時,這個文件依賴的所有文件都要在它之前導入。過于復雜的系統,依賴關系可能出現相互交叉的情況,依賴關系的管理就更加難了。
    // 如果main.js中要用到gameBg.js中定義的屬性、方法或者對象時

    // 正確,gameBg.js要在main.js之前導入
    <script src="scripts/views/gameBg.js" type="text/javascript">
    <script src="scripts/main.js" type="text/javascript">
    
    // 報錯,cannot find xxx of undefined
    <script src="scripts/views/gameBg.js" type="text/javascript">
    <script src="scripts/main.js" type="text/javascript">
    
    // 如果js文件很多呢?
    
  • 全局環境的污染。
    我在a.js中定義了一個全局變量 var a = 0,相當于定義在window上。
    你在b.js中用了我定義的全局變量,給它賦值 a = 1。
    我又在c.js中用了這個全局變量,但我不知道你在b.js中修改過a的值。于是 if (a==0) { // ...... }。(出事了?。?/p>

  • 命名沖突
    項目中通常會把一些通用的函數封裝成一個文件。
    我定義了一個函數:function func ( // ...... ) { }
    你也想實現類似功能,于是:function func2 ( // ...... ) { }
    他又想實現類似功能,于是:function func3 ( // ...... ) { }
    要避免命名沖突,只能靠你我他之間的溝通協作。

如果放著這些問題不解決,團隊的工作重點與關注點就不只是系統的業務邏輯,還包括隊內的溝通,這會阻礙著項目進度。而且當人數一多時(幾十人甚至上千人一起開發同一個項目),溝通就變得非常困難且低效了。

于是,前人創造了很多方法來避免這些問題,盡最大的努力實現模塊化:

3.2.1 避免全局環境污染的方法

  • 只創建一個全局變量作為當前應用的容器,把其他變量、方法加到該命名空間下。
    var Myapp = {};
    Myapp.location = "login";
    Myapp.info = {
    name: "flappybird",
    creator: "Dong Nguyen"
    };
    Myapp.startGame = function () {
    // ......
    };

  • 將代碼寫在一個匿名函數內部
    ( function () {
    // 局部變量和方法
    var variable1 = "I'm a variable in part";
    var func1 = function () {
    // ......
    };
    // 全局變量和方法
    window.variable2 = "I'm a variable in global";
    window.func2 = function () {
    // ......
    };
    })();

  • jquery風格匿名函數
    ( function (window) {
    // 通過給window添加屬性而暴漏到全局
    window.jQuery = window.$ = jQuery;

        // 定義全局對象jQuery($)的相關內容
    })(window);
    

jQuery的封裝風格曾被很多框架模仿。
這種方式用到了匿名函數包裝代碼(即第二種方法)。多出的點是,所依賴的外部變量可以傳給這個函數,在函數內部就可以使用這些依賴了,然后把模塊自身暴漏給window。
如果需要添加擴展,則可以作為jQuery的插件,把它掛載到$上。例如:fullpage.js插件。
這種風格雖然靈活了些,但并未解決根本問題:所需依賴還是得外部提前提供、還是增加了全局變量。

3.2.2 避免命名沖突的方法

  • java風格的命名空間,用多級命名空間來進行管理。于是編寫代碼和調用代碼就變得這么長了。
    Myapp.utils.func1 = xxx;
    Myapp.tools.func1 = xxx;
    Myapp.tools.another.func1 = xxx;

  • 設置變量名的控制權讓渡函數。
    有時候我們可能不只用到一種函數庫或插件,當用到多個函數庫時,由于庫并不是一個人編寫的,全局變量的命名沖突不是總能避免。如:jquery.js庫 和 Prototype.js庫,它們都用了$符號作為全局變量。同時導入兩個庫肯定會產生影響。
    但是jquery提供了noConflict()方法,可以讓渡變量名的控制權。
    // 將變量$的控制權讓渡給prototype.js
    jQuery.noConflict();
    // 使用jQuery
    jQuery("h1").text("我是標題");

    // 自定義一個更短的命名
    var jq = jQuery.noConflict();       
    jq("p").text("我是段落");
    

3.2.3 完善依賴關系的管理

后面提到的 require.js、sea.js 等 可以解決這個問題,這個后續再說。

3.2.4 推薦

想了解更多實現模塊化的方法,可以拜讀一下峰哥的文章:
Javascript模塊化編程(一):模塊的寫法

3.2.5 模塊化問題

當人們覺得再這樣下去寫代碼槽糕透了的時候,他們就想運用模塊化的思想,寫好一個模塊,要用就導入,導入后毫不影響原先的代碼。這樣就引發很多需要思考的問題:

  • 怎樣安全地包裝一個模塊的代碼?
  • 怎樣唯一地標識一個模塊?
  • 怎樣優雅地把模塊的API暴漏出去?
  • 怎樣方便地使用所依賴的模塊?

四、服務端 js 的誕生

4.1 nodejs

2009年,nodejs誕生,我們可以用 js 編寫服務端的代碼了。
在瀏覽器環境下,沒有模塊也不是特別大的問題,畢竟網頁程序的復雜性有限;但是在服務器端,一定要有模塊,與操作系統和其他應用程序互動,否則根本沒法編程。
于是,CommonJS 社區制定了 Modules/1.0 規范(現在已經被1.1取代)。nodejs 采用了該規范,故以下用 nodejs 作為例子。

4.2 Modules/1.0

總結起來,Modules/1.0規范指出:

  • 模塊需要提供頂級作用域的私有性。
  • 提供從其他模板導入單例對象到自身的能力
  • 提供導出自身API的能力

Modules/1.0規范的內容如下:

4.2.1 模塊上下文

  • 在模塊中存在一個自由變量"require",它是一個函數。這個"require"函數:
    ① 接收參數為:一個模塊標識符。
    var example = require('./example.js');
    ② 返回:外部模塊輸出的API。
    // 變量example即為外部模塊example.js輸出的內容
    ③ 如果出現依賴閉環(正常情況,加載main.js時,遇到 var a = require(./a.js); 則去加載a.js;加載a.js時,遇到 var b = require(./b.js); 則去加載b.js;加載b.js時,遇到 var a = require(./a.js); 則去加載a.js。無線循環,這就產生了依賴閉環的問題),為了避免這個問題,規定每個模塊只會被加載執行一次。
    // 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");  
    
    /* 輸出結果為:
    main start
    a start
    b start
    b end
    a end
    */
    
程序執行順序

④ 如果請求模塊失敗,require函數應拋出一個錯誤。

  • 模塊中存在一個名為"exports"的自由變量,它是一個對象,模板可把自身API加到其中。
    // 暴露message變量
    exports.message = "hi";
    // 暴露hello方法
    exports.say= function () {
    console.log("hello!");
    };
  • 模塊必須使用"exports"對象來作為輸出的唯一表示

4.2.2 模塊標識符

  • 模塊標識符是一個以正斜杠分隔的多個”term”組成的字符串。
  • 一個term必須是一個 駝峰格式的標識符,.字符(表示當前目錄) 或者 ..字符串(表示上一級目錄)。
  • 模塊標識符可以不加文件擴展名,比如”.js”。
    var a = require(./a);
    // 相當于 var a = require(./a.js);
  • 模塊標識符可以是 相對的 或者 頂級的 (top-level)。如果一個模塊標識符的第一個term是 .字符(表示當前目錄)或者 ..字符串(表示上一級目錄),那么它是 相對的 。
  • 頂級標識符是概念上的模塊命名空間的根。
  • 相對標識符是相對于在其內部調用了 require() 的模塊的標識符來進行解析的。

五、服務端的模塊化在前端領域的應用

既然服務端出了模塊化方案 Modules/1.0 ,那么是不是可以把這個規范直接用在客戶端啊?
只可惜,不能。出于以下原因:

  • 資源的加載方式與服務端完全不同。
    ① 服務端 require 一個模塊,是直接從 硬盤 或 內存 中讀取的。可以同步加載完成,等待時間就是硬盤的讀取時間,那速度是很快的。
    ② 客戶端,瀏覽器需要從服務端下載資源,花費的是請求所花的時間,取決于網速的快慢。若要等很長時間,瀏覽器會處于"假死"狀態。例如:
    // 第二行math.add(1, 1),在第一行require('math')之后運行,因此必須等math.js加載完成。
    // 如果加載時間很長,整個應用就會停在那里等。
    var math = require('./math.js');
    math.add(1, 1);
    因此,瀏覽器端的模塊,不能采用 "同步加載"(Sync),只能采用 "異步加載"(Async)。這就是 AMD規范(后面提及)誕生的背景。
  • 若瀏覽器加載資源的方式外層沒有 function 包裹,變量會暴漏在全局上;而全局污染這個問題在服務端編程不如瀏覽器要求嚴格。例如:
    // 變量math 和 math.js中定義在全局作用域上的變量、方法 都會污染到全局。
    var math = require('./math.js');

既然如此,問題要怎么解決?于是乎,就像黨派斗爭一樣,分裂了三種解決方案。

5.1 Modules/1.x

這一派人的意見是:

  • 在現有基礎上改進來滿足瀏覽器端的需要(function包裝不污染全局、異步加載)。所以,他們制定了 Modules/Transport規范,提出:先通過工具,把現有模塊代碼轉化為瀏覽器上使用的模塊代碼,然后再使用的方案。

典型的工具有:browserify。Browserify 可以讓你使用類似于 node 的 require() 的方式來組織瀏覽器端的 Javascript 代碼,通過 預編譯 讓前端 Javascript 可以直接使用 Node NPM 安裝的一些庫。難懂,那就直接看它的例子吧:

browserify的簡單用法

所以,若采用這一派的規范,我們就可以直接像服務端一樣編寫代碼了,編寫完后,只需要用工具把它編譯成瀏覽器使用的代碼即可。

5.2 Modules/2.0

這一派人的意見是:

  • Modules/1.0固然不適合瀏覽器,但它里面的一些理念還是很好的,如:通過 require 來聲明依賴。新的規范應該兼容這些。
  • AMD規范(請看 5.3) 也有它好的地方,如:模塊的預先加載、通過
    return 可暴漏任意類型的數據,而不像 commonjs 那樣 exports 只能為
    object。故 其中的一些觀點 也應采納。
  • 最終他們制定了一個 Modules/Wrappings規范,此規范指出了一個模塊應該如何"包裝",包含以下內容:
    ① 全局有一個 module 變量,用來定義模塊。
    ② 通過module.declare方法來定義一個模塊。
    ③ module.declare方法 只接收一個參數,那就是模塊的 factory,它可以是函數,也可以是對象(如果是對象,那么模塊輸出就是此對象)。
    ④ 模塊的 factory函數 傳入三個參數:require、exports、module,用來引入其他依賴和導出本模塊API。
    ⑤ 如果 factory函數 最后明確寫有return數據,那么 return 的內容即為模塊的輸出;不寫 return 默認返回undefined。

CMD/seajs

seajs 的作者 是 國內大牛 淘寶前端步道者 玉伯。seajs 全面擁抱
Modules/Wrappings規范,不用 RequireJS 那樣回調的方式來編寫模塊。

它的特色和用法以后再來補充。(待續)

5.3 Modules/Async

這一派人的意見是:

  • 瀏覽器與服務器環境差別太大,不能沿用舊的模塊標準。

  • 既然瀏覽器必須異步加載代碼,那么模塊在定義的時候就必須 指明所依賴的模塊,然后 把本模塊的代碼寫在回調函數里。模塊的加載也是通過 下載—>回調 這樣的過程來進行,這個思想就是AMD的基礎。
    // AMD也采用require()語句加載模塊,但是不同于CommonJS,它要求兩個參數
    // 第一個參數[module],是一個數組,里面的成員就是要加載的模塊
    // 第二個參數callback,則是加載成功之后的回調函數
    require([module], callback);

    // math.add()與math模塊加載不是同步的,瀏覽器不會發生假死。AMD比較適合瀏覽器環境。
    require(['math'], function (math) {
        math.add(2, 3);
    });
    
  • 由于與原規范不合,最終從 CommonJs 中分裂了出去,獨立制定了瀏覽器端的js模塊化規范 AMD(Asynchronous Module Definition)。

  • 目前,主要有兩個Javascript庫實現了AMD規范:require.jscurl.js

AMD/RequireJs

這里主要介紹 RequireJs,若想了解其用法,可以看我的另一篇文章:AMD/RequireJS 使用入門

六、ES6模塊化標準

既然模塊化開發的呼聲這么高,作為官方的ECMA必然要有所行動,js模塊化很早就列入草案,終于在2015年6月份發布了ES6正式版。

ES6只要增加了 export 、import 、module 等命令。具體用法以后再補充。

想了解更多關于ES6的東西,推薦大家閱讀《ECMAScript 6 入門》,這是這本書的 網上教程。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容