Javascript模塊化編程(三):模塊化編程實戰,試用SeaJS


  看了阮一峰老師的關於JavaScript模塊化的文章后,解答了我思考很久的問題,突然有種豁然開朗的感覺。后來了解到SeaJS,就想寫篇文章,實踐一下模塊化編程。今天把文章寫出來了。發出來,希望對大家有用。

本系列目錄

    1. “JavaScript模塊化編程(一):模塊原型和理論概念詳解”
    2. Javascript模塊化編程(二):模塊化編程實戰,require.js詳解

第三篇文章的兩個“引子”

    1. 給哥三十五次機會,哥就能猜中你的手機號
    2. '猜手機號游戲'的源碼分析:二分查找+面向對象
  1. Javascript模塊化編程(三):模塊化編程實戰,試用SeaJS

 

  前段時間轉載了阮一峰老師的兩篇講解Javascript模塊化編程的文章: “JavaScript模塊化編程(一):模塊原型和理論概念詳解”,介紹了Javascript模塊原型和理論概念;Javascript模塊化編程(二):模塊化編程實戰,require.js詳解,介紹了在實戰中,如何利用RequireJS庫,進行模塊化編程。

  在這兩篇文章發布出來之后,在和網友的交流討論中,了解到了SeaJS,這個由國人玉伯自己創建的模塊化編程庫。然后,我就想學習學習, 再寫篇文章給大家介紹一下。

背景介紹

  官網的資料是最靠譜的。在SeaJS的官網上發現,有一個“5分鍾上手SeaJS”的例子,然后就從這個例子的開始學習。不過,只看明白了個六六七七。我沒看明白和我平時的JavaScript編程有啥區別。另外,我沒有動手實踐,心里面不踏實。所以,動手寫個程序,玩味一下。后來,就想到了“猜手機號游戲”!

  由於官網已經有使用SeaJS的教程,我就不重復這方面的工作了,而且我也覺得我我肯定沒有官網寫的好。由於我不清楚,使用SeaJS進行“模塊化編程”和我平時不進行“模塊化編程”的區別。所以,我准備從另外一個角度來介紹SeaJS:將一個沒有進行模塊化編程的程序,改造成使用SeaJS進行“模塊化編程”的程序。由於這個想法的跨度比較大,信息量也比較多。所以我把我的想法組織成了三篇文章:第一篇文章,“給哥三十五次機會,哥就能猜中你的手機號”,通過一個小游戲,來吸引大家的興趣;第二篇,“‘猜手機號游戲’的源碼分析:二分查找+面向對象”,來講解在沒有進行模塊化編程時,程序的實現細節;然后,這是第三篇,在沒有進行模塊化編程的基礎上,將原來的程序改造成一個使用SeaJS進行模塊化編程的例子。

  在閱讀這篇文章之前,請閱讀前兩篇,尤其是“'猜手機號游戲'的源碼分析:二分查找+面向對象”。同時,還建議閱讀一下“JavaScript模塊化編程(一):模塊原型和理論概念詳解”Javascript模塊化編程(二):模塊化編程實戰,require.js詳解,規范、系統一下關於Javascript模塊化編程的知識。

CMD模塊定義規范介紹

  想享受模塊化編程帶來的良好封裝,就必須遵循模塊化編程的規范。在 SeaJS 中,所有 JavaScript 模塊都遵循 CMD(Common Module Definition) 規范。該規范確定了模塊的基本書寫格式和基本交互方式。所以,使用SeaJS之前,必須閱讀一下SeaJS所要求遵循的規范。

  鑒於規范覆蓋的東西比較多,看多了頭大。所以,我把這個規范提煉簡化一下,只關注我們需要用到的。至於,更詳細的CMD模塊定義規范,等先把例子跑通,理解了整個流程,然后再回頭看規范,梳理、規范這部分知識。

  在介紹簡化版規范之前,D瓜哥提兩個也許大家都回“納悶”的問題:

  1. 如何定義模塊?
  2. 如何獲取外部依賴的模塊?

  CMD模塊定義規范中的主要內容正是回答這兩個問題。下面請看經D瓜哥簡化的規范如下:

    1. 定義、封裝模塊的方法。(CMD模塊定義規范中有好多定義方法。簡單起見,目前只考慮使用如下這一種方式。)如下:
define(function(require, exports, module) {

	// The module code goes here

});

  這里需要特別說明一下,向參數傳遞的三個參數名必須按照代碼所示中那樣寫,不能簡寫,或者使用其他字符串代替;同時,在函數內,exports不能被改寫成其他值;可以把exports看成對象添加屬性,如exports.key,然后對其復制,又如exports.key = "dValue"。

    1. 對外提供模塊接口。在上一步中,我們在函數內定義了模塊,但是這是在函數內定義的,在函數外部不容易訪問到。該怎么向外提供模塊接口呢?定義方法如下:
define(function(require, exports, module) {
  
  // 實用這種方式向外提供模塊接口
  module.exports = {
    foo: 'bar',
    doSomething: function() {};
  };
  
  // 或者。由於,D瓜哥將模塊封裝成了一個對象,所以,本例中,使用這個方式。
  module.exports = yourFunctionName;
  
});

  傳給 factory 構造方法(就是define(function(){})方法參數中那個函數,稱為factory。函數只是factory的一種形式,其他形式以后再補充。)的 exports 參數是 module.exports 對象的一個引用。只通過 exports 參數來提供接口,有時無法滿足開發者的所有需求。 比如當模塊的接口是某個類的實例時,需要通過 module.exports 來實現。D瓜哥這里就是一個對象,所以只能使用module.exports 。

    1. 獲取外部依賴模塊。模塊定義要了,需要使用的時候,就可以使用require函數獲取外部依賴。具體代碼如下:
define(function(require, exports, module) {

  // 使用require函數獲取外部依賴
  var a = require('./a');
  a.doSomething();

});

  require函數的參數是a.js文件的相對路徑。后綴名可以省略,在SeaJS加載模塊的時候會自動加上的。另外,這里可以執行回調函數。不過,我們的任務是跑起來。因為不需要回調函數,所以這部分先略過了。

  總結一下:define函數,定義模塊;module對象,保存模塊信息;require函數,獲取外部依賴模塊。

  看到這里,估計大家還是一頭霧水。沒關系,慢慢往下看,下面的例子跑起來的時候,你再回頭看就會明白的。

模塊化改造

  先聲明一下,下面的改造過程會參考“5分鍾入門”的說明。所以,建議大家先看看。當然,一起看也可以。

  通過看"5分鍾入門"的例子可以看出,SeaJS的目錄結構還是有點復雜的。所以,最簡單的方法就是,把她的例子下載下來,在她的基礎之上修改:"5分鍾入門"例子下載

目錄結構

  下載完成后,解壓到任意目錄下。請看一下目錄,

  1. hello-seajs/下放我們的html文件;
  2. hello-seajs/assets/sea-modules下存放的是我們需要用到的第三方模塊塊;
  3. hello-seajs/assets/main,這個目錄可以說最重要,是存放我們自己編寫的JavaScript和CSS文件的地方。下面還有四個子目錄及一個文件:
    1. src存放正常的代碼;
    2. test存放測試代碼;
    3. docs存放文檔;
    4. examples存放示例代碼;
    5. package.json是打包的配置文件;

“改造”模塊代碼

  下面,我們開始改造我們的模塊。

  首先,把我GuessNumber.js放到hello-seajs/assets/main/src/下。然后,按照“第1條規范”的要求改造這個文件中代碼。由於整個文件就是GuessNumber對象的定義。同時,這個JavaScript文件又沒有引用其他模塊。所以,只需要在文件的第一行增加define,在最后一行增加括號分號就行。具體代碼如下:

define(function(require, exports, module){
	/**
	* numberScope 需要猜測的數字范圍
	*/
	function GuessNumber(numberScope){

        // 為了突出修改的代碼,我把一些相同的代碼省略了,
        // 完整代碼請看:http://www.diguage.com/archives/80.html
	
    }
	
	GuessNumber.prototype = {
    
		constructor: GuessNumber,
	
		// 完整代碼請看:http://www.diguage.com/archives/80.html
	}

});

  其次,目前我們已經定義為一個模塊。但是外部如何訪問這個GuessNumber?所以,我們要向外部提供一個接口,提供方式參考“第2條規范”。具體代碼見第18行:

define(function(require, exports, module){
	/**
	* numberScope 需要猜測的數字范圍
	*/
	function GuessNumber(numberScope){
    
		// 完整代碼請看:http://www.diguage.com/archives/80.html
	
    }
	
	GuessNumber.prototype = {
    
		constructor: GuessNumber,
	
		// 完整代碼請看:http://www.diguage.com/archives/80.html
	}

	module.exports = GuessNumber;
    
});

  這時,一個接口已經全部定義完成。下面,我們書寫調用這個模塊的例子。

  在“規范”的第三條中,我們說明了加載外部依賴模塊的方法,我們只需要按說明照做就行。另外,還需要補充一下模塊加載時需要注意的地方。具體請看代碼注釋:

define(function(require) {
	// 這是引入jQuery類庫,我們下面說明為什么這樣下。
    var $ = require('jquery');
	
	// 引入GuessNumber模塊,也就是GuessNumber.js文件。
	// 參數中傳遞的是GuessNumber.js文件的相對路徑。
	// .js的后綴名可以省略,SeaJS在加載的時候會自動加上。
	var GuessNumber = require("./GuessNumber");
    
    // 完整代碼請看:http://www.diguage.com/archives/80.html
	
	//格式化顯示結果
	function formatResult(num, type) {
		//……
	}
	
	// ……
	
	$("#initButton").click(function(){
		guess.start(scopeArr[type].min, scopeArr[type].max);
		showResult();
	});
});

  從上面的代碼中,可以看出,main.js文件的改造,只是把原來的

$(document).ready(function(){

	// 主要的業務代碼

});

改造成了,

define(function(require) {
	// 這是引入jQuery類庫,我們下面說明為什么這樣下。
    var $ = require('jquery');
	
	// 引入GuessNumber模塊,也就是GuessNumber.js文件。
	// 參數中傳遞的是GuessNumber.js文件的相對路徑。
	// .js的后綴名可以省略,SeaJS在加載的時候會自動加上。
	var GuessNumber = require("./GuessNumber");
    
    // 和原文件相同的業務代碼

});

另外,加了兩行倒入必要關聯模塊的代碼。僅此而已。

  main.js與GuessNumber.js不同的還有一點,main.js不需要向外提供訪問接口。這點也要注意一下。

  到這里所有的JavaScript都已經修改完畢了。下面,我們修改一下如何在HTML中的引入方式。

在頁面中加載模塊

  原來的寫法是,按順序使用<scrip>標簽把jQuery、GuessNumber.js以及main.js文件引入到HTML頁面中即可。如果使用SeaJS,則需要先加載SeaJS的類庫,然后使用JavaScript通過SeaJS的接口來加載所需的模塊,也就是模塊對應的JavaScript文件。具體代碼如下:

<!-- 首先,首先我們需要引入 sea.js -->
<script src="assets/sea-modules/seajs/1.3.1/sea.js"></script>
<script type="text/javascript">
seajs.config({
alias: {
// 指定使用的jQuery版本以及說明jQuery的路徑
// 請注意:這里知名了jQuery的路徑,所以,我們
// 在引入jQuery庫時,只需要填寫jquery即可。
'jquery': 'gallery/jquery/1.8.2/jquery'
}
});

// 然后SeaJS通過 use 方法來加載模塊,以后打包后也是修改這里
// 也許你會疑問為什么不加載GuessNumber.js文件,
// 這個在使用require引入依賴時,SeaJS自動加載需要的外部文件
// 另外,這里的.js后綴名也可以省略,SeaJS會自動補全。
seajs.use('./assets/main/src/main');

</script>
<!-- 這里只展示了和JavaScript引入相關的代碼 -->
<!-- 完整代碼請看:http://www.diguage.com/archives/80.html 中的HTML代碼 -->

  到此,改造工作就全部完成了。你可以打開一下inde.html文件,看看效果了。

