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

來源:互聯網
上載者:User
文章目錄
  • 本系列目錄
  • 背景介紹
  • CMD模組定義規範介紹
  • 模組化改造
  • 深入學習
  • 遺留問題
  • 未完待續

  看了阮一峰老師的關於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 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,會發現可以開啟。這是怎麼回事呢?

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

  第二個問題:按照jquery庫時,提示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

  其實,問題正如反饋資訊所示,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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.