Backbone.js 的技巧和模式

發表於2013-08-22

Backbone.js是一個開源JavaScript“MV*”框架,在三年前它的第一次釋出的時候就獲得了顯著的推動。儘管Backbone.js為Javascript應用程式提供了自己的結構,但它留下了大量根據開發者的需要而使用的設計模式和決策,並且當開發者們第一次使用Backbone.js開發的時候都會遇到許多共同的問題。 因此,在這篇文章中,我們除了會探索各種各樣你能夠應用到你的Backbone.js應用中的設計模式外,我們也會關注一些困惑開發者的常見問題。

執行物件的深複製

JavaScript中所有原始型別變數的傳遞都是值傳遞。所以,當變數被引用的時候會傳遞該變數的值。

例如,以上程式碼會將helloWorldCopy 的值設為helloWorld的值。所以對於helloWorldCopy 的所有修改都不會改變helloWorld, 因為它是一個拷貝。另一方面,JavaScript所有非原始型別變數的傳遞都是引用傳遞,意思是當變數被引用的時候,JavaScript會傳遞一個其記憶體地址的參照。

舉個例子,上面的程式碼會將helloWorldCopy 設為helloWorld 物件的別名,此時,你可能會猜到,對helloWorldCopy 的所有修改都會直接在helloWorld 物件上進行。如果你想得到一個helloWorld 物件的拷貝,你必須對這個物件進行一次複製。 你可能想知道,“為什麼他(作者)要在這篇文章中解釋這些按引用傳遞的東西?”很好,這是因為在Backbone.js的物件傳遞中,並不對進行物件複製,意味著如果你在一個模型中呼叫 get( ) 方法來取得一個物件,那麼對它的任何修改都會直接在模型中的那個物件進行操作!讓我們通過一個例子來看看什麼時候它會成為你的麻煩。假設現在你有一個如下的Person 模型:

並且假設你建立了一個新的person 物件:

現在讓我們對新物件person 的屬性做一點修改。

上述程式碼會對person 物件中的name 屬性進行修改。接下來讓我們嘗試修改person 物件的address 屬性。在這之前,我們先對address屬性新增校驗。

現在,我們會嘗試使用一個不正確的ZIP 碼來修改物件的address 屬性。

為什麼會這樣?我們的校驗方法不是已經返回一個錯誤了嗎?!為什麼attributes 屬性還是被改變了?原因正如前面所說,Backbone.js不會複製模型的attributes物件;它僅僅返回你所請求的東西。所以,你可能會猜到,如果你請求的是一個物件(如上面的address),你會得到那個物件的引用,並且你對這個物件的所有修改都會直接地操作在模型中的實際物件中(因此這樣的修改方式並不會導致校驗失敗,因為物件的引用並沒有改變)。這個問題很可能會導致你花費幾小時來進行除錯和診斷。 這個問題會逮住一些使用Backbone,js的新手甚至經驗豐富卻不夠警惕的JavaScript開發者。這個問題已經在GitHub issues 的Backbone.js部分引起了大量的討論。像 Jeremy Ashkenas 所指出的,執行深複製是一個非常棘手的問題,對那些有較大深度的物件來說,它將會是個非常昂貴的操作。 幸運地,jQuery提供了一些深複製的實現,$.extend。順帶說一句,Underscore.js,Backbone.js的一個依賴外掛,也提供了類似的方法 _.extend ,但我會避免使用它,因為它並不執行深複製。

我們現在得到了 address 物件的一個精確的拷貝,因此我們可以隨心所欲地修改它的內容而不用擔心修改到person中的address 物件。你應該意識到此模式適用於上述那個例子僅因為address 物件的所有成員都是原始值(numbers, strings, 等等),所以當深複製的物件中還包含有子物件時必須謹慎地使用。你應該知道執行一個物件的深複製會產生一個小的效能影響,但我從沒見過它導致了什麼顯而易見的問題。儘管這樣,如果你對一個複雜物件的執行深複製或者一次性執行上千個物件的深複製,你可能會想做一些效能分析。這正是下一個模式出現的原因。

為物件建立Facades

在真實的世界裡,需求經常會更改,所以那些通過模型和集合的查詢而從終端返回的JSON資料也會有所改變。如果你的檢視與底層資料模型緊緊地耦合,這將會讓你感到非常麻煩。因此,我為所有的物件建立了獲取器和設定器。 很多人贊成這種模式。就是如果任何底層資料結構被改變,檢視層不應該更新太多;當你只有一個資料入口的時候,你就不太可能忘記執行深複製,並且你的程式碼會變得更加可維護和除錯。但帶來的負面影響是這種模式會讓你的模型和集合有點膨脹。 讓我們通過一個例子來搞清楚這個模式。假設我們有一個Hotel 模型,其中包含了rooms和當前可用的rooms,我們希望能夠通過床位尺寸值來取得相應的rooms。

讓我們假設明天你將會發布你的程式碼,並且終端的開發者忘記告訴你rooms的資料結構從Object變成了一個array。你的程式碼現在如下所示:

為了將Hotel 轉換為應用所期望的資料結構,我們僅僅更新了一個方法,這讓我們整個App的仍然正常工作。如果我們沒有建立一個rooms資料的獲取器,我們可能不得不更新每一個rooms的資料入口。理想情況下,你為了使用一個新的資料結構而會想要更新所有的介面方法。但如果由於時間緊迫而不得不盡快釋出程式碼的話,這個模式能拯救你。 順帶提一下,這個模式既可以被認為是一個facade 設計模式,因為它隱藏了物件複製的細節,也可以被稱為bridge 設計模式,因為它可以被用於轉換所期望的資料結構。因而一個好的習慣是在所有的物件上使用獲取器和設定器。

儲存資料但不同步到伺服器

儘管Backbone.js規定模型和集合會對映到REST-ful終端,但你有時候會發現你只是想將資料儲存在模型或者集合而不同步到伺服器。一些其他關於Backbone.js的文章,像“Backbone.js Tips: Lessons From the Trenches”就講解過這個模式。讓我們快速地通過一個例子來看看什麼時候這個模式會派上用場。假設你有個ul列表。

當n值為200並且使用者點選了其中一個列表項,那個列表項會被選中並新增了一個類以直觀地顯示。實現它的一個方法如下所示:

現在我們想要知道哪一個item被選中。一個方法是遍歷整個列表。但如果這個列表過長,這會是一個昂貴的操作。因此,當使用者點選其中的列表項時,我們應該將它儲存起來

現在我們能夠輕易地搜尋我們的模型來確定哪一個item被選中,並且我們避免了遍歷文件物件模型 (DOM)。這個模式對於儲存一些你想要跟蹤的外部資料非常有用;還要記住的是你能夠建立不需要與終端相關聯的模型和集合。 這個模式的消極影響是你的模型或集合並不是真正地採用RESTful 架構因為它們沒有完美地對映到網路資源。另外,這個模式會讓你的模型帶來一點兒膨脹;並且如果你的終端嚴格地只接收它所期望的JSON資料,它會給你帶來一點兒麻煩。

渲染檢視的一部分而不是渲染整個檢視

當你第一次開發Backbone.js應用,你的檢視一般會是這樣的結構:

在這裡,你的模型的任何改變都會觸發一次檢視的完整的重新渲染。當我第一次使用Backbone.js來做開發的時候,我也使用過這種模式。但隨著我程式碼的膨脹,我很快意識到這個方法是不可維護和不理想的,因為模型的任何屬性的改變都會讓檢視完全重新渲染。 當我遇到這個問題的時候,我馬上在Google搜尋其他人是怎麼做的並且找到了Ian Storm Taylor的部落格寫的一篇文章, “Break Apart Your Backbone.js Render Methods,”,其中他提到了監聽模型個別的屬性改變並且響應的方法僅僅重新渲染檢視的一部分。Taylor也提到重渲染方法應該返回自身的this物件,這樣那些單獨的重渲染方法就可以輕易地串聯起來。下面的這個例子已經作出了修改而變得更易於維護和管理了,因為當模型屬性改變的時候我們僅僅更新相應部分的檢視。

