1. 程式人生 > >【前端框架】Backbone.js在大型單頁面應用中的應用實踐

【前端框架】Backbone.js在大型單頁面應用中的應用實踐

Backbone.js是什麼?

Backbone.js是一個JavaScript MVC框架,提供了良好的程式碼組織能力,可以方便地將應用程式解耦成可以複用的部分,為建立大型的單頁面應用提供框架支援,目前的版本是0.9.10(注:現在已到1.2.1版本)。通過將應用程式分解成MVC模式中不同職責的模組,帶來了以下幾點好處。

  1. 降低維護成本。資料、控制器、檢視的更新都是獨立進行的,互相之間都是鬆散耦合的。
  2. 解耦資料和檢視,便於直接對業務邏輯進行單元測試。
  3. 便於團隊合作,負責UI開發和業務邏輯開發的工程師可以分工並行工作。對於Web前端開發來講,負責HTML模板和CSS的介面工程師和負責業務邏輯的JavaScript工程師可以協同工作。

Backbone.js算是比較輕量的MVC框架,所謂輕量,是說它只關注一個框架應該關注的最基本的事情——如何給應用分層、如何組織各種功能的程式碼。至於在實際開發中需要用到的Utils或UI元件,Backbone.js則沒有提供任何支援。但Backbone.js所依賴的Underscore.js是一個功能比較全面的非侵入式工具函式類庫,算是在Utils方面的一個補充。

輕量並不意味著功能薄弱。首先,Backbone.js的精髓是它定義前端MVC的方式和編碼哲學,並依據這些規定了如何去給程式碼分層,因此Backbone.js能夠讓前端工程在可維護性和擴充套件性上都得到質的提升;同時,由於其良好且易於理解的結構,各個模組之間都是鬆散耦合的,雖然目前官方並沒有提供根據實際需求build檔案的功能,但如果你願意,完全可以自己手工刪掉原始碼中的Bakcbone.View只使用Model和Collection;最後,Backbone.js的任何一個部分都是非常容易擴充套件的。因此,Backbone.js的功能實際上非常強大的。下面將介紹Backbone.js的主要元件(架構如圖1所示)。


圖1 架構圖

Backbone.Events和Backbone.Sync

Backbone.Events和Backbone.Sync兩個元件是Backbone.js非同步通訊、事件驅動的程式設計模型的基礎。

Backbone.js中所有的元件都通過_.extend()的方法“繼承”了Backbone.Events所提供的功能,可以維護一套自己的事件訂閱和回撥列表。通過Events.on(event,[callback],[context])和Events.off([event], [callback], [context])兩個方法來實現對事件的訂閱和取消訂閱。


早期版本中的事件訂閱和取消訂閱是通過Events.bind()和Events.unbind()兩個方法實現的,目前的版本中還保留了這兩個別名方法,但不推薦使用。

Backbone.js的所有元件都有一些內建的事件,可以查閱官方文件。除了預置事件外,通過Events.trigger(event,[*args])方法也可以方便地觸發自定義事件。


從0.9版開始,Backbone.Events提供了Events.listenTo(other,event,callback)和Events.stopListening([other],[event],[callback])兩個新方法來通過另外一種形式實現事件的訂閱和取消訂閱。

與on()和of f()不同,這種方式將監聽的主動權轉換了。舉個例子來說:有物件A和物件B,B.on('someThingHappened',A.doSomeThing)是當物件BsomeThingHappened時候知物件A去doSomeThing;而A.listenTo(B,'someThingHappend',A.doSomeThing)是物件A主動去盯著物件B,當它someThingHappend的時候去doSomeThing。

第二種方式最大的意義是變被動為主動,從而實現了IoC(Inversionofcontrol,控制反轉)。監聽者和被監聽者之間沒有了耦合,只要被監聽的物件能夠丟擲指定的事件,就可以和監聽者組合在一起,甚至不需要去關心被監聽物件的型別,這對程式碼的複用和行為抽象有很大的幫助。在測試層面,可以輕易地把被監聽物件換成mock的測試程式碼來模擬真實情況。

Backbone.Sync則將同伺服器的通訊封裝了起來,當Collection和Model需要和伺服器通訊交換資料時,會去呼叫Backbone.Sync中對應的方法併發送請求,如果伺服器端支援RESTfulAPI就可以將整個通訊過程描述得非常優雅並易於擴充套件。Sync的實現可以是jQuery.ajax()的封裝,也可以是其他的類庫提供的非同步通訊工具的封裝。

Backbone.Model和Backbone.Collection

Backbone.js中的Model和Collection共同構成了MVC中的M層。Model的本質就是一組以keyvalue形式儲存的資料,可以通過Backbone.Model.extend()來定義自己的Model。


上面的示例程式碼中defaults屬性定義了一組預設值,當Model初始化時,如果沒有指定defualts中所定義的屬性的值,就會用預設值來填充Model;initialize()方法會在Model被例項化時呼叫,用來進行一些初始化的操作;validate()方法會在Model的save()或set()方法被呼叫時執行,可以根據具體需求進行擴充套件。

