11月中旬在倫敦舉行的jQuery Summit頂級大會上有個session講的是大型JavaScript應用程序架構,看完PPT之後以爲甚是不錯,因而整理一下發給你們共勉。javascript
PDF版的PPT下載地址:http://www.slideshare.net/jibyjohnc/jqquerysummit-largescale-javascript-application-architecturehtml
注:在整理的過程當中,發現做者有些思想是返來複去地說,因此刪減了一部分,若是你的英文良好,請直接閱讀英文的PPT。java
如下是本文的主要章節:web
1. 什麼叫「JavaScript大型程序」?數據庫
2. 顧當前的程序架構設計模式
3. 長遠考慮數組
4. 頭腦風暴瀏覽器
5. 建議的架構安全
5.1 設計模式性能優化
5.1.1 模塊論
5.1.1.1 綜述
5.1.1.2 Module模式
5.1.1.3 對象自面量
5.1.1.4 CommonJS模塊
5.1.2 Facade模式
5.1.3 Mediator模式
5.2 應用到你的架構
5.2.1 Facade - 核心抽象
5.2.2 Mediator - 程序核心
5.2.3 緊密聯合運做起來
6. 發佈Pub/訂閱Sub的延伸:自動註冊事件
7. Q & A
8. 致謝
在咱們開始以前,咱們來定義一下什麼叫大型JavaScript站點,不少有經驗的JS開發高手也都被challenge住了,有人說超過10萬行JavaScript代碼纔算大型,也有人說JavaScript代碼要超過1MB大小纔算,其實2者都不能算對,由於不能安裝代碼量的多少來衡量,不少瑣碎的JS代碼很容易超過10萬行的。
我對「大」的定義以下,雖然可能不太對,可是應該是比較接近了:
我我的認爲,大型JavaScript程序應該是很是重要而且融入了不少卓越開發人員努力,對重量級數據進行處理而且展現給瀏覽器的程序。
我不能強調說這個問題有多重要,不少有經驗的開發人員常常說:「現有的創意和設計模式在我上一個中型項目上運行得很是好,因此在稍微大型點的程序裏再次使用,應該沒問題,對吧?」,在必定程序上是沒錯的,但別忘記了,既然是大型程序,一般就應該有大的Concerns須要分解關注,我簡短解釋一下要花時間來review當前運行了好久的程序架構。大多數狀況下,當前的JavaScript程序架構應該是以下這個樣子的(注意,是JS架構,不是你們常說的ASP.NET MVC):
custom widgets
models
views
controllers
templates
libraries/toolkits
an application core.
你可能還會將程序單獨封裝成多個modules,或者使用其餘的設計模式,這很好,可是若是這些結構徹底表明你的架構的話,就可能會有一些潛在的問題,咱們來看看幾個重要的點:
1.你架構裏的東西,有多少能夠當即拿出來重用?
有沒有一些單獨的module不依賴別的代碼?是自包含麼?若是我到大家正在使用的代碼庫上去隨即挑選一些模塊module代碼,而後放在一個新頁面,是否能當即就能使用?你可能會說原理通就能夠了,我建議你長久打算一下,若是你的公司以前開發不少重要的程序,忽然有一天有人說,這個項目裏的聊天模塊不錯,咱們拿出來放在另一個項目裏吧,你能直接拿過來不修改代碼就能使用麼?
2.系統裏有多少模塊module須要依賴其餘模塊?
系統的各個模塊是否是都很緊耦合?在我將這個問題做爲concern以前,我先解釋一下,不是說全部的模塊都絕對不能有任何依賴,好比一個細粒度的功能多是從base功能擴展來的,個人問題和這種狀況不同,我說的是不一樣功能模塊以前的依賴,理論上,全部的不一樣功能模塊都不該該有太多的依賴。
3.若是你程序的某一部分出錯了,其餘部分是否可以依然工做?
若是你構建一個和Gmail差很少的程序,你能夠發現Gmail裏不少模塊都是動態加載的,好比聊天chat模塊,在初始化頁面的時候是不加載的,並且就算加載之後出錯了,頁面的其餘部分也能正常使用。
4.你的各個模塊Module能很簡單的進行測試麼?
你的每個模塊都有可能用在數百萬用戶的大型站點上,甚至多個站點都使用它,因此你的模塊須要能經得住測試,也就是說,無論是在架構內部仍是架構外部,都應該能很簡單的去測試,包括大部分的斷言在不一樣的環境下都可以經過。
架構大型程序的時候,最重要的是要有前瞻性,不能只考慮一個月或者一年之後的狀況,要考慮更長久的狀況下,有什麼改變的可能性?開發人員常常將DOM操做的代碼和程序綁定得太緊,儘管有時候已經封裝單獨的邏輯到不一樣的模塊裏了,想一想一下,長久之後,爲何不是很好。
個人一個同事曾經說過,一個精確的架構可能不適合將來的情景,有時候是正確的,可是當你須要該作的話,你所付出的money那但是至關地多哦。好比,你可能由於某些性能,安全,設計的緣由須要在Dojo, jQuery, Zepto, YUI之間須要選擇替換,這時候就有問題了,大部分模塊都有依賴,須要錢呀,須要時間啊,須要人呀,對不?
對於一些小型站點沒事,可是大型站點確實須要提供一個更加靈活的機制,而不去擔憂各個模塊之間的各類問題,這既然節約錢,又能節約時間。
總結一下,如今你能肯定你能不重寫整個程序就能替換一些類庫麼?若是不能,那估計咱們下面要講的內容,就比較適合你了。
不少有經驗的JavaScript開發者給出了一些關鍵的notes:
JavaScriptMVC的做者Justin Meyer說:
構建大型程序最大的祕密就是歷來不構建大型程序,而是將程序分解成各個小的模塊去作,讓每一個小模塊均可測試,可size化,而後集成到程序裏。
High-performance JavaScript websites做者Nicholas,Zakas:
"The key is to acknowledge from the start that you have no idea how this will grow. When you accept that you don't know everything, you begin to design the system defensively. You identify the key areas that may change, which often is very easy when you put a little bit of time into it. For instance, you should expect that any part of the app that communicates with another system will likely change, so you need to abstract that away." -
一大堆文字問題,太麻煩了,總結一句就是,一切皆可變,因此要抽象。
jQuery Fundamentals做者Rebecca Murphey:
各個模塊之間聯繫的越密切,重用性越小,改變起來困難越大。
以上這些重要觀點,是構建架構的核心要素,咱們須要時刻銘記。
咱們來頭腦風暴一下,咱們須要一個鬆耦合的架構,各模塊之間沒有依賴,各個模塊和程序進行通訊,而後中間層接管和處理反饋相應的消息。
例如,咱們若是有一個JavaScript構建在線麪包店程序,一個模塊發出了一個信息多是「有42個圓麪包須要派件」。咱們使用不一樣的layer層來處理模塊發來的消息,作到以下:
這將防止咱們由於某個模塊出錯,而致使全部的模塊出錯。
另一個問題是安全,真實的狀況是,大多數人都不認爲內部安全是個問題,咱們本身內心說,程序是我本身構建的,我知道哪些是公開的那些私有的,安全沒問題,但你有沒有辦法去定義哪一個模塊才能權限訪問程序核心?例如,有一個chat聊天模塊,我不想讓他調用admin模塊,或者不想讓它調用有DB寫權限的模塊,由於這之間存在很脆弱,很容易致使XSS攻擊。每一個模塊不該該能作全部的事情,可是當前大多數架構裏的JavaScript代碼都有這種的問題。提供一箇中間層來控制,哪一個模塊能夠訪問那個受權的部分,也就是說,該模塊最多隻能作到咱們所受權的那部分。
咱們本文的重點來了,此次咱們提議的架構使用了咱們都很熟知的設計模式:module, facade和mediator。
和傳統的模型不同的是,爲了解耦各個模塊,咱們只讓模塊發佈一些event事件,mediator模式能夠負責從這些模塊上訂閱消息message,而後控制通知的response,facade模式用戶限制各模塊的權限。
如下是咱們要注意講解的部分:
1 設計模式
1.1 模塊論
1.1.1 綜述
1.1.2 Module模式
1.1.3 對象自面量
1.1.4 CommonJS模塊
1.2 Facade模式
1.3 Mediator模式
2 應用到你的架構
2.1 Facade - 核心抽象
2.2 Mediator - 程序核心
2.3 緊密聯合運做起來
你們可能都或多或少地使用了模塊化的代碼,模塊是一個完整的強健程序架構的一部分,每一個模塊都是爲了單獨的目的爲建立的,回到Gmail,咱們來個例子,chat聊天模塊看起來是個單獨的一部分,其實它是有不少單獨的子模塊來構成,例如裏面的表情模塊其實就是單獨的子模塊,也被用到了發送郵件的窗口上。
另一個是模塊能夠動態加載,刪除和替換。
在JavaScript裏,咱們又幾種方式來實現模塊,你們熟知的是module模式和對象字面量,若是你已經熟悉這些,請忽略此小節,直接跳到CommonJS部分。
Module模式
module模式是一個比較流行的設計模式,它能夠經過大括號封裝私有的變量,方法,狀態的,經過包裝這些內容,通常全局的對象不能直接訪問,在這個設計模式裏,只返回一個API,其它的內容所有被封裝成私有的了。
另外,這個模式和自執行的函數表達式比較類似,惟一的不一樣是module返回的是對象,而自執行函數表達式返回的是function。
衆所周知, JavaScript不想其它語言同樣有訪問修飾符,不能爲每一個字段或者方法聲明private,public修飾符,那這個模式咱們是如何實現的呢?那就是return一個對象,裏面包括一些公開的方法,這些方法有能力去調用內部的對象。
看一下,下面的代碼,這段代碼是一個自執行代碼,聲明裏包括了一個全局的對象basketModule, basket數組是一個私有的,因此你的整個程序是不能訪問這個私有數組的,同時咱們return了一個對象,其內包含了3個方法(例如addItem,getItemCount,getTotal),這3個方法能夠訪問私有的basket數組。
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;
}
}
}());
同時注意,咱們return的對象直接賦值給了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)
那在各個流行的類庫(如Dojo, jQuery)裏是如何來作呢?
Dojo
Dojo試圖使用dojo.declare來提供class風格的聲明方式,咱們能夠利用它來實現Module模式,例如若是你想再store命名空間下聲明basket對象,那麼能夠這麼作:
//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.provide一塊兒來使用,很是強大。
YUI
下面的代碼是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裏有不少Module模式的實現,咱們來看一個不一樣的例子,一個library函數聲明瞭一個新的library,而後建立該library的時候,在document.ready裏自動執行init方法。
function library(module) {
$(function() {
if (module.init) {
module.init();
}
});
return module;
}
var myLibrary = library(function() {
return {
init: function() {
/*implementation*/
}
};
}());
對象自面量
對象自面量使用大括號聲明,而且使用的時候不須要使用new關鍵字,若是對一個模塊裏的屬性字段的publice/private不是很在乎的話,可使用這種方式,不過請注意這種方式和JSON的不一樣。對象自面量:var item={name: "tom", value:123} JSON:var item={"name":"tom", "value":123}。
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
CommonJS
關於 CommonJS的介紹,這裏就很少說了,博客園有不少帖子都有介紹,咱們這裏要提一下的是CommonJS標準裏裏有2個重要的參數exports和require,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)});
有不少CommonJS標準的模塊加載實現,我比較喜歡的是RequireJS,它可否很是好的加載模塊以及相關的依賴模塊,來一個簡單的例子,例如須要將圖片轉化成ASCII碼,咱們先加載encoder模塊,而後獲取他的encodeToASCII方法,理論上代碼應該是以下:
var encodeToASCII = require("encoder").encodeToASCII;
exports.encodeSomeSource = function(){
//其它操做之後,而後調用encodeToASCII
}
可是上述代碼並沒用工做,由於encodeToASCII函數並沒用附加到window對象上,因此不能使用,改進之後的代碼須要這樣才行:
define(function(require, exports, module) {
var encodeToASCII = require("encoder").encodeToASCII;
exports.encodeSomeSource = function(){
//process then call encodeToASCII
}
});
CommonJS 潛力很大,可是因爲大叔不太熟,因此就不過多地介紹了。
Facade模式在本文架構裏佔有重要角色,關於這個模式不少JavaScript類庫或者框架裏都有體現,其中最大的做用,就是包括High level的API,以此來隱藏具體的實現,這就是說,咱們只暴露接口,內部的實現咱們能夠本身作主,也意味着內部實現的代碼能夠很容易的修改和更新,好比今天你是用jQuery來實現的,明天又想換YUI了,這就很是方便了。
下面這個例子了,能夠看到咱們提供了不少私有的方法,而後經過暴露一個簡單的 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
Facade和下面咱們所說的mediator的區別是,facade只提供現有存在的功能,而mediator能夠增長新功能。
講modiator以前,咱們先來舉個例子,機場飛行控制系統,也就是傳說中的塔臺,具備絕對的權利,他能夠控制任何一架飛機的起飛和降落時間以及地方,而飛機和飛機以前不容許通訊,也就是說塔臺是機場的核心,mediator就至關於這個塔臺。
mediator就是用在程序裏有多個模塊,而你又不想讓各個模塊有依賴的話,那經過mediator模式能夠達到集中控制的目的。實際場景中也是,mediator封裝了不少不想幹的模塊,讓他們經過mediator聯繫在一塊兒,同時也鬆耦合他們,使得他們之間必須經過mediator才能通訊。
那mediator模式的優勢是什麼?那就是解耦,若是你以前對觀察者模式比較瞭解的話,那理解下面的mediator圖就相對簡單多了,下圖是一個high level的mediator模式圖:
想一想一下,各模塊是發佈者,mediator既是發佈者又是訂閱者。
能夠看到,各模塊之間並無通訊,另外Mediator也能夠實現監控各模塊狀態的功能,例如若是Module 3出錯了,Mediator能夠暫時只想其它模塊,而後重啓Module 3,而後繼續執行。
回顧一下,能夠看到,Mediator的優勢是:鬆耦合的模塊由同一的Mediator來控制,模塊只須要廣播和監聽事件就能夠了,而模塊之間不須要直接聯繫,另外,一次信息的處理可使用多個模塊,也方便咱們之後統一的添加新的模塊到現有的控制邏輯裏。
肯定是:因爲全部的模塊直接都不能直接通訊,全部相對來講,性能方面可能會有少量降低,可是我認爲這是值得的。
咱們根據上面的講解來一個簡單的Demo:
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;
}
};
}());
而後有2個模塊分別調用:
//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
一個facade是做爲應用程序核心的一個抽象來工做的,在mediator和模塊之間負責通訊,各個模塊只能經過這個facade來和程序核心進行通訊。做爲抽象的職責是確保任什麼時候候都能爲這些模塊提供一個始終如一的接口(consistent interface),和sendbox controller的角色比較相似。全部的模塊組件經過它和mediator通訊,因此facade須要是可靠的,可信賴的,同時做爲爲模塊提供接口的功能,facade還須要扮演另一個角色,那就是安全控制,也就是決定程序的哪一個部分能夠被一個模塊訪問,模塊組件只能調用他們本身的方法,而且不能訪問任何未受權的內容。例如,一個模塊可能廣播dataValidationCompletedWriteToDB,這裏的安全檢查須要確保該模塊擁有數據庫的寫權限。
總之,mediator只有在facade受權檢測之後才能進行信息處理。
Mediator是做爲應用程序核心的角色來工做的,咱們簡單地來講一下他的職責。最核心的工做就是管理模塊的生命週期(lifecycle),當這個核心撲捉到任何信息進來的時候,他須要判斷程序如何來處理——也就是說決定啓動或中止哪個或者一些模塊。當一個模塊開始啓動的時候,它應該可否自動執行,而不須要應用程序核心來決定是否該執行(好比,是否要在DOM ready的時候才能執行),因此說須要模塊自身須要去斷定。
你可能還有問題,就是一個模塊在什麼狀況下才會中止。當程序探測到一個模塊失敗了,或者是出錯了,程序須要作決定來防止繼續執行該模塊裏的方法,以便這個組件能夠從新啓動,目的主要是提升用戶體驗。
另外,該核心應該能夠動態添加或者刪除模塊,而不影響其餘任何功能。常見的例子是,一個模塊在頁面加載初期是不可用,可是用戶操做之後,須要動態加載這個模塊而後執行,就像Gmail裏的chat聊天功能同樣,從性能優化的目的來看,應該是很好理解的吧。
異常錯誤處理,也是由應用程序核心來處理的,另外各模塊在廣播信息的時候,也廣播任何錯誤到該核內心,以便程序核心能夠根據狀況去中止/重啓這些模塊。這也是鬆耦合架構一個很重要的部分,咱們不須要手工改變任何模塊,經過mediator使用發佈/訂閱就能夠來作到這個。
各模塊包含了程序裏各類各樣的功能,他們有信息須要處理的時候,發佈信息通知程序(這是他們的主要職責),下面的QA小節裏提到了,模塊能夠依賴一些DOM工具操做方法,可是不該該和系統的其它模塊有依賴,一個模塊不該該關注以下內容:
Facade抽象應用程序的核心,避免各個模塊之間直接通訊,它從各模塊上訂閱信息,也負責受權檢測,確保每一個模塊有用本身單獨的受權。
Mediator(應用程序核心)使用mediator模式扮演發佈/訂閱管理器的角色,負責模塊管理以及啓動/中止模塊執行,能夠動態加載以及重啓有錯誤的模塊。
這個架構的結果是:各模塊之間沒有依賴,由於鬆耦合的應用,它們能夠很容易地被測試和維護,各模塊能夠很容易地在其它項目裏被重用,也能夠在不影響程序的狀況下動態添加和刪除。
關於自動註冊事件,須要遵照必定的命名規範,好比若是一個模塊發佈了一個名字爲messageUpdate的事件,那麼全部帶有messageUpdate方法的模塊都會被自動執行。有好處也有利弊,具體實現方式,能夠看我另一篇帖子:jQuery自定義綁定的魔法升級版。
儘管架構的大綱裏提出了facade能夠實現受權檢查的功能,其實徹底可能由mediator去作,輕型架構要作的事情實際上是幾乎同樣的,那就是解耦,確保各模塊直接和應用程序核心通訊是沒問題的就行。
這其實就是一個兩面性的問題,咱們上面說到了,一個模塊也許有一些子模塊,或者基礎模塊,好比基本的DOM操做工具類等,在這個層面上講,咱們是能夠用第三方類庫的,可是請確保,咱們能夠很容易地可否替換掉他們。
我打算去搞一份代碼樣本供你們參考,不過在這以前,你能夠參考Andrew Burgees的帖子Writing Modular JavaScript 。
技術上來將,沒有理由如今模塊不能和應用程序核心直接通訊,可是對於大多數應用體驗來講,仍是不要。既然你選擇了這個架構,那就要遵照該架構所定義的規則。
感謝Nicholas Zakas的原始貼,將思想總結在一塊兒,感謝Andree Hansson的technical review,感謝Rebecca Murphey, Justin Meyer, John Hann, Peter Michaux, Paul Irish和Alex Sexton,他們全部的人都提供了和本Session相關的不少資料。
也很是感謝博客園的湯姆大叔(TomXu),將本Session的內容整理成中文版本,如對你有用,請推薦一把。