淺談JavaScript 模塊化

參考資料

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

前言

本人菜鳥,入IT只為當鼓勵師。本編文章意在簡單總結(jié)一下 什么是模塊化,模塊化的優(yōu)點, js模塊化 的發(fā)展歷史,關(guān)于 js模塊化 的一些規(guī)范 等等。

一、什么是模塊化

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

暈了,這是什么嘛。

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

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

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

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

  • c使用 #include 包含.h文件
  • php中使用 require_once 包含.php文件
  • java使用 import 導(dǎo)入包

拋開C、C++、Java、PHP這些不說,就說前端領(lǐng)域,認真想想,其實 html css 也實現(xiàn)了模塊化。

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


  • css 中有 @import " /.css " 指令可以導(dǎo)入其他css

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

二、模塊化的優(yōu)點

可維護性:

  • 多人協(xié)作互不干擾
  • 靈活架構(gòu),焦點分離
  • 方便模塊間組合、分解 、解耦
  • 方便單個模塊功能調(diào)試、升級

可測試性:

  • 可分單元測試

三、前端的模塊化思想的發(fā)展

3.1 那年的誕生——1995

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

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

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

3.2 模塊萌芽

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

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

    // 正確,gameBg.js要在main.js之前導(dǎo)入
    <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文件很多呢?
    
  • 全局環(huán)境的污染。
    我在a.js中定義了一個全局變量 var a = 0,相當于定義在window上。
    你在b.js中用了我定義的全局變量,給它賦值 a = 1
    我又在c.js中用了這個全局變量,但我不知道你在b.js中修改過a的值。于是 if (a==0) { // ...... }。(出事了?。?/p>

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

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

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

3.2.1 避免全局環(huán)境污染的方法

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

  • 將代碼寫在一個匿名函數(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;

        // 定義全局對象jQuery($)的相關(guān)內(nèi)容
    })(window);
    

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

3.2.2 避免命名沖突的方法

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

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

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

3.2.3 完善依賴關(guān)系的管理

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

3.2.4 推薦

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

3.2.5 模塊化問題

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

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

四、服務(wù)端 js 的誕生

4.1 nodejs

2009年,nodejs誕生,我們可以用 js 編寫服務(wù)端的代碼了。
在瀏覽器環(huán)境下,沒有模塊也不是特別大的問題,畢竟網(wǎng)頁程序的復(fù)雜性有限;但是在服務(wù)器端,一定要有模塊,與操作系統(tǒng)和其他應(yī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ī)范指出:

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

Modules/1.0規(guī)范的內(nèi)容如下:

4.2.1 模塊上下文

  • 在模塊中存在一個自由變量"require",它是一個函數(shù)。這個"require"函數(shù):
    ① 接收參數(shù)為:一個模塊標識符。
    var example = require('./example.js');
    ② 返回:外部模塊輸出的API。
    // 變量example即為外部模塊example.js輸出的內(nèi)容
    ③ 如果出現(xiàn)依賴閉環(huán)(正常情況,加載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。無線循環(huán),這就產(chǎn)生了依賴閉環(huán)的問題),為了避免這個問題,規(guī)定每個模塊只會被加載執(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
    */
    
程序執(zhí)行順序

④ 如果請求模塊失敗,require函數(shù)應(yīng)拋出一個錯誤。

  • 模塊中存在一個名為"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是 .字符(表示當前目錄)或者 ..字符串(表示上一級目錄),那么它是 相對的
  • 頂級標識符是概念上的模塊命名空間的根。
  • 相對標識符是相對于在其內(nèi)部調(diào)用了 require() 的模塊的標識符來進行解析的。

五、服務(wù)端的模塊化在前端領(lǐng)域的應(yīng)用

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

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

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

5.1 Modules/1.x

這一派人的意見是:

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

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

browserify的簡單用法

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

5.2 Modules/2.0

這一派人的意見是:

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

CMD/seajs

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

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

5.3 Modules/Async

這一派人的意見是:

  • 瀏覽器與服務(wù)器環(huán)境差別太大,不能沿用舊的模塊標準。

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

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

  • 目前,主要有兩個Javascript庫實現(xiàn)了AMD規(guī)范:require.jscurl.js。

AMD/RequireJs

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

六、ES6模塊化標準

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

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

想了解更多關(guān)于ES6的東西,推薦大家閱讀《ECMAScript 6 入門》,這是這本書的 網(wǎng)上教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容