通過Model的getters和setters可以實現對Model中屬性的讀寫,並且當set()方法被呼叫時,Model會將屬性變化的事件廣播給所有訂閱者(通常是檢視),驅動檢視重新渲染或其他關聯的Model的資料更新。

Collection是一組Model的集合,通過Collection可以將一組資料結構相同的Model有序地組織在一起,進行批量操作和管理等。同時,Collection代理了Undersore.js中眾多用來操作Collection的工具方法,例如find、filter、map等。

Model和Collection都可以通過RESTfulAPI同伺服器進行資料交換。

Backbone.View

Backbone.View是基於Backbone.js開發的Web App中的核心部分,負責使用者互動事件的捕捉和處理、把使用者輸入導向Model或Collection、渲染檢視、操作DOM等。Backbone.js的內部實現依賴$變數,因此DOM操作的庫可以在jQuery、Zepto或Ender等中選擇。從目前的情況來看,在桌面瀏覽器中,Sizzle.js(jQuer y所使用的SelectorEngine)的效能還是甩開Zepto幾條街的,因此面向桌面瀏覽器的開發還是推薦使用jQuery,移動端考慮到檔案體積等因素推薦使用Zepto。

對於一個Backbone.View來講,最重要的就是$el屬性,$el是一個jQuery物件(取決於所採用的DOM操作類庫),是一個檢視的最外層容器。容器所採用的HTML標籤可以通過tagName屬性來指定,可以是ul也可以是header或其他任何標籤,預設情況下是div。

容器內部的DOM渲染可以通過模板引擎來完成。Underscore.js本身提供了一個_.template()的方法,因此Backbone.js不需要額外的模板引擎支援。當然,如果有特殊的需求,例如和後端共用模板檔案,也可以選用Mustache等其他的模板引擎。


這樣一個View就被渲染到介面上了。上面的程式碼中還監聽了Model的change事件,當資料發生變化時,驅動檢視重新渲染。當Model的資料比較豐富時,只有一個屬性變化就重新渲染整個檢視顯然會帶來效能上的隱患,因此這裡的最佳實踐就是把render的過程break-down成粒度更細的片段。


值得注意的是,當一個View不再被需要時,一定要記得銷燬,除了銷燬DOM物件外,也要銷燬所有的事件監聽器。在只有Events.on()和Events.off()的年代,由於銷燬View時需要逐一取消訂閱所有的訊息,經常由於忘記解除某個繫結導致產生被稱為GhostView的垃圾物件,既無法被釋放也無法被回收。這也是Events.listentTo()方法帶來的另外一個好處——只要呼叫Events.stopListening()方法即可將此物件的所有事件監聽器銷燬。

所有的DOM事件也是通過$el來代理的,在Backbone.View中可以通過以下方法來方便地管理DOM事件。


Backbone.js的內部實現裡,在View的構造方法中呼叫View. initialize( )後將繼續呼叫View.delegateEvents()方法,這個方法將解析events屬性所定義的事件和回撥列表,並將全部事件代理到$el物件上。由於使用的是事件代理,某些不支援冒泡的DOM事件則必須另外監聽,如滾動條事件。


Backbone.Router和Backbone.History

Router是用來在URLHash和特定的動作或檢視之間做對映的。


最後一句History.start()是告訴Backbone.js開始對URLHash的變化進行監視,也可以隨時通過History.stop()來停止監視。同時,如果目標平臺是支援HTML5Histor yAPI的,那麼在start時傳入{pushstate:true}的引數,就可以去掉URL中的#字元,對SEO有一定幫助。

Backbone.js的適用場景

經常能在各種場合聽到前端工程師們討論“你們的XXX是用什麼做的啊?”“為什麼不用XXX啊?”這樣的問題。前端的類庫和框架林林總總,算在一起數量沒有一千也有五百,因此在面對一個新專案時難免會產生選擇恐懼症。

但說到底,技術方案都是由需求決定的,任何一個類庫或框架都有其適用範圍和最佳的使用場景,Backbone.js也不例外。Backbone.js的最佳使用場景是大型的單頁面應用:通過RESTfulAPI同伺服器通訊,然後根據資料的變化來驅動檢視重新渲染,整個程式建立在非同步通訊和事件驅動的程式設計模型之上。

單頁面應用給使用者帶來的使用體驗是沉浸式的、相對重型的,對於普通的Web Page和資料相對穩定、檢視不需要頻繁重新渲染的場景來講,Backbone.js顯然就沒有用武之地了。

Backbone.js和MV*不得不說的那點事兒

四十年前,Trygve Reenskaug基於Smartalk語言設計出了MVC模式,經過幾十年的發展,MVC出現了眾多的衍生。而我們今日說的MVC在不加特殊說明的情況下,通常指的是在伺服器端Web應用開發中大量使用的WebMVC。

對於典型MVC模式來講,View是無法直接獲得使用者輸入的,而Controller則是使用者輸入和View之間的橋樑。但在瀏覽器中,View層的載體是HTML,使用者輸入和互動行為都是基於HTML的,對事件的捕捉、輸入都由瀏覽器代勞了,並且輸入會首先進入View層,因此對於前端開發來講,嚴格意義的MVC是無法實現的。

因此,包括Backbone.js在內的JavaScript MVC框架的實現並沒有嚴格遵循MVC的定義,Controller的部分職責被轉移到了View層。Backbone.js對於前端MVC的定義非常易於理解,但對於沒接觸過MVC模式的同學來說在初期會有一些迷惑,原因是Backbone.js核心元件的命名。Backbone.js的核心元件包括Backbone.Collection、Backbone.Model、Backbone.View和Backbone.Router,而在早期的版本中,Backbone.Router元件的名稱是Backbone.Controller,這很容易讓人直接將其和MVC三層中的C層聯絡起來。但事實上,Backbone.Controller的作用是根據URLHash來在對應的行為和事件中做路由的,其功能同MVC中的C相比要簡單很多,因此在0.5前後Controller改名叫做Router了。從實體程式碼的角度看,View層其實是模板程式碼和Backbone.View中部分程式碼的綜合體,而Backbone.View中剩餘的部分才是MVC模式中Controller的概念,負責操作View以及資料在View和Model層中的流轉。

對於前端開發來說,使用者直接面對的一層並不是Controller而是View,使用者輸入也會首先進入View,因而用MVP和MVVM模式來描述架構更加合理。相比AngularJS等框架,Backbone.js的模式顯然更易於理解,學習曲線比較平緩,因為它並沒有引入過多的需要重新認知和理解的新概念而是儘量在靠近傳統的MVC模式,對於以前接觸過MVC模式開發的同學來說非常容易上手,而AngularJS中的Directive等概念還是需要一定認知成本的。

但如果從架構的角度討論,AngularJS其實是更為純粹並更接近嚴格意義上的MVC模式。為了把View的功能提升到一個應有的高度,引入了Directive的概念,通過擴充套件HTML標籤和自定義屬性來描述View,在Directive中來解析這些擴展出來的內容,理解成本和程式碼的複雜程度都有所提高,但View層功能則得到了質的提升。

反觀Backbone.js,並沒有在前端開發中真正的View的載體HTML上做太多文章,即便採用模板引擎也僅僅是把資料和HTML組合起來。但得益於其強大的擴充套件性,可以很容易將Knockout等Data-binding框架整合進來,從而實現MVVM的架構和分層。例如前文提到將render的過程Breakdown就完全可以用Data-binding來取代,省掉了手工更新DOM的煩瑣。

Backbone.js在豌豆莢PC客戶端2.0中的實踐

豌豆莢PC客戶端2.0的UI是完全建立在Web前端基礎上的。藉助Backbone.js,豌豆莢PC客戶端在開發大型單頁面應用中做了大量的實踐。通過在客戶端中捆綁一個Webkit引擎並對其進行擴充套件,使得跑在Webview中的前端程式碼跳出沙箱的限制,可以操作檔案系統並呼叫系統API,以此來進行桌面應用的開發。這樣做的好處有以下幾點。

  1. 極大提高開發效率:桌面應用的開發中,UI開發效率一直很低,但藉助HTML5和CSS3的新功能,Web前端可以輕易地做出精細程度和互動體驗都不輸桌面應用的UI,但開發效率和維護成本都極大降低。
  2. 易於跨平臺:UI依賴於Webkit引擎,而Webkit引擎本身是跨平臺的,因此可以很容易地移植到Web上或其他桌面平臺。
  3. 快速迭代和改進:由於維護和擴充套件成本的下降,可以快速的將原型設計產品化並進行驗證,提高產品迭代和改進的速度。

但與此同時,用Web前端技術開發桌面應用也要面臨巨大的挑戰。首先就是記憶體消耗。使用者在使用瀏覽器和使用桌面應用時的心理預期是不一樣的,即使一段有記憶體洩漏問題的前端程式碼跑在瀏覽器裡,當出現執行緩慢時大不了一下重新整理頁面,但對桌面應用來講,大多數情況下沒有重新整理Webview的機會,因此必須對記憶體實現更精細的控制。由於Webview本身依賴於GC,前端無法主動管理和回收記憶體,因此必須藉助ChromeDeveloper tools中的Profiles等工具查找出現記憶體洩漏的地方從而進行改善,這也依賴於大量的經驗積累。

其次是執行速度和介面響應速度。由於Webview是單執行緒的,但單頁面應用面臨的是數百倍於Web Page的業務邏輯複雜度,同時業務邏輯的執行和UI共用一個執行緒,如何優化執行速度也是一個很大的挑戰。雖然目前WebUI的介面流暢程度無法完全達到桌面應用的水平,但依然是很有競爭力且有提高空間的。(責編:陳秋歌)

作者簡介

趙望野:豌豆莢前端團隊負責人。

本文選自《程式設計師》2013年3月刊。2000年創刊至今所有文章目錄請檢視程式設計師封面秀。歡迎訂閱程式設計師電子版(含iPad版、Android版、PDF版)。