前言 - 碁峰資訊epaper.gotop.com.tw/pdfsample/a363.pdf · 2015-03-26 ·...

19
前言 介紹 坊間有許多書籍在探討本書所涵蓋的 Web 技術,然而只有極少數能夠推薦給想要學習如 何從頭開始建造完整 JavaScript 應用程式的讀者,同時,幾乎每一家新興科技公司都需 要熟知 JavaScript 應用程式開發的人才。這本書的存在只有一個目的︰幫助你瞭解如何 打造容易擴展及維護的完整 JavaScript 應用程式。 本書不打算教導你有關 JavaScript 的基礎知識,相反地,它將奠基於你既有的知識,探 討讓你的程式碼更容易操作且與時俱進的 JavaScript 功能和技術。通常隨著應用程式增 長,添加新功能及修正臭蟲的工作將越來越困難,你的程式碼會變得過於僵化且脆弱 不堪,甚至小小的變更就會導致巨幅的重構(refactor)。若是遵循本書所說明的設計 模式,你的程式碼就能夠保持靈活且有彈性,而不致於因為小小的變更就牽一髮而動 全身。 本書主要聚焦在客戶端架構,但也涵蓋一些伺服端主題,像是基本的 RESTful Node。趨勢上,許多應用程式邏輯正被推向客戶端。在過去,伺服器環境會負責處理模 板機制(templating)以及與供應商服務(vendor service)溝通的工作,但現在開發者 經常在瀏覽器裡處理這些任務。 事實上,現代化 JavaScript 應用程式幾乎會在瀏覽器裡完成傳統桌面應用程式所做的一 切。當然,伺服器還是很有用處的,伺服器經常負責提供靜態的內容與動態加載的模 組,資料永續儲存、動作記錄,以及與第三方 API 溝通的工作。

Upload: others

Post on 25-Jul-2020

13 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

前言

介紹

坊間有許多書籍在探討本書所涵蓋的Web技術,然而只有極少數能夠推薦給想要學習如何從頭開始建造完整 JavaScript應用程式的讀者,同時,幾乎每一家新興科技公司都需要熟知 JavaScript應用程式開發的人才。這本書的存在只有一個目的︰幫助你瞭解如何打造容易擴展及維護的完整 JavaScript應用程式。

本書不打算教導你有關 JavaScript的基礎知識,相反地,它將奠基於你既有的知識,探討讓你的程式碼更容易操作且與時俱進的 JavaScript功能和技術。通常隨著應用程式增長,添加新功能及修正臭蟲的工作將越來越困難,你的程式碼會變得過於僵化且脆弱

不堪,甚至小小的變更就會導致巨幅的重構(refactor)。若是遵循本書所說明的設計模式,你的程式碼就能夠保持靈活且有彈性,而不致於因為小小的變更就牽一髮而動 全身。

本書主要聚焦在客戶端架構,但也涵蓋一些伺服端主題,像是基本的 RESTful 與Node。趨勢上,許多應用程式邏輯正被推向客戶端。在過去,伺服器環境會負責處理模板機制(templating)以及與供應商服務(vendor service)溝通的工作,但現在開發者經常在瀏覽器裡處理這些任務。

事實上,現代化 JavaScript應用程式幾乎會在瀏覽器裡完成傳統桌面應用程式所做的一切。當然,伺服器還是很有用處的,伺服器經常負責提供靜態的內容與動態加載的模

組,資料永續儲存、動作記錄,以及與第三方 API溝通的工作。

Page 2: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

viii | 前言

本書將說明︰

• JavaScript功能與應用程式開發者的最佳實務

• 程式碼組織、模組化與重利用

• 客戶端的關注點分離(MVC等)

• 與伺服器及 API溝通

• 使用 Node.js設計及編程 RESTful API

• 建造、測試、協作、部署、及擴充應用程式

• 透過國際化擴展應用程式的服務範圍

誰適合閱讀本書