還要提到的是,許多外掛,像 Backbone.StickIt 和 Backbone.ModelBinder,提供了檢視元素和模型屬性之間的鍵值繫結,這能夠節省你很多的相似程式碼。因此,如果你有很多複雜的表單欄位,可以試著使用它們。

保持模型和檢視分離

像Jeremy Ashkenas 在Backbone.js的 GitHub issues指出的一個問題,除了模型不能夠由它們的檢視來建立以外,Backbone.js並不在資料層和檢視層之間實施任何真正的關注點分離。你覺得應該在資料層和檢視層之間實施關注點分離嗎?我和其他的一些Backbone.js開發者,像Oz Katz和 Dayal,都認為這個答案毫無疑問應該是要的:模型和集合,代表著資料層,應該禁止任何繫結到它們的檢視的入口,從而保持一個完全的關注點分離。如果你不遵循這個關注點分離,你的程式碼很快就會變得像義大利麵條那樣糾纏不清,而沒有人會喜歡這種程式碼。 保持你的資料層和檢視層完全地分離可以使你擁有更加地模組化,可重用和可維護的程式碼。你能夠輕易地在你的應用中重用和擴充模型和集合而不需要擔心和他們繫結的檢視。遵循這個模式能讓新加入專案的開發者快速的投入到程式碼中。因為它們精確的知道哪裡會發生檢視的渲染以及哪裡存放著應用的業務邏輯。 這個模式也強制使用了單一責任原則,該原則規定了每一個類應該只有一個單獨的責任,並且它的職責應該封裝在這個類中,因為你的模型和集合應該只負責處理資料,檢視應該只負責處理渲染。

路由器中的引數對映

使用例子是展示這個模式如何產生的最好方法。例如:有一些搜尋頁面,它們允許使用者新增兩個不同的過濾型別,foo 和bar,每一個都附有大量的選項。因此,你的URL結構看起來將會像這樣:

現在,所有的路由使用一個確切的檢視和模型,所以,理想狀況下,你會樂意它們都用同一個方法,search()。但是,如果你檢查Backbone.js,會發現沒有任何形式的引數對映;這些引數只是簡單地從左到右扔到方法裡面去。所以,為了讓它們都能使用相同的方法,你最終要建立不同的方法來正確地對映引數到search()方法。

和你想的一樣,這種模式會快速地膨脹你的路由。當我第一次使用接觸這種模式的時候,我嘗試使用正規表示式在實際方法定義中做一些解析而“神奇地”對映這些引數,但這隻能在引數容易區分的情況下起作用。所以我放棄了這個方法(我有時候依然會在Backbone外掛中使用它)。我在issue on GitHub上提出過這個問題,Ashkenas 給我的建議是在search方法中對映所有的引數。 下面這段程式碼已經變得更加具備可維護性:

這個模式可以徹底地減少路由器的膨脹。然而,要意識到它對於不可識別的引數時無效的。舉個例子,如果你有兩個傳遞ID的引數並且都它們以 XXXX-XXXX 這種模式表現,你將無法確定哪一個ID對應的是哪一個引數。

model.fetch() 不會清除你的模型

這個問題通常會絆倒使用Backbone.js的新手: model.fetch() 並不會清理你的模型,而是會將取回來的資料合併到你的模型當中。因此,如果你當前的模型有x,,y 和 z 屬性並且你通過fetch得到了一個新的 y 和z 值,接下來 x 會保持模型原來的值,僅僅 y 和z 的值會得到更新,下面這個例子直觀地說明了這個概念。

PUT請求需要一個ID屬性