打包部署

  根據“高性能網站的十四條黃金法則”中的實踐,我們在實際項目上線時,為了提高頁面的加載速度,必定要壓縮一下JavaScript文件。這些,SeaJS也考慮到了,甚至做得更好:還做了文件合並。

  這里,需要先介紹一下,SPM,一個基於命令行的前端項目管理工具。 SPM 和 SeaJS 關系密切,你甚至可以認為SPM是為SeaJS專門打造的工具。首先,請“安裝教程”安裝好這個工具。按照過程可能會有一個問題,請參考下面的“出現的問題”。

  使用SPM打包,需要修改一下打包的配置文件。配置文件是:hello-seajs/assets/main/package.json。打開后內容如下:

{
  "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}}"
    }
  }
}

  不過,這個需要根據我們的實際情況來修改。root屬性,由於我們的模塊是“猜數”,所以將其修改為GuessNumber;output屬性,我們只需要輸出JS,所以刪除main.css。另外,需要注意,第十四行,這個是打包后的輸出路徑。好了,開始打包。打包需要執行如下指令:

$ cd hello-seajs/assets/main
$ spm build 
...
BUILD SUCCESS!
$

  打包結束后,在hello-seajs/assets/中就會發現多了一個GuessNumber文件夾,那個就是打包輸出出來。

  這里說明一下:D瓜哥只在Linux下執行了這么命令。不知在Windows是否好使。為了方便大家測試運行,打包結果已提交,下載的代碼中包含打包結果。

  觀察這個結果,大家會發現只有一個main.js和main-debug.js;顧名思義,main.js是用於生產部署的,經過壓縮的文件;main-debug.js是為測試使用的,只是合並了代碼並沒有壓縮,使用的時候直接引用這個兩個文件中的一個就行,直接把seajs.use()中的路徑改一下就OK。GuessNumber.js哪里去了啊?大家可以打開main-debug.js看看(main.js也行,只是壓縮過來,可讀性不好),原來,GuessNumber.js已經合並到了main.js中了。SPM把兩個文件合並成一個文件了,這樣在瀏覽器訪問網頁時,就可以減少一個HTTP請求,提高網頁的加載速度。

  另外,大家也可能會注意到在原來main.js中定義的define()函數,在新的main.js有了一些變化,多了兩個參數:第一個參數模塊的ID,主要是為了方便區別一個文件中的各個模塊;第二個參數是模塊依賴的外部模塊的路徑,因為依賴的模塊可能有多個,所以這個參數是一個數組。第三個參數是原來的function,也就是factory。更詳細的解釋請看:為什么要用 spm 來壓縮 CMD 模塊?

  懶人要把懶進行到底!打包后還要修改SeaJS的加載路徑,這點其實還可以使用如下代碼來避免:

// 這個路徑只有在部署到服務器上才行,直接打開文件不好使。
seajs.use(location.host === 'localhost' ? './assets/main/src/main' : 'GuessNumber/main/1.0.0/main');

如果是非靜態頁面,也可以使用變量來配置。

折騰中出現的問題

  折騰這么個玩意,難免出現一些問題,D瓜哥遇到了三個問題。這些問題主要集中在SPM環境搭建過程中。給大家分享一下。

  第一個問題:按照seajs時,提示info.json不存在的錯誤。終端顯示如下:

d@dPC:~/Dev/hello-seajs/assets$ spm install seajs
Start installing ...
success create global config.json to /home/d/.spm
Downloading: 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,會發現可以打開。這是怎么回事呢?

  我查閱了一下SeaJS論壇,里面有類似的問題。其中的一個回復,我拿過來當作解答吧:這段時間是舉國同慶的日子,網絡不穩定。至於原因,你懂得。估計等過了這段時間就沒事了。所以,既然瀏覽器可以訪問,則內容就可以訪問到。遇到這個問題,多試兩次就可以了。

  第二個問題:按照jquery庫時,提示Error: ALREADY_EXISTS。終端顯示如下:

d@dPC:~/Dev/hello-seajs/assets$ spm install gallery.jquery
Start installing ...
Downloading: http://modules.spmjs.org/gallery/info.json
Downloaded: http://modules.spmjs.org/gallery/info.json
Downloading: http://modules.spmjs.org/gallery/jquery/1.8.2/jquery.tgz
Downloaded: 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.2
Turn on --force option if you want to override it.
[ERROR] Caught exception: Error: ALREADY_EXISTS

  其實,問題正如反饋信息所示,jQuery庫已經存在,不需要再次下載了。我們在hello-sea這里例子的源代碼中構建,這個源代碼中已經包含了jQuery了,在這里這步可以忽略。

  第三個問題:修改了package.json后,重新編譯報錯。終端顯示如下:

[WARN] http://modules.spmjs.org/GuessNumber/config.json null  

  這個不影響編譯,直接忽略就行了。另外說明一下,在第一次打包時,沒見這個錯誤;第二次會出現。

代碼下載

  為了方便大家下載代碼,我把代碼托管到了Github上,大家可以去Github上下載、提交您的修改。Github頁面:GuessNumber;不想去Github上下載的,也可以直接點擊下載:點擊下載

深入學習

  上面的例子只是簡要把一個例子跑起來了,給大家一個比較形象的認知。但是,這個例子實在是太簡單了。我還需補充我們剛才為了易於理解而簡化的一些知識。為了更深入的了解SeaJS,請繼續閱讀“SeaJS 使用文檔”。另外,這里有幾個需要重點閱讀,具體如下:

  1. CMD模塊定義規范
  2. require 書寫約定
  3. 模塊標識
  4. API 快速參考
  5. 模塊的加載啟動,重點看里面的"最佳實踐"
  6. 模塊系統

  把這個列表中的東西看完,SeaJS的學習應該就可以出師了。有好的資料請給我推薦,我再補充上來。

遺留問題

  經過上面這些折騰,我們已經成功運行起來一個使用SeaJS進行模塊化編程的例子。但是,我們還是有很多的疑問。具體疑問如下:

    1. D瓜哥在main.js中,並沒有使用$(document).ready();等DOM加載完再運行,並也沒有講JS放到HTML文件的最后,為啥還能順序執行呢?莫非SeaJS有什么內部機制,保證在DOM加載完成后再執行我們自己編寫的JavaScript代碼?
    2. 這里例子很小,並沒有很多很多的模塊。在模塊很多的情況下,如果組織模塊?這個還需要寫更多的例子,實驗一下。
    3. 同樣,在很多模塊的情況下,難道要建很多目錄准備很多的main.js,讓眾多的HTML分別加載嗎?

  剛剛D瓜哥開竅了一下,main.js只是一個例子,可以根據自己的組件名稱命名,然后在組件中加載相對應的JavaScript文件即可。另外,在配置package.json時,突然覺得,在/assets/main/src/下每個目錄應該算是一個模塊,都有一個打包的配置文件package.json,用於配置該模塊的必要信息。不知這樣理解是否正確?這個還有待考證。

未完待續

  這篇文章只是初略地讓大家認知一下SeaJS。要想更深入地了解SeaJS的原理,D瓜哥覺得最靠譜的方法就是自己實現一個SeaJS。所以,下一篇,D瓜哥准備自己動手實現一個簡化版的SeaJS。當然,為了便於理解SeaJS,D瓜哥的實現會參考“CMD模塊定義規范”來編寫代碼。敬請期待!

PS:

  這篇文章代碼比較多,排版整的不好。看着不是很爽,如有好的建議,請留言提出,D瓜哥立馬改進。謝謝!

 

參考資料:

  參考資料在文章都出現了,這里就不再贅述了。

吐槽一下:

  我費老大勁用SyntaxHighlighter給代碼排出來的、很漂亮的版,到“博客園”一下子都不好使了。只能使用pre塊代替了。希望“博客園”能支持SyntaxHighlighter,SyntaxHighlighter真的很好很強大!

  本文章,發表在博客園的同時,也發布到我的個人博客地瓜哥上。轉載請注明作者和原文網址。
地瓜哥:http://www.diguage.com/archives/82.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM