可伸縮的同構Javascript代碼
原文 http://efe.baidu.com/blog/isomorphic/
原文: http://blog.nodejitsu.com/scaling-isomorphic-javascript-code/
譯者注:這是一篇2011年的老文了,最近苦惱于單頁面應用的首屏速度與SEO問題,期望本文能給有同樣煩惱的同學們帶來些啟示。
先花點時間想想你是有多么頻繁地聽到“Model-View-Controller”(MVC)這詞兒,但你真正明白它的意義嗎?在較高層次上而言,它是指在一個基于圖像系統(非光柵化圖像,比如游戲)以展示為主的應用中對功能的 關注點分離(separation of concerns) 。進一步看,它就是一堆表示不同事物的專有名詞。過去,許多開發者社區都創造了各自的MVC解決方案,它們都能很好地應對流行的案例,并且在一步一步地發展。最好的例子就是Ruby和Python社區以及它們基于MVC架構的Rails與Django框架。
MVC模式已經被其它語言所接受,比如Java,Ruby和Python。但是對于Node.js而言還不夠好,其中的一個原因就是: Javascript現在是一個同構的語言了 。 同構 的意義就在于任何一段代碼(當然有些特殊代碼例外)都能同時跑在客戶端與服務器端。從表面上講,這個看似無害的特性帶來了一系列當前的MVC模式無法解決 的挑戰。在這篇文章中我們會探尋目前存在一些的模式,看看它們都是怎樣實現的,同時關注不同的語言及環境。另外也談談它們為什么對于真正同構的 Javascript而言還不夠好。在最后,我們會了解一種全新的模式:Resource-View-Presenter。
題要
設計模式在應用開發中至關重要。它們概述、封裝了應用程序及其環境中值得關注的地方。在瀏覽器與服務器之間這些關注點差異很大:
- 視圖是短暫的(如在服務器上)還是長期存在的(如在瀏覽器上)?
- 視圖是否能跨案例或場景復用?
- 視圖是否該被應用特定的標簽標記?
- 一堆堆的業務邏輯應該放哪里?(在Model中還是在Controller中?)
- 應用的狀態應該如何持久化和訪問?
讓我們來關注下目前存在的一些模式,看看它們是如何回答上面這些問題的:
- Model-View-Controller
- Model2
- Model-View Presenter and Model-View-ViewModel
- 現代化的Javascript實現
- Resource-View-Presenter介紹
- 結語
MVC

Model-View-Controller
傳統的Model-View-Controller模式(譯注:為了與后續的Presenter, ViewModel保持一致,Model, View, Controller都不做翻譯)假定View是持續的,同時,Controller是可熱插拔的。比如說一個View對于是否登陸會對應不同的 Controller。在一個較高的層次上而言,MVC并不關注View是如何被渲染(如具體是采用何種模版引擎)。
通過View是持續的及View定義用戶交互來看,傳統的MVC是對前端開發十分有利的模式。稍后我們會看到現實中, Backbone.js 實現的一個稍微改動的MVC模式。
Model2

Model2 Model-View-Controller
如果之前從來沒有聽過 Model2 ,請不要驚慌。它是一個可以追溯到1999年的設計模式,由Govind Seshadri提出并發表在 Understanding JavaServer Pages Model 2 architecture 。可以說,Model2并不需要完全實現MVC模式,但現代大多數的實現(比如 Ruby on Rails )都以那種方式來設計。
在像Ruby on Rails的那些類Model2的框架中有一個共識:“富Model、瘦Controllers”。這不適用于所有的應用,但在作者看來,在實踐中這一思 路應用得還是相當廣泛的。由于傳統MVC中的Controller需要監聽View并對輸入作出反應,Controller會趨于繁重(比如越來越多的業 務邏輯),因此“富Model、瘦Controller”的方式看上去更優。
鑒于無狀態的HTTP,Model2的View是很短暫的:不同請求之間,View不保持狀態。在大多數服務器端框架中,應用的狀態都是通過 Session Cookies 存儲的。這使得Controller與View之間的單向通信非常有序,但這卻不便于前端的開發。
MVP & MVVM
MVP(Model-View-Presenter)和MVVM(Model-View-ViewModel)模式與傳統的MVC十分類似,除了以下幾個關鍵區別:
- View不再直接持有對Model的引用
- Presenter(或ViewModel)持有對View的引用并借助Model的改變來更新View
MVP模式被Martin Fowler多次論述( 這兒 還有 這兒 ),并且經常基于以下兩個不同的實現來討論:
- 被動的View(Passive View) :設計盡可能簡單的View,除了必要的界面操作,其它所有的業務邏輯都應該包含在Presenter中
- 監督Controller(Supervising Controller) :View可以包含簡單的邏輯,Presenter只處理那些View無法處理的系統需求

Model-View-Presenter

Model-View-ViewModel
MVP與MVVM幾乎難以區分,除了一點:MVVM假定ViewModel中的改變會通過一個穩健的數據綁定引擎反映到View中。Niraj Bhatt在他的《 MVC vs. MVP vs. MVVM 》一文中指出:“如果在MVP中View有一個叫isChecked的屬性并且由Presenter來設置的話,那么在MVVM中ViewModel也會有一個叫isChecked的屬性并且與View保持同步。”
MVP與MVVM的優勢是Presenter(或ViewModel)更容易進行單元測試。這是因為View的狀態是由Presenter通過方法調用(MVP)或者由ViewModel通過屬性設置(MVVM)來確定的。
對前端開發而言這兩個模式都是可接受的好選擇。在瀏覽器中,路由層可以將控制權交由適當的Presenter(或ViewModel),后者又可 以更新并響應持續的View。通過一些小修改這兩個模式都可以很好的運行在服務器端,其中的原因就在于Model與View之前沒有直接的聯系,這允許短 暫View經由給定的Presenter(或ViewModel)進行渲染。就像稍后會描述的那樣,這種改變后的模式就是真正意義上的同構。
現代化的Javascript實現
上面介紹的那些模式目前已經有許多現代化的實現:
這些框架通常都用于構建單頁面應用(SPA, Singele-Page Application)。單頁面應用的用戶交互有兩個截然不同的特點
- onHashChange 和 pushState 事件:當瀏覽器的URL改變時觸發,比如導航到某某頁面
- DOM 事件:當用戶在DOM上進行特定交互時觸發,比如點擊錨點標簽
讓我們來瞅瞅具體的框架,如果你對此感興趣的話可以參考下Peter Michaux關于 JavascriptMVC框架開發 的文章。
Backbone
Backbone.js 是當今最流行的客戶端開發框架之一。它的核心是一個傳統MVC模式的實現。但就像之前提到的,深入了解后就會發現這貨與傳統MVC有些出入。