這個問題也只通常出現在Backbone.js的新手中。當你呼叫.save() 方法時,你會傳送一個HTTP PUT 請求,要求你的模型已經設定了一個ID屬性。HTTP PUT 是被設計為一個更新動作的,所以傳送PUT請求的時候要求你的模型已有一個ID屬性是合情理的。在理想的世界裡,你的所有模型都會有一個名為id的屬性,但現實情況是,你從終端接收的JSON資料的ID屬性並不總是會剛好命名為id。 因此,如果你需要更新你的模型,請確定在儲存前你的模型具有一個ID。當終端返回的ID屬性變數名不為 id 的時候,0.5及以上的版本的Backbone.js允許你使用 idAttribute 來改變ID屬性的名字。 如果使用的Backbone.js的版本仍低於0.5,我建議你修改集合或模型中的 parse 方法來對映期望的ID屬性到真正的ID屬性。這裡有一個讓你快速掌握這個技巧的例子,讓我們假設你有一個cars集合,它的ID屬性名為carID .

頁面載入中的模型資料

一些時候你會發現你需要在頁面載入的時候就使用資料來初始化你的集合和模型。一些關於Backbone.js模式的文章,像Rico Sta Cruz的“Backbone Patterns”和Katz的“Avoiding Common Backbone.js Pitfalls,”談論到了這個模式。使用你選擇的服務端語言,通過嵌入程式碼到頁面並將資料放在單個模型的屬性或JSON當中,你能夠輕易地實現這個模式。舉個例子,在Rails中,我會這樣使用:

使用這個模式能夠通過“馬上渲染你的頁面”來提高你的搜尋引擎排名,並且它能通過限制應用的HTTP請求來徹底地縮短你的應用啟動和執行所花費的時間。

處理驗證失敗的模型屬性

你經常會想知道哪一個模型屬性的驗證失敗了。舉個例子,如果你有一個極度複雜的表單域,你可能想知道哪一個模型屬性驗證失敗,這樣你能夠高亮顯示相應的表單域。不幸的是,提醒你的檢視哪一個模型屬性驗證失敗並沒有直接在Backbone.js中實現,但你可以使用不同的模式來處理這個問題。

返回一個錯誤物件

通知你的檢視哪一個模型屬性驗證失敗的一個模式是回傳一個帶有某種標誌的物件,該物件中詳述哪一個模型屬性驗證失敗,就像下面這樣:

這個模式的優點是你在一個位置處理了所有的無效資訊。缺點是如果你處理不同的無效屬性,你的屬性校驗部分會變為一個比較大的 switch 或 if 語句。

廣播傳統錯誤事件

由我朋友Derick Bailey建議的一個替換的模式,是對個別的模型屬性觸發自定義錯誤事件。這能讓你的檢視為個別的屬性繫結指定的錯誤事件。

這個模式的優點是你的檢視繫結了明確型別的錯誤事件,並且如果你對每一型別的屬性錯誤有明確的執行指令,它能整頓你檢視程式碼並使它更加可維護。這個模式的一個缺點是如果存在太多不同的想要處理的屬性錯誤,你的檢視程式碼會變得更加臃腫。 兩個模式都有他們的優缺點,所以在你應該考慮哪一種模式更加適合你的用例。如果你想對所有的驗證失敗處理都採用一個方法,那第一個方法會是個好選擇;如果你的每一個模型屬性都有明確的UI改變,那選第二個方法會更好。

HTTP狀態碼200觸發錯誤

如果你的模型或集合訪問的終端返回了無效的JSON資料,它們會觸發一個“error”事件,即使你的終端返回的HTTP狀態碼是200。這個情況通常出現在根據模擬JSON資料來做本地開發的時候。所以一個好方法是把你正在開發中的所有的模擬JSON檔案都扔到JSON 驗證器中檢驗。或者為你的IDE安裝一個外掛可以捕捉任何格式錯誤的JSON。

建立一個通用的錯誤展示

