Backbone Model——數據模型


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文檔你能輕易地理解它們。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM