這篇文章主要介紹了Backbone.js的一些使用技巧,Backbone.js是一款人氣JavaScript庫,需要的朋友可以參考下
自從3年前Backbone.js發布第一版以來,Backbone.js就成為一個流行的開源JavaScript “MV*”框架,並獲得人們的青睐。盡管Backbone.js給JavaScript應用提供了框架,但是它仍然給開發者留有很多設計模式供選擇,不管怎樣,當開發者第一次使用Backbone.js時還會產生很多普遍的問題的。
因此,在這篇文章中,我們將介紹很多不同的設計模式供你在Backbone.js應用中使用,而且我們也會一同來看看對於開發者來說會產生很多普遍的有關性能伸縮的問題。
對象深度拷貝
JavaScript對待所有原生類型變量是傳值。所以,當變量被引用時就傳遞了變量的值。
?
1 2 var helloWorld = “Hello World”; var helloWorldCopy = helloWorld;舉個例子,上面的代碼將變量helloWorldCopy的值設置為變量helloWorld的值。這樣, 自從它的值被復制之後,所有修改helloWorldCopy的值不會修改helloWorld的值。JavaScript對待所有非原始類型的變量時傳引用,這就意味著當變量傳遞的時候將會傳遞內存地址引用。
?
1 2 3 4 var helloWorld = { ‘hello': ‘world' } var helloWorldCopy = helloWorld;舉個例子,上面的代碼將設置helloWorldCopy為helloWorld的引用,而且,也許你會猜到任何修改helooWorldCopy的值都會直接導致helloWorld值的變化。如果你想要helloWorld的拷貝,你可以創建一個拷貝對象即可。
也許你會想到“為什麼Backbone.js可以解釋為所有的工作都是通過傳遞引用?”事實上,Backbone.js不會拷貝對象,這將意味著如果你從模型裡調用.get()方法獲得一個對象,任何給這個對象的修改都會直接修改原來的對象。讓我們一起來看一個例子來闡明哪裡會發生這樣的情況。如果你有個如下的Person模型:
?
1 2 3 4 5 6 7 8 9 10 11 var Person = Backbone.Model.extend({ defaults: { 'name': 'John Doe', 'address': { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 78701 } } });這樣你就創建了一個新的person對象:
?
1 2 3 var person = new Person({ 'name': 'Phillip W' });現在我們來對新對象的一些屬性進行操作操作:
person.set('name', 'Phillip W.', { validate: true });
上面的代碼成功的給person對象的name屬性賦了值。現在我們在來操作person對象的地址屬性。當然,在我們這樣做之前先驗證一下地址屬性。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var Person = Backbone.Model.extend({ validate: function(attributes) { if(isNaN(attributes.address.zipCode)) return "Address ZIP code must be a number!"; }, defaults: { 'name': 'John Doe', 'address': { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 78701 } } });現在,讓我們試圖給地址屬性設置一個不正確的ZIP代碼。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 var address = person.get('address'); address.zipCode = 'Hello World'; // Raises an error since the ZIP code is invalid person.set('address', address, { validate: true }); console.log(person.get('address')); /* Prints an object with these properties. { 'street': '1st Street' 'city': 'Austin', 'state': 'TX' 'zipCode': 'Hello World' } */這將會怎樣呢?我們的驗證出現了錯誤!為什麼屬性依舊被改變了?前邊我們說過,Backbone.js不會拷貝模型屬性;它會返回你所請求的一切。這樣,你也許會猜到,如果你需要一個對象,你將得到這個對象的引用,對這個對象的任何操作都會直接改變模型裡的對象。如果你要debug,這可能將把你帶入到無底的兔子黑洞。
這個問題對於新的Backbone.js使用要引起注意,甚至對於老練的JavaScript程序員有時也會沒有提防。這個問題在GitHub的Backbone.js討論組中有很激烈的討論。正如Jeremy Ashkenas指出,執行一個深的對象引用是個很難解決的難題,一個很深的對象引用是要花費很大代價的。
幸運的,jQuery 提供了一個深度拷貝功能來實現,$.extend. 如同, Underscore.js ,一個Backbone.js的依靠,提供_.extend 方法,但是我必須避免使用它,因為它沒有執行一份是個深度的復制,Lo-Dash, Underscore.js的一個分叉版本,提供了對象一個深度克隆的_.clone 方法的選項。然而,我使用 $.extend 方法的模型使用的語法規則去執行一個任意對象的深度克隆。記得通過後,結果它執行的是一個深度的克隆方法
?
1 var address = $.extend(true, {}, person.address);我們現在快速准確的復制一個theaddressobject?,並且我們能夠更改它對於我們要點沒有包括在內的我們不用擔心會更改它原有的模型。你必須要意思到這個父工廠對於上面所有的事例因為它所有的地址對象成員都是不可變的(numbers, strings, etc.),與此同時這上面所有的事例工廠當你要深度復制對象裡面包含的對象時你都必須小心的使用。你必須也要知道一個小小的性能影響都來自於執行一個深度的克隆,但是我從來沒有看到過很明顯的問題。然而,如果你要深度的克隆一個大對象或者成千上萬的對象所有的立即復制,你將有可能做大量的性能分析。這將領導我們直接到下一個模式。
為對象創建外觀
在現實世界中,需求經常變化,JavaScript對象符號(或者說JSON)也是一樣,這些對象是由模型和集合所在的端點返回的。這或許會成為你的基礎代碼中的一個真正的大麻煩,如果你的視圖與底層的數據模型是緊耦合的話。因此,我為所有對象創建了getters和setters。
支持這個模式的人非常多。如果任何底層的數據結構改變了,那麼視圖層並不需要更新許多;你將有一個數據的訪問點,所以你不太可能忘記做一個深度拷貝,你的代碼將會更易於維護更易於調試。負面因素在於這個模式可能導致模型或集合的一點點膨脹。
我們看一個例子來闡明這個模式。想像我們有一個Hotel模型,包含有rooms和目前可獲得的rooms,而且我們希望可以通過床位大小來獲得rooms。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 var Hotel = Backbone.Model.extend({ defaults: { "availableRooms": ["a"], "rooms": { "a": { "size": 1200, "bed": "queen" }, "b": { "size": 900, "bed": "twin" }, "c": { "size": 1100, "bed": "twin" } }, getRooms: function() { $.extend(true, {}, this.get("rooms")); }, getRoomsByBed: function(bed) { return _.where(this.getRooms(), { "bed": bed }); } } });現在我們假設明天你就要發布你的代碼,而你又發現端點開發者忘記告訴你rooms的數據結構改變了,由一個對象變為一個數組。你的代碼現在看起來會像下面這樣。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 var Hotel = Backbone.Model.extend({ defaults: { "availableRooms": ["a"], "rooms": [ { "name": "a", "size": 1200, "bed": "queen" }, { "name": "b", "size": 900, "bed": "twin" }, { "name": "c", "size": 1100, "bed": "twin" } ], getRooms: function() { var rooms = $.extend(true, {}, this.get("rooms")), newRooms = {}; // transform rooms from an array back into an object _.each(rooms, function(room) { newRooms[room.name] = { "size": room.size, "bed": room.bed } }); }, getRoomsByBed: function(bed) { return _.where(this.getRooms(), { "bed": bed }); } } });我們僅僅更新了一個函數,以便將Hotel的結構轉變為這個應用的其余部分所期望的結構,同時整個應用仍然像我們所期待的一樣運作。如果這裡沒有一個getter,我們很可能不得不為rooms更新每個訪問點。理想情況下,你會希望更新所有的函數,以適應新的數據結構,但如果你在時間方面有壓力急於發布的話,這個模式將可以拯救你。
離題說一句,這個模式既可以被認為是裝飾模式,因為它隱藏了創建對象拷貝的復雜性,也可以認為是橋接模式,因為它可以用來將數據轉換為所期望的形式。一個好的經驗是對任何對象元素使用getters 和setters 。
存儲數據不是通過服務器保存
盡管Backbone.js有模型和集合映射的規定去具象狀態的傳輸(or REST-ful)的端點,你將花大量的時間去找你想要的存儲數據在你的模型或者不是在服務器上的連接。另外一些關於Backbone.js的文章,例如“Backbone.js Tips: Lessons From the Trenches” 是通過SupportBee的Prateek Dayal ,這個模式還有其他的描述。讓我們一起來快速的看一個小例子來幫助我們說明它可能會派上用場。假設你有一個集合。
?
1 2 3 4 5 6 <ul> <li><a href="#" data-id="1">One</a></li> <li><a href="#" data-id="2">Two</a></li> . . . <li><a href="#" data-id="n">n</a></li> </ul>當使用者點擊其中一個項目時,這個項目成為了被選中狀態並且對於使用者作為選中項目是通過 aselectedclass 添加的是可視化的。以下這是一種方式:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 var Model = Backbone.Model.extend({ defaults: { items: [ { "name": "One", "id": 1 }, { "name": "Two", "id": 2 }, { "name": "Three", "id": 3 } ] } }); var View = Backbone.View.extend({ template: _.template($('#list-template').html()), events: { "#items li a": "setSelectedItem" }, render: function() { $(this.el).html(this.template(this.model.toJSON())); }, setSelectedItem: function(event) { var selectedItem = $(event.currentTarget); // Set all of the items to not have the selected class $('#items li a').removeClass('selected'); selectedItem.addClass('selected'); return false; } }); <script id="list-template" type="template"> <ul id="items"> <% for(i = items.length - 1; i >= 0; i--) { %> <li> <a href="#" data-id="<%= item[i].id %>"><%= item[i].name %></a></li> <% } %></ul> </script>現在我們能夠很容易的判斷被選中的項目,並且我們沒有必要通過對象模型去判斷。這種模式對於存儲無用的數據是非常有用的以至於 你可能非常想要去跟蹤;請記住你能夠創建一個模型並且沒有必要去關聯於他們存儲的一些無用的圖像數據。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var View = Backbone.View.extend({ initialize: function(options) { // Re-render when the model changes this.model.on('change:items', this.render, this); }, template: _.template($('#list-template').html()), events: { "#items li a": "setSelectedItem" }, render: function() { $(this.el).html(this.template(this.model.toJSON())); }, setSelectedItem: function(event) { var selectedItem = $(event.currentTarget); // Set all of the items to not have the selected class $('#items li a').removeClass('selected'); selectedItem.addClass('selected'); // Store a reference to what item was selected this.selectedItemId = selectedItem.data('id')); return false; } });現在我們可以很容易的確定哪些項已經被選中,並且我們沒有必要通過這些對象模型來了解。這個模式對於存儲無用的數據是非常有用的,請記住,您可以創建不一定有端點相關聯的存儲無關的視圖數據的模型和集合。
這種模式的缺點是你存儲了無用的數據在你的模型或者集合中,它們不能真正意義上的追隨一個平靜的架構是因為它們不會完美的去映射在web資源上;另外,這個模式會引起一些很膨脹的在你的模型中;;並且當你保存你的模型的時候如果你的端點嚴格的只接受JSON數據它會引起一個很大的煩惱。
你可能會問你自己,“我如何確定我是否應該講把額外的數據放進視圖或者是模型中?”。如果額外的屬性你將要增加的是圍繞性的呈現,例如一個容器的高度,我們應該要添加它的圖形。如果這個屬性跟底層的數據模型有一些關系,然後你想要將它放進這個模型中。例如,如果上面的例子更多的顯露出,因為某些原因我僅僅只希望用戶通過從模型返回的項目列表中選擇一個特殊的項,我可能會增加這種邏輯模型。總而言之,大多數的事情,它實際上取決於這種依賴。你能夠為保持你的模型而辯論並且你可以認為保持你的觀點是可能的並且把盡可能多的邏輯放進你的模型中。
渲染部分視圖,而不是整個視圖
當你第一次開始開發Backbone.js應用時,典型的視圖結構是像這樣的:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 var View = Backbone.View.extend({ initialize: function(options) { this.model.on('change', this.render, this); }, template: _.template($(‘#template').html()), render: function() { this.$el.html(template(this.model.toJSON()); $(‘#a', this.$el).html(this.model.get(‘a')); $(‘#b', this.$el).html(this.model.get(‘b')); } });在這裡,任何對模型的改變都會觸發對視圖的一個全面的重新渲染。我第一次用Backbone.js開發時,我是這個模式的實踐者。但隨著視圖代碼的增長,我迅速的意識到,這種方法不利於維護或優化,因為當模型的任何一個屬性發生變化時,視圖將會完全的重新渲染。
當我遇到這個問題,我迅速的用Google搜索了一下,看看別人是怎麼做的,結果找到了Ian Storm Taylor的博客,“分解你的Backbone.js渲染方法”,他在其中描述了在模型中監聽單獨的屬性變化,然後僅僅重新渲染相對於變化屬性的視圖部分。Taylor也描述了返回對象的引用,以便單獨的渲染函數可以很容易的鏈接在一起。上面的例子現在現在就變得更易於維護,性能更優。因為我們僅僅更新了模型變化的屬性相對應的視圖部分。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var View = Backbone.View.extend({ initialize: function(options) { this.model.on('change:a', this.renderA, this); this.model.on('change:b', this.renderB, this); }, renderA: function() { $(‘#a', this.$el).html(this.model.get(‘a')); return this; }, renderB: function() { $(‘#b', this.$el).html(this.model.get(‘b')); return this; }, render: function() { this .renderA() .renderB(); } });我應該說一下有許多插件,比如Backbone.StickIt和Backbone.ModelBinder,提供了模型屬性與視圖元素的鍵-值綁定,這會讓你省去編寫許多樣板代碼,如果你具有復雜的表單字段檢驗一下它們。
保持模型與視圖無關
正如 Jeremy Ashkenas 在 Backbone.js的 GitHub問題 之一中所指出的,Backbone.js 並沒有實施數據與視圖層之間關注點的任何真正分離,除非模型未引用視圖而創建。因為Backbone.js並沒有執行一個關注點分離,所以你應該將其分離嗎?我和許多其他的Backbone.js開發人員,如Oz Katz 和 Dayal ,都相信答案毫無疑問是yes:模型與集合,也就是數據層,應該徹底的與綁定到它們的視圖無關,保持一個清晰的關注點分離。如果你沒有遵循關注點分離,你的基礎代碼將很快變成意大利面條式的代碼,而沒有人喜歡意大利面條式的代碼。
保持模型與視圖無關將會幫助你預防意大利面條式的代碼,而沒有人喜歡意大利面條式的代碼!
保持你的數據層徹底的與視圖層無關,這將會使你創建出更具模塊化,可復用與可維護的基礎代碼。你可以非常容易的在應用程序各個地方復用與擴展模型和集合,而不需要考慮它們所綁定的視圖。遵循這個模式使對你項目不熟悉的開發者能迅速的深入到基礎代碼之中,因為他們會確切的知道哪裡發生了渲染,哪裡存在有你的應用的所有商務邏輯。
這個模式也執行了單一職責原則,規定了每個類應該具有一個單一的職責,而且它的職責應該封裝與這個類之中,因為模型與集合要處理數據,而視圖要處理渲染。
路由中的參數
最好的演示這個模式工作方式是舉個例子。比如說你需要對搜索頁面進行排序,每個搜索頁面都允許用戶添加兩個不同的過濾類型foo和bar,每個類型代表不同的觀點。
因此,你的URL結構將會呈現如下:
?
1 2 3 'search/:foo' 'search/:bar' 'search/:foo/:bar'現在,所有的路由都用的是同一個試圖和模型,這樣大多數人喜歡用同一個函數search()來實現。然而,你要是檢查過Backbone.js代碼的話,你會發祥它裡面沒有排序的參數映射;這些參數只是從左至右依次傳入函數。這樣,為了都能統一使用一個函數,你就要停止創建不同的函數正確的來為search()匹配參數。
?
1 routes: { 'search/:foo': 'searchFoo', 'search/:bar': 'searchBar', 'search/:foo/:bar': 'search' }, search: function(foo, bar) { }, // I know this function will actually still map correctly, but for explanatory purposes, it's left in. searchFoo: function(foo) { this.search(foo, undefined); }, searchBar: function(bar) { this.search(undefined, bar); },你也許能想象的到,這個模式可以使路由功能很快膨脹。當我第一次遇到這個問題時,我試圖創建了一些用正則表達式定義的解析函數來“神奇”的去匹配參數,當然這個是可以工作的-但這也是有約束條件的。這樣,我廢棄了這個想法(有時,我仍然可以用Backbone插件來解決)。我進入GitHub中的一個 議題,其中Ashkenas建議應該讓所有的參數都和search函數匹配。
上面的代碼現在轉變為下面維護性更強的樣子:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 routes: { 'search/:foo': 'searchFoo', 'search/:bar': 'searchBar', 'search/:foo/:bar': 'search' }, search: function(foo, bar) { }, // I know this function will actually still map correctly, but for explanatory purposes, it's left in. searchFoo: function(foo) { this.search(foo, undefined); }, searchBar: function(bar) { this.search(undefined, bar); },這種模式可以戲劇性的減少路由的過分膨脹。然而,需要注意到它不會服務於不能區別的參數。比如,如果你有兩個作為ID的參數,如模式XXXX-XXXX,你不能區分哪個ID是對哪個參數的回應。
model.fetch() 不會清除你的模型
這通常會將那些Backbone.js的新手給絆倒:model.fetch()並不能丟掉你的模型,而是擴展了你的模型的屬性。因此,如果你的模型具有屬性x,y和z,你獲取到y和z,那麼x將仍然是模型中的那個x,只有y和z會被更新。下面的例子將這個概念形象化了。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var Model = Backbone.Model.extend({ defaults: { x: 1, y: 1, z: 1 } }); var model = new Model(); /* model.attributes yields { x: 1, y: 1, z: 1 } */ model.fetch(); /* let's assume that the endpoint returns this { y: 2, z: 2, } */ /* model.attributes now yields { x: 1, y: 2, z: 2 } */PUTs 需要一個 ID 屬性
這一條也經常將Backbone.js的新手絆倒。要想在調用.save()的時候讓模型發送一個HTTP PUT請求,你的模型需要有一個ID屬性集。記得HTTP PUT謂詞是設計來做更新的吧,所以發送一個PUT請求,你的模型需要有一個ID,這麼做是有意義的。在理想的世界裡,你的所有模型都具有一個名為ID的完美的ID屬性,但是你從端點接收到的JSON數據可能並不總是具有完美命名的IDs。
因此,如果你需要更新一個模型,請在保存之前確認模型上有ID。Backbone.js 的0.5以及更高版本允許你用id屬性來更新模型的ID屬性名稱,如果你的端點返回的不是名為id的IDs的話。
如果困頓於使用的是版本低於0.5的Backbone.js,我建議你修改你的模型或集合的parse函數,以便將你期望的ID屬性映射到屬性ID。這裡有一個快速上手的例子,說明了你應怎樣修改parse函數來做到這一點。我們假設你有一個cars的集合,它的IDs是carID。
?
1 2 3 4 5 6 7 8 9 parse: function(response) { _.each(response.cars, function(car, i) { // map the returned ID of carID to the correct attribute ID response.cars[i].id = response.cars[i].carID; }); return response; },頁面加載時創建模型數據
有時你會發現你的模型或者集合需要在頁面加載時被初始化賦值。許多關於Backbone.js模式的文章,例如Rico Sta Cruz的 “Backbone 模式” 和 Katz的 “ 避免常見的Backbone.js陷阱” ,討論了這種模式。這種模式實現很容易,只需在頁面中內聯一段腳本,通過你選擇的服務端語言,將單個模型屬性或者JSON形式的數據呈現出來。例如,在Rails語言中,我采用下面方法之一:
?
1 2 3 4 5 6 // a single attribute var model = new Model({ hello: <%= @world %> }); // or to have json var model = new Model(<%= @hello_world.to_json %>);應用這種模式可以通過“立即的”渲染頁面,改善你的搜索引擎排名,而且它也可以通過限制應用初始化HTTP請求的方式,大大縮短你的應用啟動與運行所需要的時間。
處理失敗的模型屬性驗證
很多時候,你會想知道是哪個模型屬性驗證失敗了。例如,如果你有一個極其復雜的表單,你或許想知道哪個模型屬性驗證失敗,這樣你就可以將這個屬性對應的輸入字段高亮顯示。不幸的是,提醒視圖到底是哪個模型屬性驗證失敗並沒有直接集成於Backbone.js,但是你可以用一些不同的模式去處理這個問題。
返回一個錯誤對象
一個給視圖提醒哪個模型屬性驗證失敗的模式是,返回一個對象,其中包含某種標志,它詳細的記錄了哪個屬性驗證為失敗,就像下面這樣:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // Inside your model validate: function(attrs) { var errors = []; if(attrs.a < 0) { errors.push({ 'message': 'Form field a is messed up!', 'class': 'a' }); } if(attrs.b < 0) { errors.push({ 'message': 'Form field b is messed up!', 'class': 'b' }); } if(errors.length) { return errors; } } // Inside your view this.model.on('invalid', function(model, errors) { _.each(errors, function(error, i) { $(‘.' + error.class).addClass('error'); alert(error.message); }); });這個模式的優點在於,你是在一個地方處理所有不合法的消息。缺點在於,如果你以不同的方式處理不合法的屬性的話,你的invalid方法可能會成為一個很大的switch或者if語句。
廣播自定義Error事件
我的一個朋友,Derick Bailey,推薦了一個可替代模式,就是為每個模型屬性觸發自定義的errors事件。這將允許你的視圖能夠針對單獨的屬性綁定到特定的error事件:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // Inside your model validate: function(attrs) { if(attrs.a < 0) { this.trigger(‘invalid:a', 'Form field a is messed up!', this); } if(attrs.b < 0) { this.trigger(‘invalid:b', 'Form field b is messed up!', this); } } // Inside your view this.model.on('invalid:a', function(error) { $(‘a').addClass('error'); alert(error); }); this.model.on('invalid:b', function(error) { $(‘b').addClass('error'); alert(error); });這個模式的優點在於,你的視圖明確的綁定到它們所綁定到的error類型,而且如果你對每一種屬性error有特定的指令的話,它可以清理你的視圖部分代碼,使之更易於維護。這個模式的一個不好的地方在於,如果在你處理不同的屬性error時並沒有太多的不同的話,你的視圖可能會變得極為膨脹。
這兩種模式都有其利弊,你應該考慮清楚哪個模式對你的應用案例是最優的。如果你按照同樣的方式處理所有失敗的驗證,那麼第一個方法可能是最好的;如果你對每個模型屬性有特定的UI變化,那麼後一種方法更好。
HTTP狀態代碼200所觸發的錯誤
如果你的浏覽器端模型或者集合收到了無效的JSON,盡管HTTP的狀態代碼是200,但浏覽器端依然會觸發一個“錯誤”事件。這種事件常發生於本地模擬JSON數據造成的。那麼,一個好的方法就是讀取經過 JSON 驗證器驗證了的模擬JSON數據文件。或者從你的IDE獲得相應的 插件來及時獲取格式錯誤的JSON信息。
創建一個一般性錯誤顯示模式
創建一個常見錯誤顯示代碼可以節省你的時間以及創建一個統一的模式來處理、可視化錯誤信息,而且它可以增加開發者的經驗。我之前開發的每一個Backbone.js應用中我都會創建一個可以處理alert的視圖:
?
1 2 3 4 5 6 7 8 9 10 11 12 var AlertView = Backbone.View.extend({ set: function(typeOfError, message) { var alert = $(‘.in-page-alert').length ? $(‘.in-page-alert'): $(‘.body-alert'); alert .removeClass(‘error success warning') .addClass(typeOfError) .html(message) .fadeIn() .delay(5000) .fadeOut(); } });上面的代碼首先會檢查是否已在視圖代碼中創建了指定視圖in-page-alert div。如果沒有,則接著查看一般性的在其它地方聲明的body-alert div。這樣可以讓你發送具有一致性的錯誤信息以及當你忘記指定一個in-page-alert div時提供有用且可靠的信息。如下面的模式簡化了讓你怎樣在你的試圖中處理錯誤信息:
?
1 2 3 4 5 var alert = new AlertView(); this.model.on('error', function(model, error) { alert.set('TYPE-OF-ERROR', error); });單頁面應用中更新浏覽器頁面標題
這是一個比任何東西都重要的可用性問題。如果你正在開發一個單頁面應用程序,謹記更新每個頁面的標題。我寫過一個的插件(Backbone.js Router Title Helper)來擴展 backbone.js router 的功能。它通過一個 Map 對象來控制路由,鍵來代表路由函數的名字,值則映射到頁面的標題。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Backbone.Router = Backbone.Router.extend({ initialize: function(options){ var that = this; this.on('route', function(router, route, params) { if(that.titles) { if(that.titles[router]) document.title = that.titles[router]; else if(that.titles.default) document.title = that.titles.default; else throw 'Backbone.js Router Title Helper: No title found for route:' + router + ' and no default route specified.'; } }); } });單頁面應用中的緩存對象
當我們談論單頁面應用時,另一個叫緩存對象模式你將會經常用到!下面的例子直截了當而且簡單:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter: parameter }); this.cached.view = this.cached.view || new View({ model: this.cached.model }); }這個模式可以加速你得應用,因為你不用重復初始化你得Backbone.js對象。然而,它會過多的消耗內存;所以,緩存對象就要在整個應用中使用。如果以前你用過Backbone.js開發過應用,也許你會問你自己,“ 我要重取數據該怎麼做?”你可以每次在如下路徑中觸發後重取數據:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter: parameter }); this.cached.view = this.cached.view || new View({ model: this.cached.model }); this.cached.model.fetch(); }當你的應用從端點(如,一個收件箱)必須檢索最新數據時上面的模式就可以工作。當然,如果你要拿的數據時憑借應用的某個狀態(假設這個狀態是通過URL和參數來決定的),甚至是在用戶上一個頁面應用的狀態沒有改變, 你可以重取數據。一個好的解決方案去重拿數據時當應用(參數)發生變化時:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // Inside a router initialize: function() { this.cached = { view: undefined, model: undefined } }, index: function(parameter) { this.cached.model = this.cached.model || new Model({ parameter:parameter }); this.cached.model.set('parameter', parameter); this.cached.view = this.cached.view || new View({ model: this.cached.model }); } // Inside of the model initialize: function() { this.on("change:parameter", this.fetchData, this); }JSDoc函數和Backbone.js類
我是文檔注釋和JSDoc的超級粉絲。我用JSDoc對所有的Backbone類添加了文檔注釋:
?
1 2 3 4 5 6 7 8 9 10 11 12 var Thing = Backbone.View.extend(/** @lends Thing.prototype */{ /** @class Thing * @author Phillip Whisenhunt * @augments Backbone.View * @contructs Thing object */ initialize() {}, /** Gets data by ID from the thing. If the thing doesn't have data based on the ID, an empty string is returned. * @param {String} id The id of get data for. * @return {String} The data. */ getDataById: function(id) {} });如果你對Backbone類進行如上添加文檔注釋,這樣你可以給所有類和函數 添加參數、返回類型以及描述文檔注釋了。確保保持初始化函數作為一個聲明的函數,這樣可以幫助我們生成JSDoc。如果你想看看JSDoc的例子工程,那就在 HomeAway Calendar Widget下載例子。同時這裡也有個 Grunt.js插件, grunt-jsdoc-plugin,這個也可以作為你構建文檔注釋時的一部分。
聯系測試驅動的開發模式
我認為如果你用Backbone.js,你應該在開發模型和集合時遵循測試驅動開發(TDD)。我第一次用Jasmine.js創建模型和集合時遵循TDD進行單元測試,但失敗了。一旦寫下單元測試並且失敗,我會對整個模型和集合進行重寫。
通過這一點,我的所有Jasmine測試都通過了,而且我有信心我的模型和集合會和我期望的一樣工作。自從我遵循TDD,我的視圖層非常容易寫而且非常簡單。當你開始用TDD時,你得速度當然會很慢;但是一但你得腦海裡一直想著TDD,你的編程效率和質量會神奇般的提高。