Model是Backbone中所有數據模型的基類,用於封裝原始數據,並提供對數據進行操作的方法,我們一般通過繼承的方式來擴展和使用它。
如果你做過數據庫開發,可能對ORM(對象關系映射)不會陌生,而Backbone中的Model就像是映射出來的一個數據對象,它可以對應到數據庫中的某一條記錄,並通過操作對象,將數據自動同步到服務器數據庫。(下一節即將介紹的Collection就像映射出的一個數據集合,它可以對應到數據庫中的某一張或多張關聯表)。
1.1 創建數據模型
我們先通過一段代碼來看看如何創建數據模型
// 定義Book模型類 var Book = Backbone.Model.extend({ defaults: { name: 'unknown', author: 'unknown', price: 0 } }); // 實例化模型對象 var javabook = new Book({ name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 });
我們通過Model.extend方法,定義一個自己的模型類Book。
Backbone模塊類(包括子類)都包含一個extend靜態方法用於實現繼承。給extend方法傳遞的第一個參數應該是一個對象,對象中的屬性和方法將被添加到子類,我們可以通過extend方法擴展子類或重載父類的方法。
從Backbone模塊類繼承的子類,都包含一個__super__靜態屬性,這是一個指向父類原型對象的引用,例如:
var Book = Backbone.Model.extend({ constructor: function() { Book.__super__.constructor.call(this) } });
在這個例子中,我們重載了Model類的構造函數,但我們希望在子類被實例化時,調用父類的構造函數,因此我們可以通過引用Book.__super__.constructor來調用它。
實際上我們一般並不會重載模塊類的constructor方法,因為在Backbone中所有的模塊類都提供了一個initialize方法,用於避免在子類中重載模塊類的構造函數,當模塊類的構造函數執行完成后會自動調用initialize方法。
回到本節的第一個例子,我們在定義Book類的時候,傳遞了一個defaults參數,它用於定義模型數據的默認狀態,雖然我們在創建Book實例后再添加它們,但為每個數據模型定義屬性列表和默認值,是一個好的編碼習慣。
最后,我們通過new關鍵字,創建了一個Book的實例,並向它的構造函數中傳遞了一系列初始化數據,它們將覆蓋defaults中定義的默認狀態。
2、初始化和讀取數據
在我們定義好一個模型類之后,可以通過new關鍵字實例化該模型的對象。
如果模型類在定義時設置了defaults默認數據,這些數據將被復制到每一個實例化的對象中,如:
// 定義Book模型類 var Book = Backbone.Model.extend({ defaults: { name: 'unknown', author: 'unknown', price: 0 } }); // 實例化模型對象 var javabook = new Book();
上面的代碼創建了一個Book實例javabook,它包含了模型類在定義時的默認數據。
我們將實例化的代碼稍作修改:
// 實例化模型對象 var javabook = new Book({ name: 'Thinking in Java' }); // 通過get和escape()方法獲取模型中的數據 var name = javabook.get('name'); var author = javabook.escape('author'); var price = javabook.get('price'); // 在控制台輸出模型中的數據name console.log(name); // 輸出Thinking in Java console.log(author); // 輸出unknown console.log(price); // 輸出0
我們在實例化對象時傳遞了初始數據,它將覆蓋Book類定義時defaults中的默認數據,這一點很容易理解。
上面的例子中我們通過get()和escape()方法獲取模型中的數據,它們的區別在於:
get()方法用於直接返回數據
escape()方法先將數據中包含的HTML字符轉換為實體形式(例如它會將雙引號轉換為"形式)再返回,用於避免XSS攻擊。
模型將原始數據存放在對象的attributes屬性中,因此我們也可以通過javabook.attributes屬性直接讀取和操作這些數據,例如:
// 在控制台直接輸出對象的attributes屬性 console.dir(javabook.attributes); // 控制台輸出結果 // { // author: 'unknown', // name: 'Thinking in Java', // price: 0 // }
但通常並不會這樣做,因為模型中數據狀態的變化會觸發一系列事件、同步等動作,直接操作attributes中的數據可能導致對象狀態異常。更安全的做法是:通過get()或escape()方法讀取數據,通過set()等方法操作數據。
3、修改數據
我們通常可以調用模型對象的set()方法,來修改模型中的數據,例如:
// 實例化模型對象 var javabook = new Book(); // 通過set方法設置模型數據 javabook.set('name', 'Java7入門經典'); javabook.set('author', 'Ivor Horton'); javabook.set('price', 88.50); // 獲取數據並將數據輸出到控制台 var name = javabook.get('name'); var author = javabook.get('author'); var price = javabook.get('price'); console.log(name); // 輸出Java7入門經典 console.log(author); // 輸出Ivor Horton console.log(price); // 輸出88.50 //set()方法也允許同時設置多個屬性,例如: javabook.set({ name: 'Java7入門經典', author: 'Ivor Horton', price: 88.50 });
當調用set()方法修改模型中的數據時,會觸發一系列事件,我們常常通過監聽這些事件,來動態調整界面中數據的顯示,我們先來看一個例子:
// 定義Book模型類 var Book = Backbone.Model.extend({ defaults: { name: 'unknown', author: 'unknown', price: 0 } }); // 實例化模型對象 var javabook = new Book(); // 監聽模型"change"事件 javabook.on('change', function(model) { console.log('change事件被觸發'); }); // 監聽模型"change:name"事件 javabook.on('change:name', function(model, value) { console.log('change:name事件被觸發'); }); // 監聽模型"change:author"事件 javabook.on('change:author', function(model, value) { console.log('change:author事件被觸發'); }); // 通過set()方法設置數據 javabook.set({ name: 'Thinking in Java', author: 'unknown', price: 395.70 }); // 控制台輸出結果: // change:name事件被觸發 // change事件被觸發
在本例中,我們監聽了模型對象的change事件,該事件在模型對象的任何數據發生改變時被觸發,change事件觸發時,會將當前模型作為參數傳遞給監聽函數。
我們還監聽了change:name和change:author兩個屬性事件,屬性事件是當模型中對應屬性的數據發生改變時被觸發,屬性事件按照“change:屬性名”來命名,因此它並不固定。屬性事件觸發時,會將當前模型和最新的數據作為參數傳遞給監聽函數。
本例執行后在控制台的輸出結果為:
“change:name事件被觸發
change事件被觸發”
從結果中看,並沒有觸發我們監聽的change:author事件,因為在調用set()方法時,它會在內部檢查新的數據比較上一次是否發生變化,只有發生變化的數據才會被設置和觸發監聽事件。
另一個細節是,我們先監聽了change事件,然后監聽了屬性事件,但事件在觸發時,總是會先觸發屬性事件,然后再觸發change事件。
Backbone允許我們在修改模型數據時獲取上一個狀態的數據,這常常用於數據比較和數據回滾。
例如在下面的例子中,我們希望當price價格被改變時,提示用戶價格的變化情況:
// 定義Book模型類 var Book = Backbone.Model.extend({ defaults: { name: 'unknown', author: 'unknown', price: 0 } }); // 實例化模型對象 var javabook = new Book(); // 監聽"change:price"事件 javabook.on('change:price', function(model, value) { var price = model.get('price'); if (price < value) { console.log('價格上漲了' + (value - price) + '元.'); } else if (price > value) { console.log('價格下降了' + (value - price) + '元.'); } else { console.log('價格沒有發生變化.'); } }); // 設置新的價格 javabook.set('price', 50); // 控制台輸出結果: // 價格沒有發生變化.
我們通過監聽change:price事件來監聽價格的變化,並希望將最新的價格和當前(上一次)的價格作比較,但控制台的輸出結果卻是“價格沒有發生變化.”。這是因為當change事件或屬性事件被觸發時,模型中的數據已經被修改,因此通過get()方法獲取到的是模型中最新的數據。
這時,我們可以通過previous()和previousAttributes()方法來獲取數據被修改之前的狀態。
我們將代碼稍作修改,只需要修改監聽事件的函數即可
// 監聽"change:price"事件 javabook.on('change:price', function(model, value) { var price = model.previous('price'); if (price < value) { console.log('價格上漲了' + (value - price) + '元.'); } else if (price > value) { console.log('價格下降了' + (value - price) + '元.'); } else { console.log('價格沒有發生變化.'); } });
我們將get()方法修改為previous()方法,用來獲取價格在修改之前的狀態,此時控制台輸出的結果為:“價格上漲了50元.”
model.get()方法取到的是模型中最新的數據
model.previous()方法接收一個屬性名,並返回該屬性在修改之前的狀態;
previousAttributes()方法返回一個對象,該對象包含上一個狀態的所有數據。
需要注意的是,previous()和previousAttributes()方法只能在數據修改過程中調用(即在模型的change事件和屬性事件中調用),比如下面的例子就是錯誤的調用方法:
// 設置新的價格 javabook.set('price', 50); var prevPrice = javabook.previous('price'); var newPrice = javabook.get('price'); if (prevPrice < newPrice) { console.log('價格上漲了' + (newPrice - prevPrice) + '元.'); } else if (prevPrice > newPrice) { console.log('價格下降了' + (newPrice - prevPrice) + '元.'); } else { console.log('價格沒有發生變化.'); } // 控制台輸出結果: // 價格沒有發生變化.
控制台輸出的結果是“價格沒有發生變化.”,因為在set()方法被調用完畢后,模型的上一個狀態也會被新數據替換。
(有一種特殊情況是當我們使用了silent配置時,上面的代碼可以得到我們想要的結果,關於silent配置將在后面“數據驗證”章節中介紹)
4、數據驗證
Backbone模型提供了一套數據驗證機制,確保我們在模型中存儲的數據都是通過驗證的,我們通過下面的例子來說明這套驗證機制:
執行這段代碼,你會在控制台看到這段信息:“書籍價格不應低於1元.”
在定義模型類時,我們可以添加一個validate方法,該方法會在模型中的數據發生改變之前被自動調用(就像我們通過set()方法修改數據時一樣)。
validate方法接收一個參數,表示需要進行驗證的數據集合,如果validate方法沒有任何返回值(即undefined),則表示驗證通過;如果驗證不通過,我們常常會返回一個錯誤字符串或自定義對象。但實際上,當你返回一個false也會被認為驗證通過,因為Backbone內部會將validate的返回值轉換為布爾類型,如果為false則認為驗證通過,反之則認為不通過(雖然這聽起來有些別扭)。
當validate驗證不通過時,會觸發invalid事件,並將模型對象和validate方法的返回值傳遞給invalid事件的監聽函數(就像例子中的那樣)。
// 定義Book模型類 var Book = Backbone.Model.extend({ validate: function(data) { if (data.price < 1) { return '書籍價格不應低於1元.'; } } }); var javabook = new Book(); // 監聽invalid事件,當驗證失敗時觸發 javabook.on('invalid', function(model, invalid) { alert(invalid); }); javabook.save({ price: 0 });
上面的例子中,我們監聽了javabook對象的invalid事件,用於在驗證不通過時提示用戶。但在某個場景下,我希望以另一種方式提示用戶,我可以在invalid監聽函數中判斷是否處於這種場景下,然后作出不同的提示,但這顯然不是最好的辦法。
因此,Backbone提供了另一種方式對invalid事件進行覆蓋,來看看這個例子:
// 定義Book模型類 var Book = Backbone.Model.extend({ validate: function(data) { if (data.price < 1) { return '書籍價格不應低於1元.'; } return false; } }); var javabook = new Book({ price: 50 }); // 監聽invalid事件,當驗證失敗時觸發 javabook.on('invalid', function(model, invalid) { console.log(invalid); }); // 在調用set()方法時,傳遞了一個配置對象,包含自定義的invalid處理方法 javabook.save('price', 0, { invalid: function(model, invalid) { console.log('自定義錯誤:' + invalid); } });
在這段代碼中,我們在調用save()方法時,傳遞了第三個參數,它是一個用於描述配置信息的對象,我們設定了一個invalid函數。當validate方法驗證失敗時,會優先調用配置中傳遞的invalid函數,如果沒有傳遞invalid函數,則會觸發invalid事件。
var Chapter = Backbone.Model.extend({ validate: function(attrs, options) { if (attrs.end < attrs.start) { return "can't end before it starts"; } } }); var one = new Chapter({ title: "Chapter One: The Beginning" }); one.set({ start: 15, end: 10 }); if (!one.isValid()) { alert(one.get("title") + " " + one.validationError); }
5、刪除數據
Backbone中刪除模型數據的操作相對簡單,我們常常用unset()和clear()方法來刪除模型中的數據:
unset()方法用於刪除對象中指定的屬性和數據
clear()方法用於刪除模型中所有的屬性和數據
我們來看一個unset()方法的例子:
// 定義Book模型類 var Book = Backbone.Model.extend(); // 實例化模型對象 var javabook = new Book({ name: 'Java7入門經典', author: 'Ivor Horton', price: 88.50 }); // 輸出: Java7入門經典 console.log(javabook.get('name')); // 刪除對象name屬性 javabook.unset('name'); // 輸出: undefined console.log(javabook.get('name')); //當我們對模型的name屬性執行unset()方法后,模型內部會使用delete關鍵字將name屬性從對象中刪除。 //clear()方法與unset()方法執行過程類似,但clear()方法會刪除模型中的所有數據,例如: // 定義Book模型類 var Book = Backbone.Model.extend(); // 實例化模型對象 var javabook = new Book({ name: 'Java7入門經典', author: 'Ivor Horton', price: 88.50 }); // 刪除對象name屬性 javabook.clear(); // 以下均輸出: undefined console.log(javabook.get('name')); console.log(javabook.get('author')); console.log(javabook.get('price'));
在調用unset()和clear()方法清除模型數據時,會觸發change事件,我們也同樣可以在change事件的監聽函數中通過previous()和previousAttributes()方法獲取數據的上一個狀態。
6、將模型數據同步到服務器
Backbone提供了與服務器數據的無縫連接,我們只需要操作本地Model對象,Backbone就會按照規則自動將數據同步到服務器。
如果需要使用Backbone默認的數據同步特性,請確定你的服務器數據接口已經支持了REST架構。在REST架構中,客戶端會通過請求頭中的Request Method告訴服務器我們將要進行的操作(包括create、read、update和delete,它們對應的Request Method分別為POST、GET、PUT和DELETE),而對於沒有良好支持REST發送方式的瀏覽器,Backbone會使用另外一些方法來實現,這在本節中會詳細討論。
在討論數據同步相關方法之前,你需要先了解一些Backbone中與數據同步息息相關的內容:
a、數據標識:
設想一下,如果我們需要通過服務器接口刪除一條數據,僅僅在報文頭中通過Request Method標識告訴服務器進行delete操作是不夠的,更重要的是還需要告訴服務器刪除哪一條數據,這需要我們傳遞給服務器一個數據的唯一標識(例如記錄id)。
Backbone中每一個模型對象都有一個唯一標識,默認名稱為id,你可以通過idAttribute屬性來修改它的名稱。
id應該由服務器端創建並保存在數據庫中,在與服務器的每一次交互中,模型會自動在URL后面加上id,而對於客戶端新建的模型,在保存時不會在URL后加上id標識(我們可以通過模型的isNew()方法來檢查,該模型對象是否是由客戶端新建的)。
a、URL規則:
Backbone默認使用path info的方式來訪問服務器接口。
例如:我們在刪除一個模型數據時,模型會在報文頭的Request Method中聲明delete操作,並在服務器接口后自動加上模型id,格式類似於http://urlRoot/10001,其中urlRoot是我們設置的服務器接口地址,而10001是模型id。請注意它是通過URL路徑的方式自動追加到接口地址后的,因此服務器也必須要支持PATHINFO的解析方式。
使用PATHINFO方式,因為它更直觀,更利於SEO,還可以避免與Backbone中的路由器發生混淆(關於路由器將在后面的章節中介紹)。
如果我們希望讓Backbone自動與服務器接口進行交互,首先應該配置模型的URL,Backbone支持3種方式的URL配置:
第一種是urlRoot方式:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service' }); // 創建實例 var javabook = new Book({ id: 1001, name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 }); // 保存數據 javabook.save();
在這個例子中,我們創建了一個Book模型的實例,並調用save()方法將數據保存到服務器。(可能你對save()方法還不太了解,但這並不重要,因為我們馬上就會講到它,現在你只需知道我們用它將模型中的數據保存到服務器)
你可以抓包查看請求記錄,你能看到請求的接口地址為:http://localhost/service/1001
其中localhost是我的主機名,因為我在本地搭建了一個Web服務器環境。
service是該模型的接口地址,是我們在定義Book類時設置的urlRoot。
1001是模型的唯一標識(id),我們之前說過,模型的id應該是由服務器返回的,對應到數據庫中的某一條記錄,但這里為了能直觀的測試,我們假設已經從服務器端拿到了數據,且它的id為1001。
這段內容很容易理解,接下來,我們將save()方法換成destroy()方法(該方法用於將模型中的數據從服務器刪除):
// 刪除數據 javabook.destroy();
你能看到請求的接口地址仍然為:http://localhost/service/1001。這並不奇怪,如果你細心觀察,會發現兩次請求頭中的Request Method參數分別為PUT和DELETE,服務器接口會根據它來判斷你所做的操作。
如果你的瀏覽器不支持REST發送方式,你可能會看到Request Method始終是POST類型,且在Form Data中會多出一個_method參數,PUT和DELETE操作名被放在了這個_method參數中。這是Backbone為了適配低版本瀏覽器而設計的另一種方法,你的服務器接口也必須同時支持這種方式。
我們再來看第二種URL方式的例子:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service', url: '/javaservice' }); // 創建實例 var javabook = new Book({ id: 1001, name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 }); // 保存數據 javabook.save();
在這個例子中,我們在定義Book類時,新增了參數url,執行這段代碼,你會發現請求的接口地址為http://localhost/javaservice。它沒有再使用urlRoot定義的參數,也沒有將模型的id追加到接口地址中,urlRoot和url參數我們一般只會同時定義一個,它們的區別在於:
urlRoot參數表示服務器接口地址的根目錄,我們無法直接訪問它,只能通過連接模型id來組成一個最終的接口地址
url參數表示服務器的接口地址是已知的,我們無需讓Backbone自動連接模型id(這可能是在url本身已經設置了模型id,或者不需要傳遞模型id)
如果同時設置了urlRoot和url參數,url參數的優先級會高於urlRoot。
(另一個細節是,url參數不一定是固定的字符串,也可以是一個函數,最終使用的接口地址是這個函數的返回值。)
最后一種URL方式的例子:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service', url: '/javaservice' }); // 創建實例 var javabook = new Book({ id: 1001, name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 }); // 保存數據 javabook.save(null, { url: '/myservice' });
在這個例子中,我們在調用save()方法的時候傳遞了一個配置對象,它包含一個url配置項,最終抓包看到的請求地址是http://localhost/myservice。因此你可以得知,通過調用方法時傳遞的url參數優先級會高於模型定義時配置的url和urlRoot參數。
在Backbone中,模型對象提供了3個方法用於和服務器保持數據同步:
save()方法:在服務器創建或修改數據
fetch()方法:從服務器獲取數據
destroy()方法:從服務器移除數據
下面我們將依次介紹這些方法的使用:
save()方法:
save()方法用於將模型的數據保存到服務器,它可能是一條新的數據,也可能是修改服務器現有的某一條數據,這取決於模型中是否存在id(唯一標識)。
首先我們來看一個創建數據的例子:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service' }); // 創建實例 var javabook = new Book(); // 設置初始化數據 javabook.set({ name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 }); // 從將數據保存到服務器 javabook.save();
在這個例子中,我們創建了一個新的Book實例,並設置了一些數據(實際上它們可能是由用戶輸入的),我們通過save()方法將這些數據提交到服務器。
如果你抓包看一下報文頭信息,能看到Request Method參數為POST,這是因為模型內部會通過isNew()方法檢查是否為客戶端新建,如果是客戶端新建的數據,會通過POST方式發送。如果是修改服務器現有的數據,則通過PUT方式發送。
如果服務器接口的報文體中沒有返回任何數據,你會發現保存之后的模型較之前沒有發生任何變化,在你下一次調用save()方法的時候,它仍然會以POST方式通知服務器創建一
條新的數據。這是因為模型對象並沒有獲取到剛剛服務器創建成功的記錄id,因此我們希望服務器接口在將數據保存成功之后,同時將新的id返回給我們,就像這樣:
{ "id": "1001", "name": "Thinking in Java(修訂版)", "author": "Bruce Eckel", "price": "395.70" }
這一段是服務器接口返回的數據,它除了返回新記錄的id,還返回了修改后的name數據(當然,你也可以只返回新記錄的id,我們常常都是這樣做的)。這時我們再來看現在模型中的數據,它多了一個id屬性,並且name屬性的值也發生了變化,也就是說模型使用服務器返回的最新數據替換了之前的數據。
我們將代碼稍作修改:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service' }); // 創建實例 var javabook = new Book(); // 設置初始化數據 javabook.set({ name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 }); // 將數據保存到服務器 javabook.save(null, { success: function(model) { // 數據保存成功之后, 修改price屬性並重新保存 javabook.set({ price: 388.00 }); javabook.save(); } });
我們修改了save()方法的調用參數,像例子中那樣,你可以設置一個success回調函數用來表示保存成功之后將要進行操作(你也可以設置一個error回調函數用來表示保存失敗時將要進行的操作)。
在數據保存成功之后,我們將修改模型的price值,並從新調用save()方法保存數據。
我們抓包看一下請求頭,發生了一些什么變化:
Request Method變成了PUT。
請求的接口地址變成了http://localhost/service/1001(這與我們剛剛討論的URL配置有關,如果不明白可以重新閱讀本節)。
當然,還有提交的數據也變成了我們修改后的。
在調用save()方法時,我們可以傳遞一個配置項對象,上面我們已經使用它傳遞了一個success回調函數。
在配置項中,還可以包含一個wait配置,如果我們傳遞了wait配置為true,那么數據會在被提交到服務器之前進行驗證,當服務器沒有響應新數據(或響應失敗)時,模型中的數據會被還原為修改前的狀態。如果沒有傳遞wait配置,那么無論服務器是否保存成功,模型數據均會被修改為最新的狀態、或服務器返回的數據。
我們還是用一個例子來說明:
// 定義Book模型類 var Book = Backbone.Model.extend({ defaults: { name: 'unknown', author: 'unknown', price: 0 }, urlRoot: '/service' }); // 創建實例 var javabook = new Book(); // 從將數據保存到服務器 javabook.save({ name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 }, { wait: true });
請運行這個例子中的代碼,並且將服務器接口返回的數據設置為空(或404狀態),你能看到在save()方法調用完成之后,模型中的數據被恢復成最初defaults中定義的數據,因為我們在調用save()方法時傳遞了wait配置。(你也可以試着將wait配置去掉,然后再運行它,你會發現雖然服務器接口並沒有返回數據或保存成功,但模型對象中仍然保持着最新的數據)
正如我們最開始所講得那樣,save()方法用於添加一條新的數據到服務器,或修改現有的一條數據。
其實save()方法也可以同時實現數據修改和保存,例如:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service' }); // 創建實例 var javabook = new Book(); // 從將數據保存到服務器 javabook.save({ name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 });
在本例中,我們在調用時將數據傳遞給save()方法,而不是先通過set()方法設置數據。當然,你也可以像set()方法一樣,只設置某一個值:
javabook.save('name', 'Thinking in Java');
無論你通過什么方式來保存數據,它都會自動將數據同步到服務器接口(如果你沒有設置url或urlRoot參數,那么所有的操作只會在本地進行)。
我們來討論另一個問題:上面提到服務器接口返回的數據會被覆蓋到當前模型中,在剛剛的例子里,接口返回的數據就是模型需要的數據。而實際開發中往往並沒有這么順利,我們接口返回的數據可能是這樣:
{ "resultCode": "0", "error": "null", "data": [{ "isNew": "true", "bookId": "1001", "bookName": "Thinking in Java(修訂版)", "bookAuthor": "Bruce Eckel", "bookPrice": "395.70" }] }
你能看到,接口返回的數據無論從結構、還是屬性名,都與模型中定義的不一樣(有時甚至會返回XML或其它格式)。還好Backbone提供了一個parse()方法,用於在將服務器返回的數據覆蓋到模型前,對數據進行解析。
parse()方法默認不會對數據進行解析,因此我們只需要重載該方法,就可以適配上面的數據格式,例如:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service', // 重載parse方法解析服務器返回的數據 parse: function(resp, xhr) { var data = resp.data[0]; return { id: data.bookId, name: data.bookName, author: data.bookAuthor, price: data.bookPrice } } }); // 創建實例 var javabook = new Book(); // 從將數據保存到服務器 javabook.save({ name: 'Thinking in Java', author: 'Bruce Eckel', price: 395.70 });
我們重載了parse()方法,並返回了模型中能夠使用的格式,這樣就可以將服務器接口返回的數據與模型中的數據連接起來。雖然本例中使用了最簡單的方式解析,但實際上你可能還會做一些格式化、轉換和邏輯工作。
另外值得注意的一點是:我們常常會在數據保存成功后,對界面做一些改變。此時你可以通過許多種方式實現,例如通過save()方法中的success回調函數。
但我建議success回調函數中只要做一些與業務邏輯和數據無關的、單純的界面展現即可(就像控制加載動畫的顯示隱藏),如果數據保存成功之后涉及到業務邏輯或數據顯示,你應該通過監聽模型的change事件,並在監聽函數中實現它們。雖然Backbone並沒有這樣的要求和約束,但這樣更有利於組織你的代碼。
fetch()方法:
fetch()方法用於從服務器接口獲取模型的默認數據,常常用於模型的數據恢復,它的參數和原理與save()方法類似,因此你可以很容易理解它。
先讓我們看一個例子:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service' }); // 創建實例 var javabook = new Book(); // 從服務器獲取默認數據 javabook.fetch({ success: function() { // 獲取數據成功后, 重新讀取一次 javabook.fetch(); } });
在這個例子中,我們創建了一個空的(沒有初始化數據的)Book模型實例,然后通過fetch()方法從服務器接口獲取初始化數據,獲取數據成功后再次調用fetch()方法重新獲取一次。
我們將服務器接口返回的數據設置為:
{ "id": "1001", "name": "Thinking in Java", "author": "Bruce Eckel", "price": "395.70" }
你需要注意觀察兩次請求的URL和參數:
第一次請求地址為http://localhost/service,Request Method參數為GET
第二次請求地址為http://localhost/service/1001,Request Method參數為GET
你會發現第二次在請求地址后加上了1001(模型id),這是因為在第一次獲取數據成功后,服務器接口返回的數據會覆蓋到模型中,因此模型對象具備了唯一標識(id),因此在此后的每次請求中,模型都會將id加載請求地址后面。
destroy()方法:
destroy()方法用於將數據從集合(關於集合我們將在下一章中討論)和服務器中刪除,需要注意的是,該方法並不會清除模型本身的數據。(如果需要刪除模型中的數據,請手動調用unset()或clear()方法)
當你的模型對象從集合和服務器端刪除時,只要你不再保持任何對模型對象的引用,那么它會自動從內存中移除。(通常的做法是將引用模型對象的變量或屬性設置為null值)
當調用destroy()方法時,模型會觸發destroy事件,所有監聽該事件的函數將被調用。
我們還是通過一個例子來詳細了解它:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service' }); // 創建實例 var javabook = new Book({ id: '1001' }); // 從服務器刪除數據 javabook.destroy();
這個例子非常簡單,我們創建一個模型后再調用destroy()方法將它銷毀。
請抓包觀察請求地址和Request Method:
我們看到請求地址為:http://localhost/service/1001,Request Method參數為DELETE。它通過Reuqest Method請求參數通知服務器接口將要進行的操作,而請求地址和save()方法及fetch()方法產生的請求地址是相同的,這正體現了我們最開始所說的REST架構。
在調用destroy()方法時我們同樣可以傳遞一個配置對象,它除了success和error回調函數外,也能像save()方法一樣包含一個wait配置,來看下面的例子:
// 定義Book模型類 var Book = Backbone.Model.extend({ urlRoot: '/service' }); // 創建實例 var javabook = new Book({ id: '1001' }); // 監聽模型的destroy事件, 在控制台輸出字符串 javabook.on('destroy', function() { console.log('destroy'); }); // 從服務器刪除數據 javabook.destroy({ wait: true });
如果你的service服務器接口能正常訪問,那么你能看到在控制台輸出了“destroy”字符串;如果將你的接口設置為響應失敗(例如404),那么控制台就不會有輸出。
當我們傳遞了wait配置后,模型會先請求服務器接口對數據進行刪除,當服務器返回狀態成功(狀態碼200)之后,本地才會進行模型的刪除操作,最終觸發destroy事件。
如果你想通過Backbone實現數據同步,而不使用RET架構,那么你可以通過重新定義Backbone.sync方法來適配現有的服務器接口。
在Backbone中,所有與服務器交互的邏輯都定義在Backbone.sync方法中,該方法接收method、model和options三個參數。如果你想重新定義它,可以通過method參數得到需要進行的操作(枚舉值為create、read、update和delete),通過model參數得到需要同步的數據,最后根據它們來適配你自己定義的規則即可。
當然,你也可以將數據同步到本地數據庫中,而不是服務器接口,這在開發終端應用時會非常適用。
7、小結
至此,Backbone模型中的核心方法和特性我們都已經討論完了,我們總結一下本節討論的主要內容:
模型封裝了對象數據,並提供了一系列對數據進行操作的方法
我們可以在定義模型類、實例化模型對象、和調用set()方法來設置模型中的數據
當模型中數據發生改變時,會觸發change事件和屬性事件
我們可以定義validate方法對模型中的數據進行驗證
通過調用save()、fetch()和destroy()方法可以讓模型中的數據與服務器保持同步,但在此之前必須設置url或urlRoot屬性
當然,模型類還包含一些實用的方法幫助我們開發,這里就不一一介紹,通過API文檔你能輕易地理解它們。