Backbone Model-View-Controller
在上圖中我們通過hashchange事件與DOM事件來分離控制流,以此來區分Backbone提供的入口點。通過區分這個細微差別充分說明了Backbone與傳統MVC的一個重要的區別: 視圖可以操作數據 。當我們查看 Backbone的TODO示例 時就能更清楚地認識到這點:
window .AppView = Backbone.View.extend({// ....
//
// 通過一個Todo Model實例的參數來實例化View
//
addOne: function (todo) {
var view = new TodoView({model: todo});
this .$( "#todo-list" ).append(view.render().el);
}
// ....
});
window .TodoView = Backbone.View.extend({
// ....
//
// 視圖直接更新了Model的狀態
// 這有別于傳統MVC中視圖只監聽數據變化的觀點
//
toggleDone: function () {
this .model.toggle();
}
// ....
});</pre>
這個有別于傳統MVC模式的改變讓大多數的Backone應用都有相似的感覺:簡單的Controller、Model都被合并到龐大的View 中。客觀地來看,那些業務邏輯繁重的View本質上是Presenter。在大多數Backbone的項目代碼庫中你都能見到,在jQuery或 Zepto等DOM框架的幫助下,大量的View被揉合在了一起。
對傳統MVC模式的改變并沒有錯。在前端開發中,View持有對Model的引用能消除應用程序中大量的記賬式邏輯。然而不管怎樣,這個模式是不同構的。
Batman
Batman.js 是在 2011年的JSConf 上發布的一個全新的Javascript框架(譯注:這貨已經不維護啦…就全當看看思路吧)。雖然Batman中的實體是Model、View與 Controller,但其強大的數據綁定引擎與純粹的HTML視圖都暗示著這貨實際是Model-View-ViewModel模式的實現。
![]()
Batman Model-View-ViewModel
沒有大量使用Batman開發時很難有自信說大多數的Batman項目代碼庫都長什么樣。但有種說法是:在應用中強調數據綁定引擎和瘦View預示著業務邏輯最終會在Controller與Model之間轉播。
與Backone一樣,Batman也改造了傳統的Model-View-ViewModel模式:Model能直接與View通信并且 ViewModel(如Controller)不再直接操作View。另外,由于Model與View之間存在引用,這模式不能輕易的作為一個服務器端模 式來重用。但經過小小的改動就能變成服務器端的模式,如在Model層做一個適配使之能渲染一個靜態的View來響應實時的請求。
實時的含義
在眾多開發者關注的話題中“ 實時Web應用(realtime web applications) ”一直名列前茅。那么以上討論的那些模式對實時的支持又是如何的呢(比如 WebSockets )?
- Model-View-Controller (支持):Model提供實時的事件監聽并且能適當地更新View
- Model2(不支持):該模式使用了短暫View的概念,這意味著Controller不會監聽來自Model的事件(譯注:即使監聽了也沒用,View無狀態、不保存)
- Model-View-Presenter(支持):Model提供實時的事件監聽,會將事件派發給Presenter進而以適當的方式更新View
- Model-View-ViewModel(支持):Model提供實時的事件監聽,會將事件派發給ViewModel進而以適當的方式更新View
MVC、MVP、MVVM的這些特性使得 Backbone.js 和 Batman.js 對前端開發而言是實時框架。但在服務器端就不是這么一回事兒了:傳統的MVC、MVP和MVVM模式由于View與Model之間緊密的聯系阻礙了其與靜態View的協作。
Resource-View-Presenter介紹
如上所述:MVC、MVP、MVVM模式都不能同時工作在客戶端與服務器端。Resource-View-Presenter模式的關鍵之處就在 于意識到了沒有任何模式可以不經修改、完美地同時運行在客戶端與服務器端。如之前介紹MVP與MVVP時提到的,通過對Model和View層去耦合,這 兩個模式可以真正地做到同構。
Resource-View-Presenter主要地思路是:
- View與Model去耦合
- 識別客戶端與服務器端的區別并為之進行規劃
- 期待瘦View、富Presenter和Resource
- 更傾向于將業務邏輯放在Resource中而非Presenter
- 允許短暫View(如服務器端地靜態視圖)與持續View(如客戶端的DOM)同時存在
- 更傾向于使用Presenter而非ViewModel來保持標記語言(如HTML)的純粹性
- 假設Presenter與Model是持續的
雖然這些點看上去顯得比較隨意,但每一個都有特殊的目的:
- 通過View與Model的去耦合,我們可以允許短暫View與持續View的并存
- 瘦View能與更現代化、更邏輯無關的模版引擎(比如 weld 和 mustache )保持一致
- 使用Presenter替代ViewModel,使之能與對設計師友好的模版引擎(比如 weld )保持一致
- 假設Presenter與Model在客戶端與服務器端都是持續的,這能使兩端中的實時功能都被封裝在Presenter中
進一步看,RVP在客戶端與傳統的MVP模式類似。將Model改名為Resource主要是受“更傾向于將業務邏輯放在Resource中而非 Presenter”這一個思路的影響。這也使得Resource在RVP中更像Model2模式中的重型Model,而非傳統MVP模式中的 Model。在應用RVP時,對于哪些邏輯應該屬于Presenter有兩點建議: 那些對“瘦”View而言太繁重的展現邏輯,以及那些需要使用全局應用狀態的業務邏輯 。
就像 Backbone.js 和 Batman.js ,客戶端的RVP實現應該同時支持OnHashChange/pushState事件與DOM事件。

客戶端Resource-View-Presenter
在服務器端的Resource-View-Presenter與客戶端上的幾乎完全相同,除了一個明顯的例外:View是短暫,不會向Presenter 傳遞調用也不會持有對Presenter的引用。實際上,當基于JSON的Web服務器上使用RVP架構時,View幾乎都沒有存在的必要,僅僅只需要調 用下JSON.stringify()就好啦。

服務器端Resource-View-Presenter
初步看來,服務器端的RVP和Model2很像,區別在于持續的Presenter和Model都能支持實時事件,這就使得這種相似性顯得比較膚淺了。Model是由實時數據源(比如 Redis PubSub 和 CouchDB changes )支持的,RVP通過監聽Model的事件與改變來實現對實時事件的支持。
需要特別關注的是對實時的支持,因為它可以讓應用開發者聚焦于業務邏輯的開發而非底層的網絡傳輸。這聽上去似乎無關緊要,但是如果仔細觀察 Express 和 Socket.io 提供的模式就能看出明顯的差異:

Express和Socket.io
這并非是指 Express 和 Socket.io 不優秀,而是在說:它們都很明確自己提供的是什么而且做得相當好。在更高層次的設計模式中,這都不是事兒。
結語
編寫大型應用難,在服務器與客戶端之間封裝、重用組件就更難了。通過這些分析,期望能讓RVP模式在具體項目中如何實施更清晰,從而使之更容易在服務器與客戶端之間復用組件。
沒有時間去自己實現?沒問題!這篇文章就是來源自 大量開源項目 ,在開發者深思熟慮、辛苦工作后產生的結晶。我們的觀念始終如一:創建最好的工具來引領最好的系統。
</div>