你具有一些 JavaScript經驗,至少有一、二年的時間經常在使用這個語言,但想要更深入地瞭解如何運用它來打造強健的Web規模或企業級應用程式。

你通曉編程工作,但渴望學習更多東西,尤其是想要更深入瞭解如何運用 JavaScript有別於其他語言的強大功能,例如,閉包(closure)、函式編程(functional programming)與原型繼承(prototypal inheritance)⋯等(即使你是第一次聽到這些術語)。

或許,你還想要瞭解如何將測試驅動開發(test-driven development,TDD)的技術運用到你的下一個 JavaScript應用程式。貫穿本書範例,我們撰寫了許多測試程式碼,因此讀完本書後,你應該會很習慣去思考如何測試所撰寫的程式碼。

誰或許應該遠離這本書

這本書在有限的篇幅中涵蓋甚廣的範圍,絕不是針對初學者撰寫的。如果你需要澄清一

些觀念,可以參考《JavaScript: 優良部分》(Douglas Crockford 著,O’Reilly,2008)與《JavaScript大全》(David Flannagan著,O’Reilly,2011),若是想要知道一些關於軟體設計模式的知識,請參閱由著名的四人幫所撰寫的《Design Patterns: Elements of Reusable Object-Oriented Software》(http://bit.ly/1pwzcUc)(Erich Gamma、Richard Helm、Ralph Johnson與 John Vlissides,Addison-Wesley,1994)。

Google 和 Wikipedia 也是幫助你釐清一些觀念的方便指南。關於軟體設計模式,Wikipedia是相當不錯的參考資料。

Page 3: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

前言 | ix

如果你是第一次接觸 JavaScript,在試圖深入本書之前,最好先研究一些介紹性的文章與教學指南。我個人最喜歡的是《Eloquent JavaScript》(Marijn Haverbeke 著,No Starch Press,2011)(http://eloquentjavascript.net/)。請務必參閱《JavaScript: 優良部分》並且仔細閱覽附錄 A,進而瞭解有經驗之 JavaScript開發者會犯下哪些錯誤。

單元測試

單元測試的重要性絕非三言兩語所能道盡。我們運用了大量的單元測試來貫穿本書,讀

完本書之際,你應該對撰寫單元測試感到很習慣。在實作你所學到的觀念時,先從撰寫

測試開始,會讓你對問題的範疇具有較好的掌握,並且迫使你更深入地思考解決方案的

設計以及你為它創造的介面。此外,設計單元測試也能夠防止程式碼緊密耦合的現象發

生。總之,撰寫可測試、解耦合之程式碼的紀律,會讓你的編程職涯發展得更為順遂。

關於單元測試與程式碼風格的資訊,請參閱附錄 A。

本書編排慣例

本書使用的字體慣例如下所示:

斜體字(Italic)用以表示新術語、URL、電子郵件信箱、檔名及副檔名⋯等。

定寬字(Constant width)用以表示程式碼範例,以及在段落中指明程式元素,像是變數或函式名稱、資料

庫、資料型別、環境變數、陳述式與關鍵字⋯等。

定寬粗體字(Constant width bold)用以表示使用者必須如實鍵入的指令或其他文字。

定寬斜體字(Constant width italic)用以表示應以使用者提供或上下文決定之值取代的文字。

這個圖示表示建議、提示,或一般性註釋。

Page 4: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

第四章

模組

模組(module)是可重利用的軟體元件,這些軟體元件形成了應用程式的建構區塊(building block)。模組化(modularity)滿足一些非常重要的設計目標,當中最重要的或許就是簡潔性(simplicity)。

如果你正在設計的應用程式是由一堆相互依賴的不同部分所組成,那麼,要完全掌握某

些修改將對整個系統造成什麼影響,恐怕會變得更加困難。

相反地,假如你將系統的各個部分設計成符合模組化介面的約定,就可以安全地進行修

改,而不需要深入瞭解所有相關模組。

模組化的另一個重要目標是讓你的模組能夠在其他應用程式中被重利用。基於類似框架

且設計良好的模組應該很容易被移植到新的應用程式中,而幾乎不需要什麼修改(即使

需要也很少)。透過為應用程式擴展定義標準介面,然後在該介面上建構新功能,你能

夠輕鬆地建構易於擴展及維護的應用程式,這樣的應用程式也很容易在未來重新組裝成

不同的形態。

JavaScript模組是封裝式的(encapsulated)。意思是模組會將實作細節保留為私有,而只開放一組公用 API。這樣的話,你就可以只改變模組內部的行為,而無需修改那些依賴它的外部程式碼。另外,封裝也提供一種保護,那表示它會防止外部程式碼干擾模組

內部的功能。

在 JavaScript裡有一些方法可以定義模組,其中最普遍、最常見的有模組模式(module pattern)、CommonJS 模組規範(啟發 Node 模組)和 AMD(Asynchronous Module Definition,非同步模組定義)規範。

Page 5: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

80 | 第四章

模組化的原則

你可以將模組想成是小巧且獨立的應用程式,每個模組本身都具備完整的功能性與可測

試性,盡可能讓它們保持簡單小巧好專注於自身的職責。

模組應該是:

專職的

每個模組都應該具有非常特定的功能。模組的各個部分應該緊密結合、共同解決模

組的份內工作,對外開放的公用 API應該簡單且乾淨。

獨立的

模組對其他模組的瞭解應該越少越好。代替直接呼叫其他模組,它們應該透過中介

者(mediator)進行溝通,像是中央事件處理系統(central event-handling system)或命令物件(command object)。

可分解的

以相互隔絕的方式測試及使用模組應該要十分簡單。模組通常可被比喻為組合音響

的各個元件,組合音響可能包含影碟機、收音機、電視機、擴大器與揚聲器,所有

元件都可以獨立運作,即使你移走影碟機,其他元件也能夠繼續運作。

可重組的

應該能夠以不同的方式將各種模組組合在一起以建構相同軟體的不同版本,或是完

全不同的應用程式。

可替換的

應該可以使用另一個模組完全替代某個模組。只要它們提供相同的介面,應用程式

的其餘部分不應該因為這項改變而受到負面的影響,而且,替代模組不需要執行相

同的功能。舉例來說,系統中有一個使用 REST端點(endpoint)作為資料來源的資料模組,你可能想要把它替換成一個使用本地儲存資料庫作為資料來源的模組。

開閉原則(Open Closed Principle)聲明,模組介面應該「允許擴充而

開放,禁止修改而關閉」。修改大量軟體所依賴的介面往往是一項令人怯

步的任務,介面一旦被建立,最好避免被修改,無論如何,軟體應該不斷

演進(事實也是如此),因此為既有介面擴充新功能不應該是什麼問題。

Page 6: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

模組 | 81

介面

針對介面編程,而不是針對實作。

—四人幫,《Design Patterns》

介面是模組化軟體設計的基本工具之一。介面定義了模組實作要實現的約定,譬如說,

JavaScript應用程式有個常見的問題,如果網路連接斷線,應用程式就會停止運作。為了解決這個問題,你可能會使用本地儲存機制,並且定期與伺服器同步化資料的改變。

令人遺憾地,部分瀏覽器並不支援本地儲存機制,所以你可能必須降級到 cookie或甚至Flash的儲存機制(取決於你需要儲存多少資料)。

假設你正在撰寫讓使用者發文的軟體,而本地儲存機制有效的話,你想要將文章儲存在

localStorage。假如無效的話,就會降級到 cookie儲存機制。

如果你的業務邏輯直接依賴 localStorage機制,事情就會變得比較棘手(如圖 4-1 所示):

圖 4-1 直接依賴

比較好的替代方案是建立為發文模組提供資料存取的標準介面,那樣的話,發文模

組就能夠使用相同的介面來儲存資料,而不用管資料實際上被存放在哪裡(如圖 4-2 所示)。

圖 4-2 介面

Page 7: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

82 | 第四章

其他語言針對介面提供原生支援,可用來指定介面的要求。你可能聽過別種說法,像是

抽象基礎類別(abstract base class)或純虛擬函式(pure virtual function)。

在 JavaScript中,類別、介面和物件實例之間並無區別,一切都是物件實例。這種簡化其實也是一件好事,你可能會想:假如 JavaScript沒有針對介面提供原生支援,我們又何必費心去撰寫一個呢?

當你需要為相同介面產生多個實作時,最好有一種標準的程式碼寫法去明確指出這個

介面的內容究竟為何。撰寫能夠自我表述的程式碼是很重要的。例如,儲存介面可能

有一個必要的 .save()方法,你可以撰寫一個預設實作,在你忘記實作它時丟出錯誤。因為這是有效的原型,你甚至可以撰寫不丟出錯誤的合理預設實作。在此案例中,假

如 .save()方法未被實作,工廠就會丟出錯誤。

使用 Stampit來定義工廠:

(function (exports) { 'use strict';

// 確認有支援本地儲存機制 var ns = 'post', supportsLocalStorage = (typeof localStorage !== 'undefined') && localStorage !== null,

storage,

storageInterface = stampit().methods({ save: function saveStorage() { throw new Error('.save() method not implemented.'); } }),

localStorageProvider = stampit .compose(storageInterface) .methods({ save: function saveLocal() { localStorage.storage = JSON.stringify(storage); } }),

cookieProvider = stampit .compose(storageInterface) .methods({ save: function saveCookie() {

Page 8: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

模組 | 83

$.cookie('storage', JSON.stringify(storage)); } }),

post = stampit().methods({ save: function save() { storage[this.id] = this.data; storage.save(); return this; }, set: function set(name, value) { this.data[name] = value; return this; } }) .state({ data: { message: '', published: false }, id: undefined }) .enclose(function init() { this.id = generateUUID(); return this; }),

api = post;

storage = (supportsLocalStorage) ? localStorageProvider() : cookieProvider();

exports[ns] = api;

}((typeof exports === 'undefined') ? window : exports ));

$(function () { 'use strict';

var myPost = post().set('message', 'Hello, world!');

test('Interface example', function () { var storedMessage,

Page 9: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

84 | 第四章

storage;

myPost.save(); storage = JSON.parse(localStorage.storage); storedMessage = storage[myPost.id].message;

equal(storedMessage, 'Hello, world!', '.save() method should save post.'); });});

這裡的重點是儲存介面(storage interface)。首先,你建立工廠(此案例使用Stampit,但也可以是任何回傳「演示介面之物件」的方法)。這個範例只包含一個方法:.save()。

storageInterface = stampit().methods({ save: function saveStorage() { throw new Error('.save() method not implemented.'); } }),

建立繼承介面的具體實作,假如介面特別龐大,你可能會想要把每個實作放進獨立的檔

案中。而這個案例不需要這麼做。注意,這些具體實作皆使用具名函式表達式(named function expression)。偵錯期間,你可以透過檢視呼叫堆疊裡的函式名稱來判斷你正在使用哪個具體實作:

localStorageProvider = stampit .compose(storageInterface) .methods({ save: function saveLocal() { localStorage.storage = JSON.stringify(storage); } }),

cookieProvider = stampit .compose(storageInterface) .methods({ save: function saveCookie() { $.cookie('storage', JSON.stringify(storage)); } }),

Stampit的 .compose()方法允許你繼承任意數量的來源(source),並且回傳你可以進一步使用 .methods()、.state()、或 .enclose()來擴展的 stamp,你可以利用這些功能來完成具體的實作。

Page 10: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

模組 | 85

最後一步是決定要使用哪個實作。下列三元運算式檢查 localStorage是否被支援,如果是的話,就使用 localStorageProvider();否則,就降級到 cookie儲存機制:

storage = (supportsLocalStorage) ? localStorageProvider() : cookieProvider();

在 JavaScript中,還有一些定義介面的替代做法,例如,你可以簡單地定義物件實字,並且使用 jQuery.extend()之類的東西建立想要的具體實作,缺點是無法利用原型委託或資料隱私的機制。

你也可以將具體實作定義為原型物件,接著在最後的步驟中將合適的原型傳遞給

Stampit 或 Object.create()。我偏好使用 stamp,因為它賦予你更多彈性與可組合性(composability)。

創造「設計模式」的「四人幫」之一的 Erich Gamma,在接受 Bill Venners訪談時分享了一些關於介面的有趣想法,請參考〈Leading-Edge Java Design Principles from Design Patterns: A Conversation with Erich Gamma, Part III〉(http://www.artima.com/lejava/

articles/designprinciples.html)。

模組模式

瀏覽器裡的模組使用包裹函式(wrapping function)將私有資料封裝在閉包中(例如,透過 IIFE來實現,請參考第 21頁的「即刻調用函式表達式」)。如果沒有 IIFE提供的封裝函式作用域,其他指令稿可能試圖使用相同的變數與函式名稱,那會導致一些意想

不到的行為。

大多數程式庫,像是 jQuery與 Underscore,都被封裝在模組中。

模組模式(module pattern)將模組內容封裝在 IIFE(即刻調用函式表達式)然後透過指定(assignment)對外開放公用介面。Douglas Crockford提出「模組模式」這個名詞,而 Eric Miraglia透過 YUI部落格上的一篇著名文章將它推廣開來(http://yuiblog.com/

blog/2007/06/12/module-pattern/)。

最初的模組模式是將 IIFE的執行結果指定給預先定義的名稱空間變數:

var myModule = (function () { return { hello: function hello() { return 'Hello, world!'; }

Page 11: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

86 | 第四章

};}());

test('Module pattern', function () { equal(myModule.hello(), 'Hello, world!', 'Module works.');});

這種模式存在著一個問題,你必須針對每個模組暴露至少一個全域變數。如果你正在建

構的應用程式擁有大量的模組,這顯然不是一個好辦法。相反地,我們可以傳進既有的

變數,並使用你的新模組來擴展它。

在此,為了兼容於 CommonJS(關於 CommonJS的解釋,請參閱第 91頁的「Node風格的模組」),這個變數被命名為 exports。如果 exports不存在,你還可以退回 window全域物件:

(function (exports) { var api = { moduleExists: function test() { return true; } }; $.extend(exports, api);}((typeof exports === 'undefined') ? window : exports));

test('Pass in exports.', function () { ok(moduleExists(), 'The module exists.');});

常見的錯誤是,在模組的原始碼檔案中把特定的應用程式名稱空間傳進去(而不是使用

全域定義的 exports)。一般來說,這不會導致什麼嚴重的後果,但如果你想要在其他應用程式中重利用這個模組,就必須修改模組的原始碼,以便將它繫結到正確的名稱 空間。

合適的替代做法是,將你的應用程式物件傳進去作為 exports。在客戶端裡採取一個建置步驟,將你的所有模組包裹到單一的外層函式是一種很常見的做法。如果你把應用程

式物件當作名為 exports的參數傳進這個外層包裹函式中,一切就算是準備就緒了:

Page 12: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

模組 | 87

var app = {};

(function (exports) {

(function (exports) { var api = { moduleExists: function test() { return true; } }; $.extend(exports, api); }((typeof exports === 'undefined') ? window : exports));

}(app));

test('Pass app as exports.', function () { ok(app.moduleExists(), 'The module exists.');});

最後這一版的模組模式有個好處,使用它來撰寫的程式碼很容易就可以在 Node中執行與測試。現在開始,每當模組模式被提及時,你的腦海裡就應該立刻蹦出這個版本,早

先的版本已經過時了。

非同步模組定義(AMD)客戶端經常需要在執行時期以非同步的方式來載入模組,以避免每次應用程式加載時

客戶端都必須下載整個程式碼基礎。想像一下,假如你擁有一個 Twitter之類的應用程式,用戶可以在上面發佈訊息或更新狀態,而這個應用程式的核心功能是訊息傳遞,

然而,你還擁有一個龐大的帳號資料編輯模組,讓用戶客製化個人主頁(profile)的 外觀。

一般來說,用戶偶爾才會更新他們的個人主頁(一年幾次吧),因此整個帳號資料編輯

模組(差不多五萬行)在百分之九十九的情況下根本不會用到。因此你需要一種方式,

將帳號資料編輯模組的加載遞延到用戶真正進入編輯模式的時候,你可以讓它成為一個

獨立的頁面,但這樣的話,用戶就必須忍受頁面重整的過程,而用戶想要做的也許只是

更新他的頭像。讓所有動作在一個頁面裡完成,而不要載入新頁面,才能夠為用戶帶來

更美好的操作體驗。

Page 13: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

88 | 第四章

模組模式並不能解決這個問題,CommonJS模組(如 Node所採用的那些)也不是非同步的。在未來,JavaScript將具有在瀏覽器中運作的原生模組系統(參見第 95頁的「ES6模組」),但它仍是一項非常新穎的技術,在可預見的未來還不太可能在所有主流瀏覽器中普遍被實作。

非同步模組定義(AMD,asynchronous module definition)是這個問題的過渡性解法,它的運作方式是把模組包裹在名為 define()的函式中,其調用語法如下:

define([moduleId,] dependencies, definitionFunction);

moduleId參數是識別該模組的字串,不過這個參數現在已經失寵了,因為應用程式或模組結構的修改將無可避免地導致重構,我們真的不需要在一開始就賦予模組一個 ID。如果你忽略它,直接從依賴清單(dependencies)開始你的 define呼叫,就會建立一個更具有適應性的匿名模組。

define(['ch04/amd1', 'ch04/amd2'], function myModule(amd1, amd2) { var testResults = { test1: amd1.test(), test2: amd2.test() },

// 為你的模組定義公用 API: api = { testResults: function () { return testResults; } };

return api; });

要啟用這個模組,就呼叫 require()。你可以像 define()那樣指定依賴關係:

require(['ch04-amd'], function (amd) { var results = amd.testResults();

test('AMD with Require.js', function () { equal(results.test1, true, 'first dependency loaded correctly.');

equal(results.test2, true, 'Second dependency loaded correctly.'); });});

Page 14: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

模組 | 89

盡量使用匿名模組,以避免重構。

這種做法的問題是,如果你像這樣定義你的模組,它就只能透過 AMD載入器(AMD loader)而被使用,像是 Require.js和 Curl.js(兩種廣受歡迎的 AMD載入器)。不過,我們可以結合 AMD與模組模式的好處,使用模組模式簡單建立你的模組,並且在包裹函式的結尾加上這個:

if (typeof define === 'function') { define([], function () { return api; });}

那樣一來,假如你想要的話,就能夠以非同步的方式載入你的模組。然而,如果你使用

簡單的 script標籤來載入你的模組,或者把它跟一群其他模組一起編譯的話,你的模組還是能夠正常運作。這種做法唯一的麻煩就是,必須特別當心依賴關係的時間性問題,

在試圖使用模組之前,你必須確保依賴模組都已經完成載入。

UMD(Universal Module Definition,通用模組定義)是另一種選擇。我最喜歡的 UMD建立方式就是使用 Browserify以獨立模式的方式打包(bundle)模組。請參閱第 96頁的「使用 CommonJS、npm、Grunt與 Browserify建構客戶端程式碼」。

插件

載入器插件是讓你載入非 JavaScript資源的 AMD機制,像是模板與 CSS。Require.js提供 text!插件,你可以透過它載入你的 HTML模板。要使用插件,只需在檔案路徑前面冠上插件名稱即可:

'use strict';require(['ch04/mymodule.js', 'text!ch04/mymodule.html'], function (myModule, view) { var container = document.body, css = 'ch04/mymodule.css';

myModule.render(container, view, css);

test('AMD Plugins', function () { equal($('#mymodule').text(), 'Hello, world!', 'Plugin loading works.'); });});

Page 15: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

90 | 第四章

下面是 mymodule.js:

define(function () { 'use strict'; var api = { render: function render(container, view, css) { loadCss('ch04/mymodule.css');

$(view).text('Hello, world!') .appendTo(container); } };

return api;});

以及 mymodule.html模板:

<div id="mymodule"></div>

樣式表很簡單:

#mymodule { font-size:2em; color: green;}

注意,CSS並未以插件的方式被載入,相反地,它的 URL被指定給變數,並且被傳進 .render()方法進行手動載入。loadCSS()函式如下:

function loadCss(url) { $('<link>', { type: 'text/css', rel: 'stylesheet', href: url, }).appendTo('head');}

這顯然不是一個完美的解法,但撰寫直到本書時,Require.js尚未發佈標準的 css!插件,而 Curl.js倒是有一個 css!插件。另外,也可以試試 Xstyle,你能夠像處理 HTML模板那樣地使用它們。

AMD有二個嚴重的缺點。首先,它會要求你針對每個模組包含一層照本宣科的包裹函式;其次,它強迫你不是在編譯步驟中編譯你的整個應用程式,就是在客戶端非同步地

載入每一個模組—由於同時下載的限制以及網路延遲的關係,這實際上會拖慢指令稿的

載入與執行,與它所宣揚的精神相違背。

Page 16: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

模組 | 91

相較於非同步載入的方案,我比較推薦預先編譯的解法,假如你已經這樣做了,不妨也

同時使用簡化的 CommonJS語法以及 Browserify(http://browserify.org/)之類的工具。

請參考第 96頁的「使用 CommonJS、npm、Grunt與 Browserify建構客戶端程式碼」。

Node風格的模組CommonJS是為了讓 JavaScript引擎實作更具兼容性而制定的一套標準。CommonJS模組指定一組 API,模組會利用它來宣告依賴性,CommonJS模組實作負責讀取模組,並且解析它們的依賴關係。

在 Node.js之前就已經有一些想要讓 JavaScript執行在伺服端的嘗試,這些嘗試可追溯到 1990 年代晚期,當時 Netscape 與 Microsoft 皆允許在它們的伺服器環境上執行JavaScript兼容的指令稿,然而只有極少數人使用過這些功能。第一個真正受到關注的伺服端 JavaScript解決方案是 Rhino,但是它太過於緩慢且笨重,以致於很難奠基於它來建構Web層級的應用程式。

在 Node.js登場之前,已經有好幾種 JavaScript伺服端環境,而且全都是以不同的約定來處理模組載入之類的議題,CommonJS就是為了統一這些亂象而產生的。Node風格的模組基本上正是 CommonJS模組規格的實作。

CommonJS模組系統的語法確實比模組模式或 AMD簡單很多。在 CommonJS中,檔案就是模組,不需要使用包裹函式來包含作用域,因為每個檔案本身就已經被賦予它

自己的作用域。模組使用同步的 require()函式來宣告依賴關係,這表示,當被含括(required)的模組正在被解析時,程式碼執行是被阻塞的(blocked),因此,你可以放心地在 require模組之後立刻使用它。

首先,透過賦值給自由變數 exports上的鍵(key),來宣告你的模組的公用 API:

'use strict';var foo = function foo () { return true;};

exports.foo = foo;

接著,使用 require()匯入你的模組,並且將它指定給本地變數。你可以指定在已安裝 Node模組清單裡的模組名稱,也可以透過相對路徑的方式來指定一個指向模組的 路徑。

Page 17: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

92 | 第四章

例如,如果你想要使用 Flatiron HTTP模組,你可以 require()它的名稱,如下所示(摘自 Flatiron.js的說明文件(http://flatironjs.org/#routing))。

var flatiron = require('flatiron'), app = flatiron.app;

app.use(flatiron.plugins.http, { // HTTP選項});

//// app.router現在可以使用,app[HTTP-VERB]也可使用// 作為建立路由的捷徑//app.router.get('/version', function () { this.res.writeHead(200, { 'Content-Type': 'text/plain' }) this.res.end('flatiron ' + flatiron.version);});

app.start(8080);

或者,指定相對路徑:

'use strict';var mod = require('./ch04-modules.js'), result = (mod.foo() === true) ? 'Pass:' : 'Fail:';

console.log(result, '.foo() should return true.');

在此,console.log()被用來模擬單元測試框架,但就 Node而言,還有一些更好的替代方案,包括 tape(https://github.com/substack/tape)與 nodeunit(https://github.com/caolan/

nodeunit)。

npm Node 套件管理器(Node package manager,npm)是 Node 隨附的套件管理器。不同於一般人的認知,根據 npm的 FAQ,npm其實並不是一個首字母縮略字(https://www.

npmjs.org/doc/faq.html),因此,若使用大寫字母,在技術上是有問題的。npm提供一種簡單的方式,為你的應用程式安裝模組,包括所有必要的依賴模組。Node倚靠 package.

json規格來指明套件的組態。在伺服端,很常利用 npm來安裝專案所需的一切依賴套件,但現在也有人使用 npm為客戶端安裝依賴套件。

Page 18: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

模組 | 93

npm包含許多說明文件齊備的指令,但就本書的目的而言,你只需要知道你最常需要修改的那些指令,好讓你的應用程式順利跑起來:

name套件名稱。

version套件版本編號(npm模組必須使用語義化的版本管理)。

author作者資訊。

description套件的簡要描述。

keywords幫助使用者找到這個套件的搜尋關鍵字。

main主要套件檔案的路徑。

scripts開放給 npm的指令稿清單。大多數專案都應該定義一個 "test"指令稿,可透過 npm test命令來執行,你可以利用它來進行單元測試。

repository套件儲存庫的位置。

dependencies, bundledDependencies 你的套件要 require()的依賴套件。

devDependencies開發者需要的依賴套件清單,以便貢獻他們的程式碼。

engines指定要使用哪個 Node版本。

Page 19: 前言 - 碁峰資訊epaper.gotop.com.tw/PDFSample/A363.pdf · 2015-03-26 · 者(mediator)進行溝通,像是中央事件處理系統( central event-handling system) 或命令物件(command

94 | 第四章

如果你想要建構 Node應用程式,首先要做的就是建立伺服器,最簡單的做法之一就是使用 Express─一個精簡的 Node應用程式框架。開始動手之前,你應該先檢視一下最新的版本為何。當你讀到這裡時,本書所使用的版本或許已經不是最新版了。

$ npm info express

3.0.0rc5

範例 4-1說明如何將它增加到你的 package.json檔案:

範例 4-1 package.json{ "name": "simple-express-static-server", "version": "0.1.0", "author": "Sandro Padin", "description": "A very simple static file server. For development use only.", "keywords": ["http", "web server", "static server"], "main": "./server.js", "scripts": { "start": "node ./server.js" }, "repository": { "type": "git", "url": "https://github.com/spadin/simple-express-static-server.git" }, "dependencies": { "express": "3.0.x" }, "engines": { "node": ">=0.6" }}

注意,Express的版本被指定為 3.0.x,這裡的 x相當於萬用字元(wildcard),它會安裝最新的 3.0版本,而不管任何補丁編號,也就是說,「給我最新的臭蟲修正版本,但 API不要改變」。Node模組採取語義化版本管理(http://semver.org/),其版本編號格式為

Major.Minor.Patch(主要版本 .次要版本 .補丁編號)。從後面往前看,臭蟲修正會遞增補丁編號,不影響 API的改變將增加次要版本編號,而無法向後兼容的大更新會增加主要版本編號。主要版本編號為零表示這是初始開發版本,公用 API尚不穩定,而且版本字串也不會告訴你是否有向後兼容的問題。