<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>javascript模塊化編程</title> </head> <body> <p></p>
網頁越來越像桌面程序,需要一個團隊分工協作、進度管理、單元測試等等......開發者不得不使用軟件工程的方法,管理網頁的業務邏輯。
Javascript模塊化編程,已經成為一個迫切的需求。理想情況下,開發者只需要實現核心的業務邏輯,其他都可以加載別人已經寫好的模塊。
但是,Javascript不是一種模塊化編程語言,它不支持"類"(class),更遑論"模塊"(module)了。(正在制定中的ECMAScript標准第六版,將正式支持"類"和"模塊",但還需要很長時間才能投入實用。)
Javascript社區做了很多努力,在現有的運行環境中,實現"模塊"的效果。本文總結了當前"Javascript模塊化編程"的最佳實踐,說明如何投入實用。雖然這不是初級教程,但是只要稍稍了解Javascript的基本語法,就能看懂。
<h1>模塊就是實現特定功能的一組方法。</h1>
<h2>只要把不同的函數(以及記錄狀態的變量)簡單地放在一起,就算是一個模塊。</h2>
<script> /** * 第一部分介紹Javascript模塊的基本寫法 * */ //一、原始寫法 "污染"了全局變量 function m1(){} function m2(){} //二、對象寫法 這樣的寫法會暴露所有模塊成員,內部狀態可以被外部改寫。 var module = new Object({ _count : 0, m1:function(){}, m2:function m2(){} }); module.m1(); module._count = 5; //三、立即執行函數寫法 外部代碼無法讀取內部的_count變量。 var module1 = (function(){ var _count = 0; var m1 = function () {} var m2 = function () { console.log("我是module1上的m2,很高興認識你!") } return { m1:m1, m2:m2 } })(); // module1.m2() //module1就是Javascript模塊的基本寫法。下面,再對這種寫法進行加工。 // 四、放大模式 (對原始模塊功能的擴展) // 如果一個模塊很大,必須分成幾個部分,或者一個模塊需要繼承另一個模塊 var module2 = (function(mod){ mod.m3 = function(){ console.log("我是module2上的m3,很高興認識你!") } return mod; })(module1); console.log(module2) // 五、寬放大模式(Loose augmentation) // 在瀏覽器環境中,模塊的各個部分通常都是從網上獲取的,有時無法知道哪個部分會先加載。如果采用上一節的寫法,第一個執行的部分有可能加載一個不存在空對象,這時就要采用"寬放大模式"。 var module3 = (function(mod){ //... return mod; })(window.module2||{}); // 六、輸入全局變量 // 獨立性是模塊的重要特點,模塊內部最好不與程序的其他部分直接交互。 // 為了在模塊內部調用全局變量,必須顯式地將其他變量輸入模塊。 var jQuery = {}; var module4 = (function(mod){ //... })(jQuery,module3) // 這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關系變得明顯 /** * 第二部分介紹如何規范地使用模塊。 * */ //七、模塊的規范 // 目前,通行的Javascript模塊規范共有兩種:CommonJS和AMD。我主要介紹AMD,但是要先從CommonJS講起。 // node.js的模塊系統,就是參照CommonJS規范實現的。在CommonJS中,有一個全局性方法require(),用於加載模塊。 //八、CommonJS (同步) // 假定的一個math.js模塊,加載方式如下 // var math = require('math'); // 調用 // math.add(2,3); //5 // 九、瀏覽器環境 AMD(異步) //瀏覽器端的模塊,不能采用"同步加載"(synchronous),只能采用"異步加載"(asynchronous)。這就是AMD規范誕生的背景。 // AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。它采用異步方式加載模塊,模塊的加載不影響它后面語句的運行。所有依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成之后,這個回調函數才會運行。 // AMD也采用require()語句加載模塊,但是不同於CommonJS,它要求兩個參數: // require([module], callback); // 第一個參數[module],是一個數組,里面的成員就是要加載的模塊;第二個參數callback,則是加載成功之后的回調函數。如果將前面的代碼改寫成AMD形式,就是下面這樣: /** * require([module1,module2,...],function(module1,module2,...){ module1.add(2,3); }); */ // 目前,主要有兩個Javascript庫實現了AMD規范:require.js和curl.js。 /** * 第三部分介紹require.js,進一步講解AMD的用法,以及如何將模塊化編程投入實戰。 * */ // 一、為什么要用require.js? // 最早的時候,所有Javascript代碼都寫在一個文件里面,只要加載這一個文件就夠了。后來,代碼越來越多,一個文件不夠了,必須分成多個文件,依次加載。下面的網頁代碼,相信很多人都見過。 /** * <script src="1.js"><//script> <script src="2.js"><//script> <script src="3.js"><//script> 缺點: 1.加載的時候,瀏覽器會停止網頁渲染, 2.由於js文件之間存在依賴關系,因此必須嚴格保證加載順序, 3.當依賴關系很復雜的時候,代碼的編寫和維護都會變得困難。 * */ //require.js的誕生,就是為了解決這兩個問題: // (1)實現js文件的異步加載,避免網頁失去響應; // (2)管理模塊之間的依賴性,便於代碼的編寫和維護。 // 二、require.js的加載 // 使用require.js的第一步,是先去官方網站下載最新版本。 // 下載后,假定把它放在js子目錄下面,就可以加載了。 // <script src="js/require.js"><//script> // 有人可能會想到,加載這個文件,也可能造成網頁失去響應。解決辦法有兩個,一個是把它放在網頁底部加載,另一個是寫成下面這樣: // <script src="js/require.js" defer async="true" ><//script> // async屬性表明這個文件需要異步加載,避免網頁失去響應。IE不支持這個屬性,只支持defer,所以把defer也寫上。 // 加載require.js以后,下一步就要加載我們自己的代碼了。假定我們自己的代碼文件是main.js,也放在js目錄下面。那么,只需要寫成下面這樣就行了: // <script src="js/require.js" data-main="js/main"><//script> // data-main屬性的作用是,指定網頁程序的主模塊。在上例中,就是js目錄下面的main.js,這個文件會第一個被require.js加載。由於require.js默認的文件后綴名是js,所以可以把main.js簡寫成main。 // 三、主模塊的寫法 // 上一節的main.js,我把它稱為"主模塊",意思是整個網頁的入口代碼。它有點像C語言的main()函數,所有代碼都從這兒開始運行。 // 下面就來看,怎么寫main.js。 // 主模塊依賴於其他模塊,這時就要使用AMD規范定義的的require()函數。 // main.js /** * require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){ // some code here }); * */ //四、模塊的加載 // 上一節最后的示例中,主模塊的依賴模塊是['jquery', 'underscore', 'backbone']。默認情況下,require.js假定這三個模塊與main.js在同一個目錄,文件名分別為jquery.js,underscore.js和backbone.js,然后自動加載。 //使用require.config()方法,我們可以對模塊的加載行為進行自定義。require.config()就寫在主模塊(main.js)的頭部。參數就是一個對象,這個對象的paths屬性指定各個模塊的加載路徑。 //另一種則是直接改變基目錄(baseUrl)。 /** * require.config({ baseUrl: "js/lib", paths: { "jquery": "jquery.min", "underscore": "underscore.min", "backbone": "backbone.min" } }); * */ // require.js要求,每個模塊是一個單獨的js文件。這樣的話,如果加載多個模塊,就會發出多次HTTP請求,會影響網頁的加載速度。因此,require.js提供了一個優化工具,當模塊部署完畢以后,可以用這個工具將多個模塊合並在一個文件中,減少HTTP請求數。 // 五、AMD模塊的寫法 // require.js加載的模塊,采用AMD規范。也就是說,模塊必須按照AMD的規定來寫。 // 具體來說,就是模塊必須采用特定的define()函數來定義。如果一個模塊不依賴其他模塊,那么可以直接定義在define()函數之中。 // 假定現在有一個math.js文件,它定義了一個math模塊。那么,math.js就要這樣寫: // math.js /** * define(function (){ var add = function (x,y){ return x+y; }; return { add: add }; }); */ // 如果這個模塊還依賴其他模塊,那么define()函數的第一個參數,必須是一個數組,指明該模塊的依賴性。 // 當require()函數加載上面這個模塊的時候,就會先加載myLib.js文件。 /** * define(['myLib'], function(myLib){ function foo(){ myLib.doSomething(); } return { foo : foo }; }); */ // 六、加載非規范的模塊 // 理論上,require.js加載的模塊,必須是按照AMD規范、用define()函數定義的模塊。 // 但是實際上,雖然已經有一部分流行的函數庫(比如jQuery)符合AMD規范,更多的庫並不符合。 // 那么,require.js是否能夠加載非規范的模塊呢? // 回答是可以的。 // 這樣的模塊在用require()加載之前,要先用require.config()方法,定義它們的一些特征。 // 舉例來說,underscore和backbone這兩個庫,都沒有采用AMD規范編寫。如果要加載它們的話,必須先定義它們的特征。 /** * require.config({ shim: { 'underscore':{ exports: '_' }, 'backbone': { deps: ['underscore', 'jquery'], exports: 'Backbone' } } }); require.config()接受一個配置對象,這個對象除了有前面說過的paths屬性之外, 還有一個shim屬性,專門用來配置不兼容的模塊。 具體來說,每個模塊要定義 (1)exports值(輸出的變量名),表明這個模塊外部調用時的名稱; (2)deps數組,表明該模塊的依賴性。 */ // 比如,jQuery的插件可以這樣定義: /** * shim: { 'jquery.scroll': { deps: ['jquery'], exports: 'jQuery.fn.scroll' } } * * */ // 七、require.js插件 // require.js還提供一系列插件,實現一些特定的功能。 // domready插件,可以讓回調函數在頁面DOM結構加載完成后再運行。 /** * require(['domready!'], function (doc){ // called once the DOM is ready }); * * */ // text和image插件,則是允許require.js加載文本和圖片文件。 /** * define([ 'text!review.txt', 'image!cat.jpg' ], function(review,cat){ console.log(review); document.body.appendChild(cat); } ); * * */ // 類似的插件還有json和mdown,用於加載json文件和markdown文件。 </script> <script src="js/require.js" defer anync="true" data-main="js/main"></script> </body> </html>
main.js
console.log("我加載進來了,哈哈哈哈0.0"); // require(['module1','module2'],function (module1,module2) { // module1.m2(); // // module2.m3(); // }) // require()函數接受兩個參數。第一個參數是一個數組,表示所依賴的模塊,上例就是['moduleA', 'moduleB', 'moduleC'], // 即主模塊依賴這三個模塊;第二個參數是一個回調函數,當前面指定的模塊都加載成功后,它將被調用。 // 加載的模塊會以參數形式傳入該函數,從而在回調函數內部就可以使用這些模塊。 // // require()異步加載moduleA,moduleB和moduleC,瀏覽器不會失去響應; // 它指定的回調函數,只有前面的模塊都加載成功后,才會運行,解決了依賴性的問題。 // 假定主模塊依賴jquery、underscore和backbone這三個模塊,main.js就可以這樣寫: /** * require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){ // some code here }); * */ require.config({ baseUrl:'js/lib/', //根目錄 paths:{ "jquery":"jquery" //擴展名.js不用寫,會自動加上 } }) require(['jquery','../module1'], function ($,module1){ $("p").html("我是jQuery,我進來了,很高興認識你!") module1.m2(); });
module1.js
//無依賴版 // define(function(){ // var _count = 0; // var m1 = function () {} // var m2 = function () { // console.log("我是module1上的m2,很高興認識你!") // } // return { // m1:m1, // m2:m2 // } // }) //有依賴 define(['../js/module2'], function(module2){ var _count = 0; var m1 = function () {} var m2 = function () { console.log("我是module1上的m2,很高興認識你!") } module2.m3() module2.m4 = function(){ console.log("我是module1上的m4,我很特殊,是module1給module2寫的擴展,很高興認識你!") } module2.m4(); return { m1:m1, m2:m2 } });
module2.js
// ;(function(mod){ // mod.m3 = function(){ // console.log("我是module2上的m3,很高興認識你!") // } // return mod; // })(module1); define(function(){ var m3 = function () { console.log("我是module2上的m3,很高興認識你!") } return { m3:m3 } })
參考資料:
http://www.ruanyifeng.com/blog/2012/10/javascript_module.html
http://www.ruanyifeng.com/blog/2012/10/asynchronous_module_definition.html