建立一個通用的錯誤展示意味著你有一個統一的模式來處理和顯示錯誤資訊,這能夠節省你的時間,並能提升使用者的整體體驗。在我開發的任何的Backbone.js 應用中我都建立了一個通用的檢視來處理警告。

上面這個檢視首先檢視在檢視之內是否已經宣告瞭 in-page-alert div。如果沒有宣告,它會回到被宣告在佈局的某個地方的通用 body-alert div中。這讓你能夠傳遞一個一致的錯誤資訊給你使用者,並在你忘記指定一個特定的 in-page-alert div時提供有效的備用div。上面的模式簡化了你在檢視中對錯誤資訊的處理工作,如下面所示:

更新單頁應用的文件標題

這比關注任何東西都更加有用。如果你正在開發一個單頁應用,請記得更新每一頁的文件標題!我寫過一個簡單的Backbone.js外掛,Backbone.js Router Title Helper,它通過擴充Backbone.js路由器來簡單又優雅地實現這個功能。它允許你指定一個標題的物件常量,它的鍵對映到路由的方法名,值則是頁標題。

在單頁應用中快取物件

當我們討論單頁應用的時候,你有必要遵循的另一個模式是快取那些將會被重複使用的物件。這個技巧是相當簡單和直接的:

這個模式將會讓你的應用像小石子那般飛快起來,因為你不需要重新初始化你的Backbone.js物件。然而,它可能會導致你的應用的記憶體佔用變得相當大;因此,我一般僅僅快取那些貫穿整個應用的物件。如果你以前開發過Backbone.js應用,你可能會問自己“如果我想重新獲取資料會怎樣?”那麼你可以在每次路由被觸發的時候重新獲取資料。

當你的應用必須從終端中取回最新資料的時候(例如,一個收信箱),這個模式會很好用。然而,如果你正在取回的資料依賴應用的狀態(假設狀態是靠你的URL和引數維持的),那即使自使用者上一次瀏覽頁面以來應用的狀態沒有改變,你仍會更新資料。一個比較好的解決辦法是僅當應用的狀態(parameter)被改變的時候才更新資料。

JSDoc功能和Backbone.js的類

我喜歡編制文件並且是JSDoc的忠實粉絲,我用JSDoc 為所有遵循下面所示格式的Backbone 類和方法生成了文件:

如果你為上面這種格式的Backbone類編制文件,你可以編制一份漂亮的文件,它包含你所有的類和帶有引數,返回值和描述的方法。請確保initialize 始終是第一個宣告的方法,因為這有助於生成JSDoc。如果你想要看一個使用JSDoc的專案的例子,請查閱HomeAway Calendar Widget。有個 Grunt.js 外掛,grunt-jsdoc-plugin 外掛,使用它們會把生成文件作為構建過程的一部分。

實踐測試驅動開發

在我看來,如果你正在使用Backbone.js,你應該讓你模型和集合遵循測試驅動開發(TDD)。我通過第一次為我的模型和集合編寫失敗的 Jasmine.js 單元測試而開始遵循TDD。一旦我寫的單元測試編寫和失敗,我就把模型和集合排出。通過這一點,我所有的Jasmine 測試將會被傳遞,並且我有信心我的模型方法全部都會像預期般工作。因為我一直遵循TDD,我的檢視層已經可以相當容易地編寫並會極度地輕薄。當你剛開始實踐TDD的時候,你肯定會慢下來;不過一旦你深入其中,你的生產力和程式碼質量都會大大提升。 我希望這些技巧和模式會對你有幫助!如果你對其他的模式有什麼建議或者你發現了一個錯誤或者你認為其中的一個模式並不是最好的方法,請在下面評論或到推特聯絡我。 感謝Patrick LewisAddy OsmaniDerick Bailey 和Ian Storm Taylor 為這篇文章做的審查。 譯者手語:整個翻譯依照原文線路進行,並在翻譯過程略加了個人對技術的理解。如果翻譯有不對之處,還煩請同行朋友指點。謝謝!

相關文章