- 1. 我是誰,以及我為什么寫這個主題?
- 2. 可以用 140 個字概述這篇文章嗎?
- 3. 究竟什么是“大型” JavaScript 應用程序?
- 4. 讓我們回顧一下當前的架構
- 5. 想得長遠一些
- 6. 頭腦風暴
- 7. 架構提議
原文: Patterns For Large-Scale JavaScript Application Architecture ?by? @Addy Osmani
今天我們要討論大型 JavaScript 應用架構中的有效模式。這篇文章基于我最近在 LondonJS 的同名演講,靈感則來自 Nicholas Zakas? 之前的成果 。
譯注: Nicholas Zakas: Scalable JavaScript Application Architecture
1. 我是誰,以及我為什么寫這個主題?
我目前是 AOL 的一名 JavaScript 和 UI 開發人員,負責為我們下一代面向客戶的應用程序計劃和編寫前端架構。 由于這些應用程序不僅復雜,而且需要可擴展和高度可重用的架構,因此我的職責之一就是確保用于實現應用程序的模式盡可能是可持續的。
我自認為是一名設計模式愛好者(雖然關于這個主題有很多專家比我更專業)。我之前基于創作共用許可證寫了 Essential JavaScript Design Patterns ?一書,現在我想寫得更詳盡一些,作為這本書的后續部分。
2. 可以用 140 個字概述這篇文章嗎?
如果你時間不夠,下面是這篇文章的摘要,只有一條 tweet 的長度:
解耦應用。架構/模塊,外觀和中介者模式。模塊生產消息,中介者發布/訂閱消息,外觀處理安全問題。
3. 究竟什么是“大型” JavaScript 應用程序?
在開始之前,讓我們嘗試弄清一點,當我們提到某個 JavaScript 應用程序是“大型”時,究竟是什么意思。這個問題對于有多年經驗的開發人員仍然是一項挑戰,而且答案也相當主觀。
我作了一個試驗,咨詢了幾位中級開發人員,讓他們試著做出非正式的定義。一個開發人員建議“JavaScript 應用程序的代碼行數超過 100,000 行”,而另一位建議“應用程序中 JavaScript 代碼超過 1MB”。雖然是勇敢的建議(如果不是故意嚇人),但是這些都不正確,因為代碼庫的大小并不總是與應用程序的復雜度相關 - 100,000 行代碼很可能是相當瑣碎的代碼。
譯注:LOC,Lines of Code,代碼行數
我自己的定義可能會也可能不會被普遍接受,但是我相信它更接近大型應用程序的真實含義。
在我看來,大型 JavaScript 應用程序是 成體系的 ,需要開發人員的 努力 維護,而最繁重的數據處理和顯示則是在 瀏覽器 中。
這個定義的最后一部分可能是最重要的。
4. 讓我們回顧一下當前的架構
如果開發一個大型 JavaScript 應用程序,記得要投入 足夠的時間 來規劃基礎架構,這是最有意義的事情。它往往比你最初想象的要更復雜。
我無法再強調基礎架構的重要性——我見過一些開發人員在遇到大型應用程序時,先退后幾步,然后說:“好吧。恩,在我最近的中型項目中已經有一套思路和模式工作的不錯。當然,它們也應該大致適用于稍大一點的項目,對吧?”。雖然在某種程度上這么說可能是正確的,但是請不要想當然 -? 大型應用程序通常需要考慮更大的問題 。我稍后要討論為什么多花點時間來為你的應用程序規劃結構在長遠來看是值得的。
大部分 JavaScript 開發人員可能在他們的當前架構中混合使用下面的概念:
- 自定義控件 custom widgets
- 模型 models
- 視圖 views
- 控制器 controllers
- 模板 templates
- 庫/工具集 libraries/toolkits
- 應用程序的核心 an application core
相關閱讀
- Rebecca Murphey - Structuring JavaScript Applications
- Peter Michaux - MVC Architecture For JavaScript Applications
- StackOverflow - A discussion on modern MVC frameworks
- Doug Neiner - Stateful Plugins and the Widget Factory
你也可能把應用程序分解為一個個的模塊,或者應用其他模式來實現。這么做不錯,但如果這就是你的應用程序的全部架構,那么你仍然可能栽到一些潛在的問題中。
1. 這種架構有多少是可立即復用的?? ?
單個模塊可以獨立存在嗎?它們是自包含的嗎?如果我現在要看看你或你的團隊所工作的大型應用程序的代碼庫,并且隨機選擇一個模塊,我可以簡單地把它放入一個新頁面,然后開始使用它嗎?你可能會質疑這么做背后的理由,但是我鼓勵你多想想未來。假使你的公司開始構建越來越多的非凡應用,而它們之間在功能上共享某些交叉點,情形將會怎么樣呢?如果有人說,“我們的用戶喜歡在我們的郵件客戶端中使用聊天模塊。讓我們把它放到協同編輯套件”,不顯著修改代碼可以做到這點嗎?
2. 在這個系統中,模塊之間的依賴有多少?? ?
它們是緊耦合的嗎?在深入挖掘這為什么會是一個問題之前,我要指出的是,一個系統中的模塊絕對無依賴并不總是可行的。在某個粒度級別,你有充分的理由讓模塊從其他模塊擴展基本功能,但問題在于具有不同功能的模塊組之間的關聯度。而在你的應用程序中,所有這些不同的模塊組在正常運行時,不依賴太多其他模塊的存在和加載,應該是有可能的。
3. 如果應用程序的特定部分崩潰了,應用程序仍然可以運行嗎?? ?
如果你正在構建一個類似 GMail 的應用程序,并且你的 webmail 模塊崩潰了,此時不應該阻塞 UI 的其余部分,或者阻止用戶使用頁面上的其他部分,例如聊天模塊。同時,按照之前說的,模塊最好可以在當前應用程序的架構之外獨立存在。在我的演講中,我提到了基于用戶意圖的動態依賴(或模塊)加載,用戶意圖以相關的事件來表達。例如,在 GMail 中,聊天模塊默認是收起的,在頁面初始化時不需要核心模塊代碼已經加載完畢。如果用戶表示出使用聊天特性的意圖,只需要動態加載即可。理想情況下,你期望不受應用程序其余部分的負面影響是可能的。
4. 可以輕松地測試各個模塊嗎?? ?
當在一個有著顯著規模的系統上工作,而且這個系統有數以百萬計的潛在用戶使用或誤用系統的不同部分時,模塊必然會被多個經過充分測試的應用程序所復用。既需要對模塊在(負責初始化它的)架構內部的情況進行測試,也需要對模塊在架構之外的情況進行測試。在我看來,當模塊應用在另一個系統時,測試為模塊不會崩潰提供了最大限度的保證。
5. 想得長遠一些
當為你的大型項目設計架構時,最重要的是超前思考。不僅僅是從現在開始的一個月或一年,比這要久的多。會改變什么嗎?猜測你的應用程序會如何成長當然是不可能的,但是肯定有空間來考慮什么是可能的。在這節內容中,至少會思考應用程序的某個特定方面。
開發人員經常把 DOM 操作代碼與應用程序的其他部分耦合地相當緊密——甚至在他們已經把核心業務分離為模塊時。想想為什么這么做不是一個好主意,如果我們正在做長期規劃的話。
我的觀眾之一認為原因是,現在定義的這種僵硬架構在未來可能不再合適。這種觀點千真萬確,而且還有另一層擔憂,就是如果現在不考慮進來的話,將來花費的成本甚至可能會更多。
你可能在未來因為性能、安全或設計的原因,決定把正在使用的 Dojo、jQuery、Zepto 或 YUI 切換為某個完全不同的東西。如果庫與你的應用程序緊密耦合的話,這種決定就會演變為一個問題,因為互換庫并不容易,而且切換的成本高昂。
如果你是一個 Dojo 開發人員(例如我演講會上的一些觀眾),目前你可能沒有值得切換的、更好的庫,但是誰敢說在 2-3 年內不會出現更好的、你想要切換的庫?
在較小的代碼庫中,這是一個比較瑣碎(容易)的決定,但是對于大型應用程序,擁有一個靈活到可以不關心模塊所用庫的架構,從財政和節省時間的角度來看,都可以帶來很大的好處。
總之,如果你現在回顧你的架構,能夠做出無需重寫整個應用程序就可以切換庫的決定嗎?如果不能,請繼續讀下去,因為我覺得今天介紹的架構可能正是你所感興趣的。
到目前為止,對于我所關注的問題,一些有影響力的 JavaScript 開發人員已經有所涉獵。我想要分享他們的三個關鍵觀點,引文如下所示:
“構建大型應用程序的秘訣是永不構建大型應用程序。把你的應用程序分解為小塊。然后把這些可測試、粒度合適的小塊組裝到你的大型應用程序中” -? Justin Meyer,JavaScriptMVC 的作者
“關鍵是從一開始就承認你不知道該如何成長。當你接受了你是一無所知的之后,你會開始保守地設計系統。你確定可能會改變的關鍵領域,當你花一點時間在這上面的話,要做到這點往往很容易。舉個例子,你應該想到與應用程序中其他系統進行通信的部分將可能會改變,因此你需要把它抽象出來。” -? Nicholas Zakas,《High-performance JavaScript websites》的作者
最后但并非最不重要的:
“彼此緊密綁定的組件,較少復用的組件,以及因為會影響到其他組件而變得更難改變的組件” -? Rebecca Murphey, 《jQuery Fundamentals》的
這些原則是構建架構的關鍵,能夠經得起時間的考驗,應該始終牢記。
6. 頭腦風暴
思考一下我們要達到什么目的。
我們希望有一個松耦合的架構,功能可以分解為 獨立的模塊 ,最好模塊間沒有依賴。當有趣的事情發生時,模塊 通知 應用程序的其他部分,一個 中間層 解釋并響應這些消息。
例如,如果我們有一個負責在線面包店的 JavaScript 應用程序,從一個模塊發來的“有趣”消息可能是“準備配送第 42 批次的面包卷”。
我們使用一個不同的分層來解釋模塊的消息,以便于:a) 模塊不直接訪問核心,b) 模塊不需要直接調用其他模塊或或與之交互。這有助于防止應用程序因為特定模塊的錯誤而崩潰,并且提供了一種方式來重啟崩潰的模塊。
另一個令人關注的問題是安全性。然而真實情況是,我們中的大多數人不會考慮應用程序內部的安全性。我們告訴自己,因為是我們構建了應用程序,有足夠的聰明來弄清楚哪些應該是公開或試下訪問。
然而,如果你有辦法判斷系統中一個模塊允許做什么,就不會有幫助了嗎?例如,如果我已經在系統中限制了權限,不允許一個公開的聊天部件與權限管理模塊,或者與一個用于數據庫寫權限模塊交互,就可以防范有人利用聊天部件的已知漏洞來發起 XSS 攻擊。模型不應該有能力訪問所有的事務。目前的大多數架構可能可以做到這一點,但是真的需要這么做嗎?
用一個中間層來處理權限問題,來決定哪些模塊可以訪問框架的哪部分,可以天然的增強安全性。這意味一個模塊唯一能做的就是我們允許它做的。
7. 架構提議
我們所尋求的架構解決方案是三個著名設計模式的組合體: 模塊化 , 外觀模式 和 中介者模式 。
在傳統的模式中,模塊彼此之間直接進行通信,而在解耦架構中,模塊只發布感興趣的事件(在理想情況下,不需要知道系統中的其他模塊)。中介者模式將訂閱從模塊來的消息,并在收到通知時給與適當的響應。中介者模式將用于模塊鑒權。
我將在后面闡述這些模式的更多細節:
-
設計模式
-
模塊化理論
- 摘要
- 模塊模式
- 對象字面量
- CommonJS 模塊
- 外觀模式
- 中介者模式
-
模塊化理論
-
在架構中應用
- 外觀 - 抽象的核心
- 中介者 - 應用程序的核心
- 集成
7.1 模塊化理論
你可能在現有架構中已經使用了一些模塊。但如果沒有的話,本節將簡要介紹關于模塊的一些引文。
在任何健壯的應用程序的架構中,模塊是一個 完整 部件,并且在可互換的較大系統中,模塊通常是單一用途的。
按照實現模塊的方式,你可以定義模塊的依賴,并瞬間自動把其他部分加載進來。相較于無奈地跟蹤它們的各種依賴關系,然后手動加載模塊或插入 script 標簽,這種方式被認為更具有擴展性。
任何成體系的應用程序都應該基于模塊化組件構建。回到 GMail,你可以把模塊理解為可以獨立存在的功能單元,就像聊天模塊。然而這取決于功能單元的復雜度,它很可能還依賴于更精細的子模塊。例如,有一個子模塊負責簡單地處理表情符號,而該系統的聊天部件和郵件部件則共享使用這些表情符號。
在正討論的架構中,模塊對系統其他部分的情況所知甚少。而且,我通過一個外觀把職責代理到一個中介者上。
這個刻意設計的,因為如果一個模塊只負責通知系統所感興趣的事情發生了,而不用擔心其他模塊是否正在運行,那么系統就能夠支持添加、移除或更換模塊,而系統中的其他模塊不會因為緊密耦合而崩潰。
這種思路行得通的關鍵是松耦合。松耦合通過在必要時移除代碼依賴關系,簡化了模塊的維護。在我們的例子中,模塊不應該依賴于其他模塊才能正常運行。當松耦合被有效地貫徹時,看看系統某個部分的變化是如何影響其他部分的。
在 JavaScript 中,有幾種可選的模塊化實現方式,包括廣為人知的模塊模式和對象字面量。有經驗的開發人員應該已經熟知這些知識,如果是這樣的話,請跳到介紹 CommonJS 模塊的部分。
模塊模式
模塊模式是一種流行的設計模式,通過使用閉包來封裝“隱私”、狀態和結構。它可以包裹公開和私有的方法和變量,避免它們污染全局作用域,以及避免與其他開發人員的接口沖突。這種模式只會返回公開的 API,此外的一切則是封閉和私有的。
模塊模式提供了一種清爽的解決方案,屏蔽了承擔繁重任務的邏輯,只向應用程序的其他部分暴露希望它們使用的接口。這種模式與立即調用的函數表達式(IIFE)非常相似,只不過前者返回的是一個對象,而后者返回的是一個函數。。
應該指出的是,在 JavaScript 中并不存在真正意義上的“隱私”,因為它不像一些傳統語言一樣具有訪問修飾符。從技術的角度,變量不能被聲明為公開或私有,所以我們用函數作用域來模擬這個概念。在模塊模式中,仰賴于閉包機制,聲明的變量或方法只在模塊自身內部有效。而返回的對象中的變量或方法對所有人都是可用的。
你可以在下面看到一個購物車示例,其中使用了模塊模式。該模塊自身被包含在一個稱為
basketModule
?的全局對象中,完全自給自足。模塊中的數組?
basket
?是私有的,應用程序的其他部分無法直接讀取它。它只存在于這個模塊的閉包中,因此,只有可以訪問它所屬作用域的方法(即?
addItem()
、
getItem()
?等),才可以訪問它。
var basketModule = ( function () { var basket = []; // private return { // exposed to public addItem: function (values) { basket.push(values); }, getItemCount: function () { return basket.length; }, getTotal: function (){ var q = this .getItemCount(),p=0 ; while (q-- ){ p += basket[q].price; } return p; } } }());
在模塊內部,你會發現它返回了一個對象。這種做法使得返回值被自動賦值給 basketModule,因此你像下面這樣與它交互:
// basketModule is an object with properties which can also be methods basketModule.addItem({item:'bread',price:0.5 }); basketModule.addItem({item: 'butter',price:0.3 }); console.log(basketModule.getItemCount()); console.log(basketModule.getTotal()); // however, the following will not work: console.log(basketModule.basket); // (undefined as not inside the returned object) console.log(basket); // (only exists within the scope of the closure)
上面的方法被有效的限制在命名空間 basketModule 中。
從歷史的角度看,模塊模式最初是由一些人發現的,包括 Richard Cornford(2013年)。后來被 Douglas Crockford 在他的演講中推廣,并被 Eric Miraglia 在 YUI 的博客中再次介紹。
在具體的工具庫或框架中,模塊模式是什么樣的情況呢?
Dojo
Dojo 嘗試通過?
dojo.declare
?來實現模塊模式,提供與“class”類似的功能。例如,如果我們想把?
basket
?聲明為命名空間?
store
?下的一個模塊,可以做如下實現:
// traditional way var store = window.store || {}; store.basket = store.basket || {}; // using dojo.setObject dojo.setObject("store.basket.object", ( function () { var basket = []; function privateMethod() { console.log(basket); } return { publicMethod: function (){ privateMethod(); } }; }()));
如果?
dojo.declare
?與?
dojo.provide
?和 mixins 結合使用,可以變得非常強大。
YUI
下面的例子基于 Eric Miraglia 實現的原始 YUI 模塊模式,雖然有些厚重,但尚能自圓其說。
YAHOO.store.basket = function () { // "private" variables: var myPrivateVar = "I can be accessed only within YAHOO.store.basket ." ; // "private" method: var myPrivateMethod = function () { YAHOO.log( "I can be accessed only from within YAHOO.store.basket" ); } return { myPublicProperty: "I'm a public property." , myPublicMethod: function () { YAHOO.log( "I'm a public method." ); // Within basket, I can access "private" vars and methods: YAHOO.log(myPrivateVar); YAHOO.log(myPrivateMethod()); // The native scope of myPublicMethod is store so we can // access public members using "this": YAHOO.log( this .myPublicProperty); } };
}();
jQuery
把 jQuery 代碼(不局限于插件)封裝為模塊模式有很多種方式。Ben Cherry 曾經建議過一種實現:用一個函數把模塊定義包裹起來,模塊定義則含有一些共性事件。
在下面的示例中,定義了一個函數?
library
,該函數用于聲明一個新庫,當新庫(即模塊)被創建時,會并自動把函數?
init
?綁定到?
document.ready
。
function library(module) { $( function () { if (module.init) { module.init(); } }); return module; } var myLibrary = library( function () { return { init: function () { /* implementation */ } }; }());
相關閱讀
- Ben Cherry - The Module Pattern In-Depth
- John Hann - The Future Is Modules, Not Frameworks
- Nathan Smith - A Module pattern aliased window and document gist
- David Litmark - An Introduction To The Revealing Module Pattern
對象字面量? ?
在對象字面量中,一個對象被描述為一組用逗號分隔的名稱/值對,并用大括號(
{}
)包裹起來。對象中的名稱可以是字符串或唯一標識,后跟一個冒號。不應該在對象中最后一對名稱/值的后面使用逗號,因為這可能導致錯誤。
對象字面量不需要使用操作符?
new
?來實例化,但是不應該使用在語句的起始處,因為?
{
?可能會被解釋為代碼塊的開始。你可以在下面看到一個使用對象字面量來定義模塊的示例。新成員可能被通過賦值添加到對象上,就像下面的?
myModule.property = 'someValue';
。
雖然模塊模式適用于很多場景,但如果你發現并不需要特定的私有屬性或方法,那么對象字面量無疑是更合適的替代品。
var myModule = { myProperty : 'someValue' , // object literals can contain properties and methods. // here, another object is defined for configuration // purposes: myConfig:{ useCaching: true , language: 'en' }, // a very basic method myMethod: function (){ console.log( 'I can haz functionality?' ); }, // output a value based on current configuration myMethod2: function (){ console.log( 'Caching is:' + ( this .myConfig.useCaching)?'enabled':'disabled' ); }, // override the current configuration myMethod3: function (newConfig){ if ( typeof newConfig == 'object' ){ this .myConfig = newConfig; console.log( this .myConfig.language); } } }; myModule.myMethod(); // I can haz functionality myModule.myMethod2(); // outputs enabled myModule.myMethod3({language:'fr',useCaching: false }); // fr
相關閱讀
- Rebecca Murphey - Using Objects To Organize Your Code
- Stoyan Stefanov - 3 Ways To Define A JavaScript Class
- Ben Alman - Clarifications On Object Literals (There's no such thing as a JSON Object)
- John Resig - Simple JavaScript Inheritance
7.2 CommonJS 模塊? ?
在過去一兩年中,你可能已經聽說過? CommonJS ?- 一個致力于設計、原型化和標準化 JavaScript API 的志愿者工作組。迄今為止,他們已經批準了針對模塊和包的標準。CommonJS AMD 建議規范一個簡單的 API 來聲明模塊,并且可以在瀏覽器中通過同步和異步 script 標簽來加載聲明的模塊。他們的模塊模式相對比較清爽,并且我認為它是 ES Harmony(JavaScript 語言的下一個版本)所建議的模塊系統的可靠基石。
從結構的角度來看,一個 CommonJS 模塊是一段可重用的 JavaScript,它輸出特定的對象以供任何依賴它的代碼使用。這種模塊格式正在變得相當普及,成為事實上的 JS 標準模塊格式。有許多關于實施 CommonJS 模塊的偉大教程,但是從高層次角度看的話,它們基本上包含兩個主要部分:一個?
exports
?對象包含了希望對其他模塊可用的模塊,一個?
require
?函數用來讓模塊導入其他模塊的輸出。
/* Example of achieving compatibility with AMD and standard CommonJS by putting boilerplate around the standard CommonJS module format: */
( function (define){ define( function (require,exports){ // module contents var dep1 = require("dep1" ); exports.someExportedFunction = function (){...}; // ... }); })( typeof define=="function"?define: function (factory){factory(require,exports)});
有許多偉大的 JavaScript 庫可以按照 CommonJS 模塊規范來處理模塊加載,但我個人偏好于 RequireJS。完整的 RequireJS 教程超出了本文的范疇,不過我推薦讀一讀 James Burke 的博文?
ScriptJunkie
。我知道有些人也喜歡?
Yabble
。
從使用的角度看,RequireJS 提供了一些包裝方法,來簡化靜態模塊的創建過程和異步加載。它可以很容易的加載模塊以及模塊的依賴,然后在模塊就緒時執行模塊的內容。
有些開發人員聲稱 CommonJS 模塊不太適用在瀏覽器中。原因是 CommonJS 模塊無法通過 script 標簽加載,除非有服務端協助。我們假設有一個把圖片編碼為 ASCII 的庫,它暴露出一個
encodeToASCII
?函數。它的模塊類似于:
var encodeToASCII = require("encoder" ).encodeToASCII; exports.encodeSomeSource = function (){ // process then call encodeToASCII }
在這類情況下,script 標簽將無法正常工作,因為作用域不匹配,這就意味著方法?
encodeToASCII
?將被綁定到
window
?對象、
require
?未定義,并且需要為每個模塊單獨創建 exports。但是,客戶端庫在服務端的協助下,或者庫通過 XHR 請求加載腳本并使用了?
eval()
,都可以很容易地處理這種情況,
使用 RequireJS,該模塊的早期版本可以重寫為下面這樣:
define( function (require, exports, module) { var encodeToASCII = require("encoder" ).encodeToASCII; exports.encodeSomeSource = function (){ // process then call encodeToASCII } });
對于不只依賴于靜態 JavaScript 的項目來說,CommonJS 模塊是很好的選擇,不過一定要花一些時間來閱讀相關的內容。我僅僅涉及到了冰山一角,如果你想進一步閱讀的話,CommonJS 的 wikie 和 Sitepen 有著大量資源。
相關閱讀
- The CommonJS Module Specifications
- Alex Young - Demystifying CommonJS Modules
- Notes on CommonJS modules with RequireJS
7.3 外觀模式? ?
接下來,我們要看看外觀模式,這個設計模式在今天定義的架構中扮演著關鍵角色。
當構造一個外觀時,通常是創建一個掩蓋了不同現實的外在表現。外觀模式為更大的代碼塊提供了一個方便的 高層接口 ,通過隱藏其真正復雜的底層。把它看成是提交給其他開發人員的簡化版 API。
外觀是 結構模式 的一種,經常可以在 JavaScript 庫和框架中看到它,它的內部實現雖然可以提供各種行為的方法,但是只有一個“外觀”或這些方法的有限抽象被提交給客戶使用。
這樣一來,我們是與外觀交互,而不是與幕后的子系統交互。
外觀之所以好用的原因在于,它能夠隱藏各個模塊中功能的具體實現細節。模塊實現的改變甚至可以在客戶不知情的情況下進行。
通過維護一個統一的外觀(簡化后的 API),對模塊是否使用 dojo、jQuery、YUI、zepto 或者別的東西的擔心就顯得不太重要。只要交互層不改變,就保留了在將來切換庫(例如用 jQuery 替換 Dojo)的能力,而不會影響系統的其他部分。
下面是一個非常簡單的外觀行為示例。正如你可以看到的,我們的模塊包含了一些定位為私有的方法。然后用外觀提供的更簡單的 API 來訪問這些方法。
var module = ( function () { var _private = { i: 5 , get : function () { console.log( 'current value:' + this .i); }, set : function ( val ) { this .i = val; }, run : function () { console.log( 'running' ); }, jump: function (){ console.log( 'jumping' ); } }; return { facade : function ( args ) { _private.set(args.val); _private.get(); if ( args.run ) { _private.run(); } } } }());
module.facade({run: true, val:10}); //outputs current value: 10, running
在把外觀應用到我們的架構中之前,關于外觀就介紹這么多。接下來,我們將深入激動人心的中介者模式。外觀模式和中介者模式之間的核心區別在于,外觀模式(一種結構模式)只公開已有的功能,而中介者模式(一種行為模式)可以添加功能。
相關閱讀
7.4 中介者模式? ?
介紹中介者模式的最佳方式是用一個簡單的比喻——想象一下機場交通管制。塔臺處理哪些飛機可以起飛或降落,因為所有的通信都由飛機和控制塔完成,而不是由飛機之間。集中控制是這個系統成功的關鍵,而這就是一個中介者。
當模塊之間的通信有可能是復雜的,請使用中介者,但是這一點 不易鑒定 。如果有這樣一個系統,代碼中的模塊之間有大多的關系,那么就該有一個中央控制點了,這就是這個模式的用武之地。
一個中介者 封裝 了不同模塊之間的 交互 行為,就像現實世界中的中間人。該模式阻止了對象彼此之間直接引用,從而促進了松耦合——這有助于我們解決系統中模塊互相依賴的問題。
它還必須提供什么其他的優勢呢?恩,中介者允許每個模塊的行為可以獨立變化,所以它非常靈活。如果你曾經在你的系統使用過觀察者(發布/訂閱)模式來實現模塊之間的事件廣播系統,你將會發現中介者相對而言比較容易理解。
讓我們以高層次的視角來看看模塊是如何與中介者交互的:
模塊是發布者,中介者則既是發布者又是訂閱者。模塊 1 廣播一個事件了通知中介者有事要做。中介者捕獲這個消息,繼而啟動需要完成這項任務的模塊 2,模塊 2 執行模塊 1 要求的任務,并向中介者廣播一個完成事件。與此同時,模塊 3 也會被中介者啟動,記錄從中介者傳來的任何通知。
任何模塊沒有機會與其他模塊 直接通信 ,請注意是如何做到這點的。如果調用鏈中的模塊 3 失敗或停止運行,中介者可以假裝“暫停”其他模塊的任務,停止模塊 3 并重啟它,然后繼續工作,這對系統而言幾乎沒有影響。這種程度的解耦是中介者模塊提供的主要優勢之一。
回復一下,中介者的優勢如下:
它通過引入一個中間人作為中央控制點來解耦模塊。它允許模塊廣播或監聽消息,而不必關注系統的其他的部分。消息可以同時被任意數量的模塊所處理。
顯然,向松耦合的系統添加或移除功能變得更容易。
但它的缺點是:
通過在模塊之間增加中介者,模塊必須總是間接地通信。這可能會導致輕微的性能下降——因為松耦合的性質所然,而且很難預期一個關注廣播的系統會如何響應。緊耦合令人各種頭疼,而中介者正是一條解決之道。
示例: 這是中介者模式在? @rpflorence ?早先工作基礎上的一種可能實現。
var mediator = ( function (){ var subscribe = function (channel, fn){ if (!mediator.channels[channel]) mediator.channels[channel] = []; mediator.channels[channel].push({ context: this , callback: fn }); return this ; }, publish = function (channel){ if (!mediator.channels[channel]) return false ; var args = Array.prototype.slice.call(arguments, 1 ); for ( var i = 0, l = mediator.channels[channel].length; i < l; i++ ) { var subscription = mediator.channels[channel][i]; subscription.callback.apply(subscription.context, args); } return this ; }; return { channels: {}, publish: publish, subscribe: subscribe, installTo: function (obj){ obj.subscribe = subscribe; obj.publish = publish; } }; }());
示例: 這是前面實現的兩個使用示例。發布/訂閱被有效的管理起來。
//Pub/sub on a centralized mediator
mediator.name = "tim" ; mediator.subscribe( 'nameChange', function (arg){ console.log( this .name); this .name = arg; console.log( this .name); }); mediator.publish( 'nameChange', 'david'); // tim, david // Pub/sub via third party mediator var obj = { name: 'sam' }; mediator.installTo(obj); obj.subscribe( 'nameChange', function (arg){ console.log( this .name); this .name = arg; console.log( this .name); }); obj.publish( 'nameChange', 'john'); // sam, john
相關閱讀
- Stoyan Stefanov - Page 168, JavaScript Patterns
- HB Stone - JavaScript Design Patterns: Mediator
- Vince Huston - The Mediator Pattern (not specific to JavaScript, but a concise)
7.5 應用外觀:核心的抽象? ?
架構建議:
一個外觀作為應用程序核心的 抽象 ,位于中介者和模塊之間——理想情況下,它應該是系統中唯一可以感知其他模式的模塊。
這個抽象的職責包括了為這些模塊提供 統一的接口 ,以及確保在任何時候都是可用的。這一點非常類似于杰出架構中 沙箱控制器 的角色,它由 Nicholas Zakas 首次提出。
組件將通過外觀與中介者通信,所以外觀必須是可靠的。應該澄清的是,當我說“通信”時實際上是指與外觀進行通信,外觀是中介者的抽象,將監聽模塊的廣播,再把廣播回傳給中介者。
除了為模塊提供接口,中介者還扮演者安保的角色,確定一個模塊可以訪問應用程序的哪些部分。組件只能訪問它們自己的方法,對于它沒有權限的任何東西,則不能與之行交互。假設一個模塊可以廣播
dataValidationCompletedWriteToDB
。此時,安全檢查的概念是指確保有權限的模塊才能請求數據寫操作。我們最好避免讓模塊意外地試圖做一些它們本不該做的事情。
總之,中介者是發布/訂閱的管理者,不過,只有通過外觀權限檢查的感興趣事件才會被傳給中介者。
7.6 應用中介者:應用程序的核心? ?
中介者扮演的角色是應用程序的核心。我們已經簡要介紹了一些它的職責,不過還是要澄清下它的所有職責。
核心的主要任務是管理模塊的 生命周期 。當核心偵測到一個 感興趣的事件 時,它需要決定應用程序該如何響應——這實際上意味著決定是否需要 啟動 或 停止 一個或一組模塊。
理想情況下,一旦某個模塊被啟動,它應該 自動 執行。模塊是否在 DOM 就緒時運行,以及運行條件是否全部滿足,決定這些并不是核心的任務,而是由架構中的模塊指定決定。
你可能想知道一個模塊在什么情況下可能需要“停止”——如果應用程序偵測到某個特定模塊出現故障或正處于嚴重的錯誤中,可以決定讓這個模塊中的方法停止繼續執行,并且可能會重新啟動它。這么做的目的是幫助降低對用戶體驗的破壞。
此外,核心應該可以 添加或移除 模塊而不破壞任何東西。一個典型的應用場景是,功能在頁面初始化時尚不可用,而是基于用戶的意圖動態加載,例如,回到 GMail 的例子,Google 可以讓聊天部件默認收起,只有在用戶表現出使用它的興趣時才會動態加載。從性能優化的角度看,這么做是有意義的。
錯誤管理應該由應用程序的核心處理。模塊除了廣播感興趣的事件外,也會廣播發生的任何錯誤,然后核心可以做出相應的反饋(例如停止模塊、重啟模塊等)。提供足夠的上下文,以便用更新或更好的方式來處理或者向終端用戶顯示錯誤,而不必手動改變每個模塊,是解耦架構中重要的一環。通過中介者使用發布/訂閱機制,可以做到這一點。
7.7 整合? ?
模塊 ?為應用程序提供特定的功能。每當發生了感興趣的事情,模塊發布消息通知應用程序——這是它們的主要關注點。正如我在 FAQ(常見問題)中介紹的,模塊可以依賴 DOM 工具方法,但是理想情況下不應該依賴系統的任何其他模塊。它們不應該關注:
- 什么對象或模塊將訂閱它們發布的消息
- 這些對象在哪里(是否在客戶端或服務端)
- 有多少對象訂閱了消息
外觀 ?抽象核心,用于避免模塊直接接觸核心。它訂閱(從模塊來的)感興趣的事情,并且說“干得好!發生了什么事?把詳細資料給我!”。它還負責檢查模塊的安全性,以確保發布消息的模塊具備必要的權限來傳遞可接受的事件。
中介者(應用程序的核心) ?扮演“發布/訂閱”管理者的角色。負責管理模塊,在需要時啟動或停止模塊。特別適用于動態依賴加載,并確保失敗的模塊可以在需要時集中重啟。
如此架構的結果是模塊(大多數情況下)在理論上不再依賴于其他模塊。它們可以很容易地獨立測試和維護,正因為這種程度的解耦,可以把模塊放入一個新頁面中供其他系統使用,而不需要做太多額外的工作。模塊可以被動態地添加或移除,而不會導致應用程序崩潰。
7.8 超越發布/訂閱:自動注冊事件? ?
正如 Michael Mahemoff 在前面提到的,當考慮大型 JavaScript 時,適當利用這門語言的動態特性是有益的。關于詳細內容請閱讀 Michael 的? G+ ?頁面,我特別關注其中一個概念——自動注冊事件(AER Automatic Event Registration)。
AER 通過引入基于命名約定的自動連接模式,解決了訂閱者到發布者的連接問題。例如,如果某個模塊發布一個稱為?
messageUpdate
?的事件,所有相關的?
messageUpdate
?方法將被自動調用。
譯注:有點類似于 jQuery 事件系統的手動觸發方法 .trigger(),即可以觸發通過 jQuery 事件方法(.on())綁定的事件,也可以觸發行內事件(elem.click())。
這種模式的結構涉及到:注冊所有可能訂閱事件的模塊,注冊所有可能被訂閱的事件,最后為組件庫中的每個訂閱者注冊方法。對于這篇文章所討論的架構來說,這是一個非常有趣的方法,但也確實帶來一些有趣的挑戰。
例如,當動態地執行時,對象可以被要求在創建時注冊自身。請閱讀 Michael 關于 AER 的 文章 ,他更深入地討論了如何處理這類問題。
7.9 常問問題? ?
問:是否有可能避免必須實現一個沙箱或外觀?? ?
答:雖然前面介紹的架構使用了一個外觀來來實現安全功能,但是如果不用外觀,而是用一個中介者和發布/訂閱機制來通信系統中感情興趣的事件是也完全可行的。這個輕量級版本(后者)可以提供類似程度的解耦,但如果選擇這么做,模塊就可以隨意地直接接觸應用程序的核心(中介者)。
問:你提到了模塊沒有任何依賴。是否包括對第三方庫的依賴(例如 jQuery)?? ?
答:我特別指對其他模塊的依賴。一些開發人員為架構做出的選擇實際上等同于 DOM 庫的的公用抽象——例如,可以一個構建 DOM 公用類,使用 jQuery 來查詢選擇起表達式并返回查找到的 DOM(或者 Dojo,如果將來切換了的話)。通過這種方式,盡管模塊依然會查詢 DOM,但不會以硬編碼的方式直接使用任何特定的庫或工具。有相當多的方式可以實現這一點,但要選擇的話,它們的共同點是核心模塊(理想情況下)不應該依賴其他模塊。
在這種情況下,你會發現,有時只需要一點額外的工作量,就可以讓一個項目的完整模塊運行在另一個項目中。我應該說清楚的是,我完全同意對模塊進行擴展或者只使用模塊的部分功能,而且有時可能是更明智的選擇,但是記住,在某些情況下,想要把這樣的模塊應用到其他項目會增加工作量。
問:我想開始使用這種架構。是否有可供參考的樣板代碼?? ?
答:如果時間允許的話,我打算為這篇文章發布一個樣板包,但目前你最好的選擇是 Andrew Burgees 的超值教程? Writing Modular JavaScript (在推薦之前需要完全披露的是,這僅僅是一個推薦鏈接,收到的任何反饋都將有助于完善內容)。Andrew 的樣板包包含一張屏幕截屏以及代碼,覆蓋了這篇文章的的大部分主要觀點,但選擇把外觀稱作“沙箱”,就像 Zakas。還有一些討論是關于如何理想地在這樣一個架構中實現 DOM 抽象庫———類似于我對第二個問題的回答,Andrew 在實現選擇器表達式查詢時采用了一些有趣的模式,使得在大多數情況下,用短短幾行代碼就可以做到切換庫。我并不是說它是正確的或最好的實現方式,但是它是一種可能,而且我個人也在使用它。
問:如果模塊需要直接與核心通信,這么做可能嗎?? ?
答:正如 Zakas 之前暗示的,為什么模塊不應該訪問核心在技術上沒有理由,但這是最佳實現,比其他任何事情都重要。如果你想嚴格地堅持這種架構,你需要遵循定義的這些規則,或者選擇一個更松散的結構,就像第一個問題的答案。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
