Javascript模塊化編程(三):模塊化編程實(shí)戰(zhàn),試用SeaJS
看了阮一峰老師的關(guān)于JavaScript模塊化的文章后,解答了我思考很久的問題,突然有種豁然開朗的感覺。后來了解到SeaJS,就想寫篇文章,實(shí)踐一下模塊化編程。今天把文章寫出來了。發(fā)出來,希望對(duì)大家有用。
本系列目錄
第三篇文章的兩個(gè)“引子”
前段時(shí)間轉(zhuǎn)載了阮一峰老師的兩篇講解Javascript模塊化編程的文章: “JavaScript模塊化編程(一):模塊原型和理論概念詳解”,介紹了Javascript模塊原型和理論概念;Javascript模塊化編程(二):模塊化編程實(shí)戰(zhàn),require.js詳解,介紹了在實(shí)戰(zhàn)中,如何利用RequireJS庫,進(jìn)行模塊化編程。
在這兩篇文章發(fā)布出來之后,在和網(wǎng)友的交流討論中,了解到了SeaJS,這個(gè)由國人玉伯自己創(chuàng)建的模塊化編程庫。然后,我就想學(xué)習(xí)學(xué)習(xí), 再寫篇文章給大家介紹一下。
背景介紹
官網(wǎng)的資料是最靠譜的。在SeaJS的官網(wǎng)上發(fā)現(xiàn),有一個(gè)“5分鐘上手SeaJS”的例子,然后就從這個(gè)例子的開始學(xué)習(xí)。不過,只看明白了個(gè)六六七七。我沒看明白和我平時(shí)的JavaScript編程有啥區(qū)別。另外,我沒有動(dòng)手實(shí)踐,心里面不踏實(shí)。所以,動(dòng)手寫個(gè)程序,玩味一下。后來,就想到了“猜手機(jī)號(hào)游戲”!
由于官網(wǎng)已經(jīng)有使用SeaJS的教程,我就不重復(fù)這方面的工作了,而且我也覺得我我肯定沒有官網(wǎng)寫的好。由于我不清楚,使用SeaJS進(jìn)行“模塊化編程”和我平時(shí)不進(jìn)行“模塊化編程”的區(qū)別。所以,我準(zhǔn)備從另外一個(gè)角度來介紹SeaJS:將一個(gè)沒有進(jìn)行模塊化編程的程序,改造成使用SeaJS進(jìn)行“模塊化編程”的程序。由于這個(gè)想法的跨度比較大,信息量也比較多。所以我把我的想法組織成了三篇文章:第一篇文章,“給哥三十五次機(jī)會(huì),哥就能猜中你的手機(jī)號(hào)”,通過一個(gè)小游戲,來吸引大家的興趣;第二篇,“‘猜手機(jī)號(hào)游戲’的源碼分析:二分查找+面向?qū)ο?/a>”,來講解在沒有進(jìn)行模塊化編程時(shí),程序的實(shí)現(xiàn)細(xì)節(jié);然后,這是第三篇,在沒有進(jìn)行模塊化編程的基礎(chǔ)上,將原來的程序改造成一個(gè)使用SeaJS進(jìn)行模塊化編程的例子。
在閱讀這篇文章之前,請(qǐng)閱讀前兩篇,尤其是“'猜手機(jī)號(hào)游戲'的源碼分析:二分查找+面向?qū)ο?/a>”。同時(shí),還建議閱讀一下“JavaScript模塊化編程(一):模塊原型和理論概念詳解”和Javascript模塊化編程(二):模塊化編程實(shí)戰(zhàn),require.js詳解,規(guī)范、系統(tǒng)一下關(guān)于Javascript模塊化編程的知識(shí)。
CMD模塊定義規(guī)范介紹
想享受模塊化編程帶來的良好封裝,就必須遵循模塊化編程的規(guī)范。在 SeaJS 中,所有 JavaScript 模塊都遵循 CMD(Common Module Definition) 規(guī)范。該規(guī)范確定了模塊的基本書寫格式和基本交互方式。所以,使用SeaJS之前,必須閱讀一下SeaJS所要求遵循的規(guī)范。
鑒于規(guī)范覆蓋的東西比較多,看多了頭大。所以,我把這個(gè)規(guī)范提煉簡化一下,只關(guān)注我們需要用到的。至于,更詳細(xì)的CMD模塊定義規(guī)范,等先把例子跑通,理解了整個(gè)流程,然后再回頭看規(guī)范,梳理、規(guī)范這部分知識(shí)。
在介紹簡化版規(guī)范之前,D瓜哥提兩個(gè)也許大家都回“納悶”的問題:
- 如何定義模塊?
- 如何獲取外部依賴的模塊?
CMD模塊定義規(guī)范中的主要內(nèi)容正是回答這兩個(gè)問題。下面請(qǐng)看經(jīng)D瓜哥簡化的規(guī)范如下:
- 定義、封裝模塊的方法。(CMD模塊定義規(guī)范中有好多定義方法。簡單起見,目前只考慮使用如下這一種方式。)如下:
define(function(require, exports, module) { // The module code goes here});
這里需要特別說明一下,向參數(shù)傳遞的三個(gè)參數(shù)名必須按照代碼所示中那樣寫,不能簡寫,或者使用其他字符串代替;同時(shí),在函數(shù)內(nèi),exports不能被改寫成其他值;可以把exports看成對(duì)象添加屬性,如exports.key,然后對(duì)其復(fù)制,又如exports.key = "dValue"。
- 對(duì)外提供模塊接口。在上一步中,我們?cè)诤瘮?shù)內(nèi)定義了模塊,但是這是在函數(shù)內(nèi)定義的,在函數(shù)外部不容易訪問到。該怎么向外提供模塊接口呢?定義方法如下:
define(function(require, exports, module) { // 實(shí)用這種方式向外提供模塊接口 module.exports = { foo: 'bar', doSomething: function() {}; }; // 或者。由于,D瓜哥將模塊封裝成了一個(gè)對(duì)象,所以,本例中,使用這個(gè)方式。 module.exports = yourFunctionName; });
傳給 factory 構(gòu)造方法(就是define(function(){})方法參數(shù)中那個(gè)函數(shù),稱為factory。函數(shù)只是factory的一種形式,其他形式以后再補(bǔ)充。)的 exports 參數(shù)是 module.exports 對(duì)象的一個(gè)引用。只通過 exports 參數(shù)來提供接口,有時(shí)無法滿足開發(fā)者的所有需求。 比如當(dāng)模塊的接口是某個(gè)類的實(shí)例時(shí),需要通過 module.exports 來實(shí)現(xiàn)。D瓜哥這里就是一個(gè)對(duì)象,所以只能使用module.exports 。
- 獲取外部依賴模塊。模塊定義要了,需要使用的時(shí)候,就可以使用require函數(shù)獲取外部依賴。具體代碼如下:
define(function(require, exports, module) { // 使用require函數(shù)獲取外部依賴 var a = require('./a'); a.doSomething();});
require函數(shù)的參數(shù)是a.js文件的相對(duì)路徑。后綴名可以省略,在SeaJS加載模塊的時(shí)候會(huì)自動(dòng)加上的。另外,這里可以執(zhí)行回調(diào)函數(shù)。不過,我們的任務(wù)是跑起來。因?yàn)椴恍枰卣{(diào)函數(shù),所以這部分先略過了。
總結(jié)一下:define函數(shù),定義模塊;module對(duì)象,保存模塊信息;require函數(shù),獲取外部依賴模塊。
看到這里,估計(jì)大家還是一頭霧水。沒關(guān)系,慢慢往下看,下面的例子跑起來的時(shí)候,你再回頭看就會(huì)明白的。
模塊化改造
先聲明一下,下面的改造過程會(huì)參考“5分鐘入門”的說明。所以,建議大家先看看。當(dāng)然,一起看也可以。
通過看"5分鐘入門"的例子可以看出,SeaJS的目錄結(jié)構(gòu)還是有點(diǎn)復(fù)雜的。所以,最簡單的方法就是,把她的例子下載下來,在她的基礎(chǔ)之上修改:"5分鐘入門"例子下載。
目錄結(jié)構(gòu)
下載完成后,解壓到任意目錄下。請(qǐng)看一下目錄,
- hello-seajs/下放我們的html文件;
- hello-seajs/assets/sea-modules下存放的是我們需要用到的第三方模塊塊;
- hello-seajs/assets/main,這個(gè)目錄可以說最重要,是存放我們自己編寫的JavaScript和CSS文件的地方。下面還有四個(gè)子目錄及一個(gè)文件:
- src存放正常的代碼;
- test存放測(cè)試代碼;
- docs存放文檔;
- examples存放示例代碼;
- package.json是打包的配置文件;
“改造”模塊代碼
下面,我們開始改造我們的模塊。
首先,把我GuessNumber.js放到hello-seajs/assets/main/src/下。然后,按照“第1條規(guī)范”的要求改造這個(gè)文件中代碼。由于整個(gè)文件就是GuessNumber對(duì)象的定義。同時(shí),這個(gè)JavaScript文件又沒有引用其他模塊。所以,只需要在文件的第一行增加define,在最后一行增加括號(hào)分號(hào)就行。具體代碼如下:
define(function(require, exports, module){ /** * numberScope 需要猜測(cè)的數(shù)字范圍 */ function GuessNumber(numberScope){ // 為了突出修改的代碼,我把一些相同的代碼省略了, // 完整代碼請(qǐng)看:http://www.diguage.com/archives/80.html } GuessNumber.prototype = { constructor: GuessNumber, // 完整代碼請(qǐng)看:http://www.diguage.com/archives/80.html }});
其次,目前我們已經(jīng)定義為一個(gè)模塊。但是外部如何訪問這個(gè)GuessNumber?所以,我們要向外部提供一個(gè)接口,提供方式參考“第2條規(guī)范”。具體代碼見第18行:
define(function(require, exports, module){ /** * numberScope 需要猜測(cè)的數(shù)字范圍 */ function GuessNumber(numberScope){ // 完整代碼請(qǐng)看:http://www.diguage.com/archives/80.html } GuessNumber.prototype = { constructor: GuessNumber, // 完整代碼請(qǐng)看:http://www.diguage.com/archives/80.html } module.exports = GuessNumber; });
這時(shí),一個(gè)接口已經(jīng)全部定義完成。下面,我們書寫調(diào)用這個(gè)模塊的例子。
在“規(guī)范”的第三條中,我們說明了加載外部依賴模塊的方法,我們只需要按說明照做就行。另外,還需要補(bǔ)充一下模塊加載時(shí)需要注意的地方。具體請(qǐng)看代碼注釋:
define(function(require) { // 這是引入jQuery類庫,我們下面說明為什么這樣下。 var $ = require('jquery'); // 引入GuessNumber模塊,也就是GuessNumber.js文件。 // 參數(shù)中傳遞的是GuessNumber.js文件的相對(duì)路徑。 // .js的后綴名可以省略,SeaJS在加載的時(shí)候會(huì)自動(dòng)加上。 var GuessNumber = require("./GuessNumber"); // 完整代碼請(qǐng)看:http://www.diguage.com/archives/80.html //格式化顯示結(jié)果 function formatResult(num, type) { //…… } // …… $("#initButton").click(function(){ guess.start(scopeArr[type].min, scopeArr[type].max); showResult(); });});
從上面的代碼中,可以看出,main.js文件的改造,只是把原來的
$(document).ready(function(){ // 主要的業(yè)務(wù)代碼});
改造成了,
define(function(require) { // 這是引入jQuery類庫,我們下面說明為什么這樣下。 var $ = require('jquery'); // 引入GuessNumber模塊,也就是GuessNumber.js文件。 // 參數(shù)中傳遞的是GuessNumber.js文件的相對(duì)路徑。 // .js的后綴名可以省略,SeaJS在加載的時(shí)候會(huì)自動(dòng)加上。 var GuessNumber = require("./GuessNumber"); // 和原文件相同的業(yè)務(wù)代碼});
另外,加了兩行倒入必要關(guān)聯(lián)模塊的代碼。僅此而已。
main.js與GuessNumber.js不同的還有一點(diǎn),main.js不需要向外提供訪問接口。這點(diǎn)也要注意一下。
到這里所有的JavaScript都已經(jīng)修改完畢了。下面,我們修改一下如何在HTML中的引入方式。
在頁面中加載模塊
原來的寫法是,按順序使用<scrip>標(biāo)簽把jQuery、GuessNumber.js以及main.js文件引入到HTML頁面中即可。如果使用SeaJS,則需要先加載SeaJS的類庫,然后使用JavaScript通過SeaJS的接口來加載所需的模塊,也就是模塊對(duì)應(yīng)的JavaScript文件。具體代碼如下:
<!-- 首先,首先我們需要引入 sea.js -->
<script src="assets/sea-modules/seajs/1.3.1/sea.js"></script>
<script type="text/javascript">
seajs.config({
alias: {
// 指定使用的jQuery版本以及說明jQuery的路徑
// 請(qǐng)注意:這里知名了jQuery的路徑,所以,我們
// 在引入jQuery庫時(shí),只需要填寫jquery即可。
'jquery': 'gallery/jquery/1.8.2/jquery'
}
});
// 然后SeaJS通過 use 方法來加載模塊,以后打包后也是修改這里
// 也許你會(huì)疑問為什么不加載GuessNumber.js文件,
// 這個(gè)在使用require引入依賴時(shí),SeaJS自動(dòng)加載需要的外部文件
// 另外,這里的.js后綴名也可以省略,SeaJS會(huì)自動(dòng)補(bǔ)全。
seajs.use('./assets/main/src/main');
</script>
<!-- 這里只展示了和JavaScript引入相關(guān)的代碼 -->
<!-- 完整代碼請(qǐng)看:http://www.diguage.com/archives/80.html 中的HTML代碼 -->
到此,改造工作就全部完成了。你可以打開一下inde.html文件,看看效果了。
打包部署
根據(jù)“高性能網(wǎng)站的十四條黃金法則”中的實(shí)踐,我們?cè)趯?shí)際項(xiàng)目上線時(shí),為了提高頁面的加載速度,必定要壓縮一下JavaScript文件。這些,SeaJS也考慮到了,甚至做得更好:還做了文件合并。
這里,需要先介紹一下,SPM,一個(gè)基于命令行的前端項(xiàng)目管理工具。 SPM 和 SeaJS 關(guān)系密切,你甚至可以認(rèn)為SPM是為SeaJS專門打造的工具。首先,請(qǐng)“安裝教程”安裝好這個(gè)工具。按照過程可能會(huì)有一個(gè)問題,請(qǐng)參考下面的“出現(xiàn)的問題”。
使用SPM打包,需要修改一下打包的配置文件。配置文件是:hello-seajs/assets/main/package.json。打開后內(nèi)容如下:
{ "name": "main", "version": "1.0.0", "dependencies": { "jquery": "gallery/jquery/1.8.2/jquery" }, "root": "hello-seajs", "output": { "main.js": ".", "main.css": "." }, "spmConfig": { "build": { "to": "../sea-modules/{{root}}/{{name}}/{{version}}" } }}
不過,這個(gè)需要根據(jù)我們的實(shí)際情況來修改。root屬性,由于我們的模塊是“猜數(shù)”,所以將其修改為GuessNumber;output屬性,我們只需要輸出JS,所以刪除main.css。另外,需要注意,第十四行,這個(gè)是打包后的輸出路徑。好了,開始打包。打包需要執(zhí)行如下指令:
$ cd hello-seajs/assets/main$ spm build ...BUILD SUCCESS!$
打包結(jié)束后,在hello-seajs/assets/中就會(huì)發(fā)現(xiàn)多了一個(gè)GuessNumber文件夾,那個(gè)就是打包輸出出來。
這里說明一下:D瓜哥只在Linux下執(zhí)行了這么命令。不知在Windows是否好使。為了方便大家測(cè)試運(yùn)行,打包結(jié)果已提交,下載的代碼中包含打包結(jié)果。
觀察這個(gè)結(jié)果,大家會(huì)發(fā)現(xiàn)只有一個(gè)main.js和main-debug.js;顧名思義,main.js是用于生產(chǎn)部署的,經(jīng)過壓縮的文件;main-debug.js是為測(cè)試使用的,只是合并了代碼并沒有壓縮,使用的時(shí)候直接引用這個(gè)兩個(gè)文件中的一個(gè)就行,直接把seajs.use()中的路徑改一下就OK。GuessNumber.js哪里去了???大家可以打開main-debug.js看看(main.js也行,只是壓縮過來,可讀性不好),原來,GuessNumber.js已經(jīng)合并到了main.js中了。SPM把兩個(gè)文件合并成一個(gè)文件了,這樣在瀏覽器訪問網(wǎng)頁時(shí),就可以減少一個(gè)HTTP請(qǐng)求,提高網(wǎng)頁的加載速度。
另外,大家也可能會(huì)注意到在原來main.js中定義的define()函數(shù),在新的main.js有了一些變化,多了兩個(gè)參數(shù):第一個(gè)參數(shù)模塊的ID,主要是為了方便區(qū)別一個(gè)文件中的各個(gè)模塊;第二個(gè)參數(shù)是模塊依賴的外部模塊的路徑,因?yàn)橐蕾嚨哪K可能有多個(gè),所以這個(gè)參數(shù)是一個(gè)數(shù)組。第三個(gè)參數(shù)是原來的function,也就是factory。更詳細(xì)的解釋請(qǐng)看:為什么要用 spm 來壓縮 CMD 模塊?
懶人要把懶進(jìn)行到底!打包后還要修改SeaJS的加載路徑,這點(diǎn)其實(shí)還可以使用如下代碼來避免:
// 這個(gè)路徑只有在部署到服務(wù)器上才行,直接打開文件不好使。seajs.use(location.host === 'localhost' ? './assets/main/src/main' : 'GuessNumber/main/1.0.0/main');
如果是非靜態(tài)頁面,也可以使用變量來配置。
折騰中出現(xiàn)的問題
折騰這么個(gè)玩意,難免出現(xiàn)一些問題,D瓜哥遇到了三個(gè)問題。這些問題主要集中在SPM環(huán)境搭建過程中。給大家分享一下。
第一個(gè)問題:按照seajs時(shí),提示info.json不存在的錯(cuò)誤。終端顯示如下:
d@dPC:~/Dev/hello-seajs/assets$ spm install seajsStart installing ...success create global config.json to /home/d/.spmDownloading: http://modules.spmjs.org/info.json[ERROR] Caught exception: Error: not found config http://modules.spmjs.org/info.json
大家可以在瀏覽器地址中打開http://modules.spmjs.org/info.json,會(huì)發(fā)現(xiàn)可以打開。這是怎么回事呢?
我查閱了一下SeaJS論壇,里面有類似的問題。其中的一個(gè)回復(fù),我拿過來當(dāng)作解答吧:這段時(shí)間是舉國同慶的日子,網(wǎng)絡(luò)不穩(wěn)定。至于原因,你懂得。估計(jì)等過了這段時(shí)間就沒事了。所以,既然瀏覽器可以訪問,則內(nèi)容就可以訪問到。遇到這個(gè)問題,多試兩次就可以了。
第二個(gè)問題:按照jquery庫時(shí),提示Error: ALREADY_EXISTS。終端顯示如下:
d@dPC:~/Dev/hello-seajs/assets$ spm install gallery.jqueryStart installing ...Downloading: http://modules.spmjs.org/gallery/info.jsonDownloaded: http://modules.spmjs.org/gallery/info.jsonDownloading: http://modules.spmjs.org/gallery/jquery/1.8.2/jquery.tgzDownloaded: http://modules.spmjs.org/gallery/jquery/1.8.2/jquery.tgz** This module already exists: /home/d/Dev/hello-seajs/assets/sea-modules/gallery/jquery/1.8.2Turn on --force option if you want to override it.[ERROR] Caught exception: Error: ALREADY_EXISTS
其實(shí),問題正如反饋信息所示,jQuery庫已經(jīng)存在,不需要再次下載了。我們?cè)趆ello-sea這里例子的源代碼中構(gòu)建,這個(gè)源代碼中已經(jīng)包含了jQuery了,在這里這步可以忽略。
第三個(gè)問題:修改了package.json后,重新編譯報(bào)錯(cuò)。終端顯示如下:
[WARN] http://modules.spmjs.org/GuessNumber/config.json null
這個(gè)不影響編譯,直接忽略就行了。另外說明一下,在第一次打包時(shí),沒見這個(gè)錯(cuò)誤;第二次會(huì)出現(xiàn)。
代碼下載
為了方便大家下載代碼,我把代碼托管到了Github上,大家可以去Github上下載、提交您的修改。Github頁面:GuessNumber;不想去Github上下載的,也可以直接點(diǎn)擊下載:點(diǎn)擊下載。
深入學(xué)習(xí)
上面的例子只是簡要把一個(gè)例子跑起來了,給大家一個(gè)比較形象的認(rèn)知。但是,這個(gè)例子實(shí)在是太簡單了。我還需補(bǔ)充我們剛才為了易于理解而簡化的一些知識(shí)。為了更深入的了解SeaJS,請(qǐng)繼續(xù)閱讀“SeaJS 使用文檔”。另外,這里有幾個(gè)需要重點(diǎn)閱讀,具體如下:
把這個(gè)列表中的東西看完,SeaJS的學(xué)習(xí)應(yīng)該就可以出師了。有好的資料請(qǐng)給我推薦,我再補(bǔ)充上來。
遺留問題
經(jīng)過上面這些折騰,我們已經(jīng)成功運(yùn)行起來一個(gè)使用SeaJS進(jìn)行模塊化編程的例子。但是,我們還是有很多的疑問。具體疑問如下:
- D瓜哥在main.js中,并沒有使用$(document).ready();等DOM加載完再運(yùn)行,并也沒有講JS放到HTML文件的最后,為啥還能順序執(zhí)行呢?莫非SeaJS有什么內(nèi)部機(jī)制,保證在DOM加載完成后再執(zhí)行我們自己編寫的JavaScript代碼?
- 這里例子很小,并沒有很多很多的模塊。在模塊很多的情況下,如果組織模塊?這個(gè)還需要寫更多的例子,實(shí)驗(yàn)一下。
- 同樣,在很多模塊的情況下,難道要建很多目錄準(zhǔn)備很多的main.js,讓眾多的HTML分別加載嗎?
剛剛D瓜哥開竅了一下,main.js只是一個(gè)例子,可以根據(jù)自己的組件名稱命名,然后在組件中加載相對(duì)應(yīng)的JavaScript文件即可。另外,在配置package.json時(shí),突然覺得,在/assets/main/src/下每個(gè)目錄應(yīng)該算是一個(gè)模塊,都有一個(gè)打包的配置文件package.json,用于配置該模塊的必要信息。不知這樣理解是否正確?這個(gè)還有待考證。
未完待續(xù)
這篇文章只是初略地讓大家認(rèn)知一下SeaJS。要想更深入地了解SeaJS的原理,D瓜哥覺得最靠譜的方法就是自己實(shí)現(xiàn)一個(gè)SeaJS。所以,下一篇,D瓜哥準(zhǔn)備自己動(dòng)手實(shí)現(xiàn)一個(gè)簡化版的SeaJS。當(dāng)然,為了便于理解SeaJS,D瓜哥的實(shí)現(xiàn)會(huì)參考“CMD模塊定義規(guī)范”來編寫代碼。敬請(qǐng)期待!
PS:
這篇文章代碼比較多,排版整的不好。看著不是很爽,如有好的建議,請(qǐng)留言提出,D瓜哥立馬改進(jìn)。謝謝!
參考資料:
參考資料在文章都出現(xiàn)了,這里就不再贅述了。
吐槽一下:
我費(fèi)老大勁用SyntaxHighlighter給代碼排出來的、很漂亮的版,到“博客園”一下子都不好使了。只能使用pre塊代替了。希望“博客園”能支持SyntaxHighlighter,SyntaxHighlighter真的很好很強(qiáng)大!
本文章,發(fā)表在博客園的同時(shí),也發(fā)布到我的個(gè)人博客地瓜哥上。轉(zhuǎn)載請(qǐng)注明作者和原文網(wǎng)址。
地瓜哥:http://www.diguage.com/archives/82.html
作者:D瓜哥
出處:http://www.diguage.com/
出處:http://www.cnblogs.com/diguage/
本文版權(quán)歸作者所有;歡迎轉(zhuǎn)載!請(qǐng)注明文章作者和原文連接。