`

Backbone入门指南(四):Model(数据模型)

阅读更多

本文转自:http://yujianshenbing.iteye.com/

 

7. Model (数据模型)

  Model是Backbone中所有数据模型的基类,用于封装原始数据,并提供对数据进行操作的方法,我们一般通过继承的方式来扩展和使用它。

  如果你做过数据库开发,可能对ORM(对象关系映射)不会陌生,而Backbone中的Model就像是映射出来的一个数据对象,它可以对应到数据库中的某一条记录,并通过操作对象,将数据自动同步到服务器数据库。(下一节即将介绍的Collection就像映射出的一个数据集合,它可以对应到数据库中的某一张或多张关联表)。

7.1 创建数据模型

  我们先通过一段代码来看看如何创建数据模型:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     defaults : {  
  4.         name : 'unknown',  
  5.         author : 'unknown',  
  6.         price : 0  
  7.     }  
  8. });  
  9.   
  10. // 实例化模型对象  
  11. var javabook = new Book({  
  12.     name : 'Thinking in Java',  
  13.     author : 'Bruce Eckel',  
  14.     price : 395.70  
  15. });  

 

 

  我们通过Model.extend方法,定义一个自己的模型类Book。

  Backbone模块类(包括子类)都包含一个extend静态方法用于实现继承。给extend方法传递的第一个参数应该是一个对象,对象中的属性和方法将被添加到子类,我们可以通过extend方法扩展子类或重载父类的方法。

  从Backbone模块类继承的子类,都包含一个__super__静态属性,这是一个指向父类原型对象的引用,例如:

 

Js代码  收藏代码
  1. var Book = Backbone.Model.extend({  
  2.     constructor: function() {  
  3.         Book.__super__.constructor.call(this)  
  4.     }  
  5. });  

 

 

  在这个例子中,我们重载了Model类的构造函数,但我们希望在子类被实例化时,调用父类的构造函数,因此我们可以通过引用Book.__super__.constructor来调用它。

  实际上我们一般并不会重载模块类的constructor方法,因为在Backbone中所有的模块类都提供了一个initialize方法,用于避免在子类中重载模块类的构造函数,当模块类的构造函数执行完成后会自动调用initialize方法。

 

  回到本节的第一个例子,我们在定义Book类的时候,传递了一个defaults参数,它用于定义模型数据的默认状态,虽然我们在创建Book实例后再添加它们,但为每个数据模型定义属性列表和默认值,是一个好的编码习惯。

  最后,我们通过new关键字,创建了一个Book的实例,并向它的构造函数中传递了一系列初始化数据,它们将覆盖defaults中定义的默认状态。

7.2 初始化和读取数据

  在我们定义好一个模型类之后,可以通过new关键字实例化该模型的对象。

  如果模型类在定义时设置了defaults默认数据,这些数据将被复制到每一个实例化的对象中,如:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     defaults : {  
  4.         name : 'unknown',  
  5.         author : 'unknown',  
  6.         price : 0  
  7.     }  
  8. });  
  9.   
  10. // 实例化模型对象  
  11. var javabook = new Book();  

 

 

  上面的代码创建了一个Book实例javabook,它包含了模型类在定义时的默认数据。

  我们将实例化的代码稍作修改:

 

Js代码  收藏代码
  1. // 实例化模型对象  
  2. var javabook = new Book({  
  3.     name : 'Thinking in Java'  
  4. });  
  5.   
  6. // 通过get和escape()方法获取模型中的数据  
  7. var name = javabook.get('name');  
  8. var author = javabook.escape('author');  
  9. var price = javabook.get('price');  
  10.   
  11. // 在控制台输出模型中的数据name  
  12. console.log(name); // 输出Thinking in Java  
  13. console.log(author); // 输出unknown  
  14. console.log(price); // 输出0  

 

 

  我们在实例化对象时传递了初始数据,它将覆盖Book类定义时defaults中的默认数据,这一点很容易理解。

 

  上面的例子中我们通过get()和escape()方法获取模型中的数据,它们的区别在于:

 

  • get()方法用于直接返回数据
  • escape()方法先将数据中包含的HTML字符转换为实体形式(例如它会将双引号转换为"形式)再返回,用于避免XSS攻击。

 

 

  模型将原始数据存放在对象的attributes属性中,因此我们也可以通过javabook.attributes属性直接读取和操作这些数据,例如:

 

Js代码  收藏代码
  1. // 在控制台直接输出对象的attributes属性  
  2. console.dir(javabook.attributes);  
  3.   
  4. // 控制台输出结果  
  5. // {  
  6. //     author: 'unknown',  
  7. //     name: 'Thinking in Java',  
  8. //     price: 0  
  9. // }  

 

 

  但通常并不会这样做,因为模型中数据状态的变化会触发一系列事件、同步等动作,直接操作attributes中的数据可能导致对象状态异常。更安全的做法是:通过get()或escape()方法读取数据,通过set()等方法操作数据。

7.3 修改数据

  我们通常可以调用模型对象的set()方法,来修改模型中的数据,例如:

 

Js代码  收藏代码
  1. // 实例化模型对象  
  2. var javabook = new Book();  
  3.   
  4. // 通过set方法设置模型数据  
  5. javabook.set('name''Java7入门经典');  
  6. javabook.set('author''Ivor Horton');  
  7. javabook.set('price', 88.50);  
  8.   
  9. // 获取数据并将数据输出到控制台  
  10. var name = javabook.get('name');  
  11. var author = javabook.get('author');  
  12. var price = javabook.get('price');  
  13.   
  14. console.log(name); // 输出Java7入门经典  
  15. console.log(author); // 输出Ivor Horton  
  16. console.log(price); // 输出88.50  
  17. set()方法也允许同时设置多个属性,例如:  
  18. javabook.set({  
  19.     name : 'Java7入门经典',  
  20.     author : 'Ivor Horton',  
  21.     price : 88.50  
  22. });  

 

 

  当调用set()方法修改模型中的数据时,会触发一系列事件,我们常常通过监听这些事件,来动态调整界面中数据的显示,我们先来看一个例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     defaults : {  
  4.         name : 'unknown',  
  5.         author : 'unknown',  
  6.         price : 0  
  7.     }  
  8. });  
  9.   
  10. // 实例化模型对象  
  11. var javabook = new Book();  
  12.   
  13. // 监听模型"change"事件  
  14. javabook.on('change'function(model) {  
  15.     console.log('change事件被触发');  
  16. });  
  17. // 监听模型"change:name"事件  
  18. javabook.on('change:name'function(model, value) {  
  19.     console.log('change:name事件被触发');  
  20. });  
  21. // 监听模型"change:author"事件  
  22. javabook.on('change:author'function(model, value) {  
  23.     console.log('change:author事件被触发');  
  24. });  
  25. // 通过set()方法设置数据  
  26. javabook.set({  
  27.     name : 'Thinking in Java',  
  28.     author : 'unknown',  
  29.     price : 395.70  
  30. });  
  31.   
  32. // 控制台输出结果:  
  33. // change:name事件被触发  
  34. // change事件被触发  

 

 

  在本例中,我们监听了模型对象的change事件,该事件在模型对象的任何数据发生改变时被触发,change事件触发时,会将当前模型作为参数传递给监听函数。

  我们还监听了change:name和change:author两个属性事件,属性事件是当模型中对应属性的数据发生改变时被触发,属性事件按照“change:属性名”来命名,因此它并不固定。属性事件触发时,会将当前模型和最新的数据作为参数传递给监听函数。

  本例执行后在控制台的输出结果为:

  “change:name事件被触发

  change事件被触发”

  从结果中看,并没有触发我们监听的change:author事件,因为在调用set()方法时,它会在内部检查新的数据较上一次是否发生变化,只有发生变化的数据才会被设置和触发监听事件。

  另一个细节是,我们先监听了change事件,然后监听了属性事件,但事件在触发时,总是会先触发属性事件,然后再触发change事件。

 

  Backbone允许我们在修改模型数据时获取上一个状态的数据,这常常用于数据比较和数据回滚。

  例如在下面的例子中,我们希望当price价格被改变时,提示用户价格的变化情况:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     defaults : {  
  4.         name : 'unknown',  
  5.         author : 'unknown',  
  6.         price : 0  
  7.     }  
  8. });  
  9.   
  10. // 实例化模型对象  
  11. var javabook = new Book();  
  12.   
  13. // 监听"change:price"事件  
  14. javabook.on('change:price'function(model, value) {  
  15.     var price = model.get('price');  
  16.   
  17.     if(price < value) {  
  18.         console.log('价格上涨了' + (value - price) + '元.');  
  19.     } else if(price > value) {  
  20.         console.log('价格下降了' + (value - price) + '元.');  
  21.     } else {  
  22.         console.log('价格没有发生变化.');  
  23.     }  
  24. });  
  25. // 设置新的价格  
  26. javabook.set('price', 50);  
  27.   
  28. // 控制台输出结果:  
  29. // 价格没有发生变化.  

 

 

  我们通过监听change:price事件来监听价格的变化,并希望将最新的价格和当前(上一次)的价格作比较,但控制台的输出结果却是“价格没有发生变化.”。这是因为当change事件或属性事件被触发时,模型中的数据已经被修改,因此通过get()方法获取到的是模型中最新的数据。

  这时,我们可以通过previous()和previousAttributes()方法来获取数据被修改之前的状态。

  我们将代码稍作修改,只需要修改监听事件的函数即可:

 

Js代码  收藏代码
  1. // 监听"change:price"事件  
  2. javabook.on('change:price'function(model, value) {  
  3.     var price = model.previous('price');  
  4.   
  5.     if(price < value) {  
  6.         console.log('价格上涨了' + (value - price) + '元.');  
  7.     } else if(price > value) {  
  8.         console.log('价格下降了' + (value - price) + '元.');  
  9.     } else {  
  10.         console.log('价格没有发生变化.');  
  11.     }  
  12. });  

 

 

  我们将get()方法修改为previous()方法,用来获取价格在修改之前的状态,此时控制台输出的结果为:“价格上涨了50元.”

 

  • previous()方法接收一个属性名,并返回该属性在修改之前的状态;
  • previousAttributes()方法返回一个对象,该对象包含上一个状态的所有数据。

 

  需要注意的是,previous()和previousAttributes()方法只能在数据修改过程中调用(即在模型的change事件和属性事件中调用),比如下面的例子就是错误的调用方法:

 

Js代码  收藏代码
  1. // 设置新的价格  
  2. javabook.set('price', 50);  
  3.   
  4. var prevPrice = javabook.previous('price');  
  5. var newPrice = javabook.get('price');  
  6.   
  7. if(prevPrice < newPrice) {  
  8.     console.log('价格上涨了' + (newPrice - prevPrice) + '元.');  
  9. else if(prevPrice > newPrice) {  
  10.     console.log('价格下降了' + (newPrice - prevPrice) + '元.');  
  11. else {  
  12.     console.log('价格没有发生变化.');  
  13. }  
  14.   
  15. // 控制台输出结果:  
  16. // 价格没有发生变化.  

 

 

  控制台输出的结果是“价格没有发生变化.”,因为在set()方法被调用完毕后,模型的上一个状态也会被新数据替换。

  (有一种特殊情况是当我们使用了silent配置时,上面的代码可以得到我们想要的结果,关于silent配置将在后面“数据验证”章节中介绍)

7.4 数据验证

  Backbone模型提供了一套数据验证机制,确保我们在模型中存储的数据都是通过验证的,我们通过下面的例子来说明这套验证机制:

 

  执行这段代码,你会在控制台看到这段信息:“书籍价格不应低于1元.”

  在定义模型类时,我们可以添加一个validate方法,该方法会在模型中的数据发生改变之前被自动调用(就像我们通过set()方法修改数据时一样)。

  validate方法接收一个参数,表示需要进行验证的数据集合,如果validate方法没有任何返回值(即undefined),则表示验证通过;如果验证不通过,我们常常会返回一个错误字符串或自定义对象。但实际上,当你返回一个false也会被认为验证通过,因为Backbone内部会将validate的返回值转换为布尔类型,如果为false则认为验证通过,反之则认为不通过(虽然这听起来有些别扭)。

  当validate验证不通过时,会触发error事件,并将模型对象和validate方法的返回值传递给error事件的监听函数(就像例子中的那样)。

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     validate : function(data) {  
  4.         if(data.price < 1) {  
  5.             return '书籍价格不应低于1元.';  
  6.         }  
  7.     }  
  8. });  
  9.   
  10. var javabook = new Book();  
  11.   
  12. // 监听error事件,当验证失败时触发  
  13. javabook.on('error'function(model, error) {  
  14.     console.log(error);  
  15. });  
  16.   
  17. javabook.set('price', 0);  

 

 

  上面的例子中,我们监听了javabook对象的error事件,用于在验证不通过时提示用户。但在某个场景下,我希望以另一种方式提示用户,我可以在error监听函数中判断是否处于这种场景下,然后作出不同的提示,但这显然不是最好的办法。

  因此,Backbone提供了另一种方式对error事件进行覆盖,来看看这个例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     validate : function(data) {  
  4.         if(data.price < 1) {  
  5.             return '书籍价格不应低于1元.';  
  6.         }  
  7.         return false;  
  8.     }  
  9. });  
  10.   
  11. var javabook = new Book({  
  12.     price : 50  
  13. });  
  14.   
  15. // 监听error事件,当验证失败时触发  
  16. javabook.on('error'function(model, error) {  
  17.     console.log(error);  
  18. });  
  19.   
  20. // 在调用set()方法时,传递了一个配置对象,包含自定义的error处理方法  
  21. javabook.set('price', 0, {  
  22.     error : function(model, error) {  
  23.         console.log('自定义错误:' + error);  
  24.     }  
  25. });  

 

 

  在这段代码中,我们在调用set()方法时,传递了第三个参数,它是一个用于描述配置信息的对象,我们设定了一个error函数。当validate方法验证失败时,会优先调用配置中传递的error函数,如果没有传递error函数,则会触发error事件。

 

  如果我们不希望对数据进行验证,可以在调用set()方法时传递一个silent配置,silent配置用于忽略验证规则,并且它不会触发change和error等事件。

  在上面例子的基础上,我们修改一下set()方法的调用方式:

 

Js代码  收藏代码
  1. javabook.set('price', 0, {  
  2.     silent : true  
  3. });  

 

 

  我们设置了一个非法的数据,同时指定了silent配置,结果并没有触发error事件,因为它并没有调用validate方法对数据进行验证。

  我们再将set()方法的调用代码修改一下:

 

Js代码  收藏代码
  1. javabook.set('price', 0, {  
  2.     silent : true  
  3. });  
  4.   
  5. javabook.set('name''Thinking in Java');  

 

 

  执行这段代码,控制会再次输出“书籍价格不应低于1元.”。

  从结果中可以看出,silent配置仅仅是忽略对本次数据的验证,而在下一次数据发生变化时,会重新执行validate方法进行验证。

 

  使用silent配置设置数据时,模型也不会触发change事件,这这意味着数据在业务层面并没有真正被设置到模型中,这有利于我们及时对错误的数据进行恢复和回滚。

  我们来看一个数据回滚的例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend();  
  3.   
  4. // price的初始值为50  
  5. javabook = new Book({  
  6.     price : 50  
  7. });  
  8.   
  9. // 我们设置一个非法数据并绕过验证  
  10. javabook.set('price', 0.2, {  
  11.     silent : true  
  12. });  
  13.   
  14. // 检查数据非法,回滚数据  
  15. if(javabook.get('price') < 1) {  
  16.     var value = javabook.previous('price');  
  17.     javabook.set('price', value);  
  18. }  

 

 

  在本例中,我们在实例化Book模型时设置price的初始化数据为50,然后将price设置为一个非法数据并绕过验证,最后调用previous()方法获取数据的上一个状态,并重新设置。

  值得注意的是,在上一节中我们提到,previous()方法只有在change事件的监听函数中才能获取数据的上一个状态。而这里我并没有这样做,因为当我们使用silent配置时,设置的数据仅仅是被临时存放在模型,这种情况下Backbone允许我们随时调用previous()和previousAttributes()方法获取数据的上一个状态。

  (数据的上一个状态会在下一次调用set()方法将数据设置到模型中后自动清除)

7.5 删除数据

  Backbone中删除模型数据的操作相对简单,我们常常用unset()和clear()方法来删除模型中的数据:

 

  • unset()方法用于删除对象中指定的属性和数据
  • clear()方法用于删除模型中所有的属性和数据

 

 

  我们来看一个unset()方法的例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend();  
  3.   
  4. // 实例化模型对象  
  5. var javabook = new Book({  
  6.     name : 'Java7入门经典',  
  7.     author : 'Ivor Horton',  
  8.     price : 88.50  
  9. });  
  10.   
  11. // 输出: Java7入门经典  
  12. console.log(javabook.get('name'));  
  13.   
  14. // 删除对象name属性  
  15. javabook.unset('name');  
  16.   
  17. // 输出: undefined  
  18. console.log(javabook.get('name'));  
  19. 当我们对模型的name属性执行unset()方法后,模型内部会使用delete关键字将name属性从对象中删除。  
  20.   
  21. clear()方法与unset()方法执行过程类似,但clear()方法会删除模型中的所有数据,例如:  
  22. // 定义Book模型类  
  23. var Book = Backbone.Model.extend();  
  24.   
  25. // 实例化模型对象  
  26. var javabook = new Book({  
  27.     name : 'Java7入门经典',  
  28.     author : 'Ivor Horton',  
  29.     price : 88.50  
  30. });  
  31.   
  32. // 删除对象name属性  
  33. javabook.clear();  
  34.   
  35. // 以下均输出: undefined  
  36. console.log(javabook.get('name'));  
  37. console.log(javabook.get('author'));  
  38. console.log(javabook.get('price'));  

 

 

  在调用unset()和clear()方法清除模型数据时,会触发change事件,我们也同样可以在change事件的监听函数中通过previous()和previousAttributes()方法获取数据的上一个状态。

7.6 将模型数据同步到服务器

  Backbone提供了与服务器数据的无缝连接,我们只需要操作本地Model对象,Backbone就会按照规则自动将数据同步到服务器。

  如果需要使用Backbone默认的数据同步特性,请确定你的服务器数据接口已经支持了REST架构。在REST架构中,客户端会通过请求头中的Request Method告诉服务器我们将要进行的操作(包括create、read、update和delete,它们对应的Request Method分别为POST、GET、PUT和DELETE),而对于没有良好支持REST发送方式的浏览器,Backbone会使用另外一些方法来实现,这在本节中会详细讨论。

 

  在讨论数据同步相关方法之前,你需要先了解一些Backbone中与数据同步息息相关的内容:

  数据标识:

  设想一下,如果我们需要通过服务器接口删除一条数据,仅仅在报文头中通过Request Method标识告诉服务器进行delete操作是不够的,更重要的是还需要告诉服务器删除哪一条数据,这需要我们传递给服务器一个数据的唯一标识(例如记录id)。

  Backbone中每一个模型对象都有一个唯一标识,默认名称为id,你可以通过idAttribute属性来修改它的名称。

  id应该由服务器端创建并保存在数据库中,在与服务器的每一次交互中,模型会自动在URL后面加上id,而对于客户端新建的模型,在保存时不会在URL后加上id标识(我们可以通过模型的isNew()方法来检查,该模型对象是否是由客户端新建的)。

  URL规则:

  Backbone默认使用PATHINFO的方式来访问服务器接口。

  例如:我们在删除一个模型数据时,模型会在报文头的Request Method中声明delete操作,并在服务器接口后自动加上模型id,格式类似于http://urlRoot/10001,其中urlRoot是我们设置的服务器接口地址,而10001是模型id。请注意它是通过URL路径的方式自动追加到接口地址后的,因此服务器也必须要支持PATHINFO的解析方式。

  使用PATHINFO方式,因为它更直观,更利于SEO,还可以避免与Backbone中的路由器发生混淆(关于路由器将在后面的章节中介绍)。

 

  如果我们希望让Backbone自动与服务器接口进行交互,首先应该配置模型的URL,Backbone支持3种方式的URL配置:

  第一种是urlRoot方式:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service'  
  4. });  
  5.   
  6. // 创建实例  
  7. var javabook = new Book({  
  8.     id : 1001,  
  9.     name : 'Thinking in Java',  
  10.     author : 'Bruce Eckel',  
  11.     price : 395.70  
  12. });  
  13.   
  14. // 保存数据  
  15. javabook.save();  

 

 

  在这个例子中,我们创建了一个Book模型的实例,并调用save()方法将数据保存到服务器。(可能你对save()方法还不太了解,但这并不重要,因为我们马上就会讲到它,现在你只需知道我们用它将模型中的数据保存到服务器)

  你可以抓包查看请求记录,你能看到请求的接口地址为:http://localhost/service/1001

  其中localhost是我的主机名,因为我在本地搭建了一个Web服务器环境。

  service是该模型的接口地址,是我们在定义Book类时设置的urlRoot。

  1001是模型的唯一标识(id),我们之前说过,模型的id应该是由服务器返回的,对应到数据库中的某一条记录,但这里为了能直观的测试,我们假设已经从服务器端拿到了数据,且它的id为1001。

  这段内容很容易理解,接下来,我们将save()方法换成destroy()方法(该方法用于将模型中的数据从服务器删除):

 

Js代码  收藏代码
  1. // 删除数据  
  2. javabook.destroy();  

 

 

  你能看到请求的接口地址仍然为:http://localhost/service/1001。这并不奇怪,如果你细心观察,会发现两次请求头中的Request Method参数分别为PUT和DELETE,服务器接口会根据它来判断你所做的操作。

  如果你的浏览器不支持REST发送方式,你可能会看到Request Method始终是POST类型,且在Form Data中会多出一个_method参数,PUT和DELETE操作名被放在了这个_method参数中。这是Backbone为了适配低版本浏览器而设计的另一种方法,你的服务器接口也必须同时支持这种方式。

 

  我们再来看第二种URL方式的例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3. urlRoot : '/service',  
  4. url : '/javaservice'  
  5. });  
  6.   
  7. // 创建实例  
  8. var javabook = new Book({  
  9.     id : 1001,  
  10.     name : 'Thinking in Java',  
  11.     author : 'Bruce Eckel',  
  12.     price : 395.70  
  13. });  
  14.   
  15. // 保存数据  
  16. 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方式的例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service',  
  4.     url : '/javaservice'  
  5. });  
  6.   
  7. // 创建实例  
  8. var javabook = new Book({  
  9.     id : 1001,  
  10.     name : 'Thinking in Java',  
  11.     author : 'Bruce Eckel',  
  12.     price : 395.70  
  13. });  
  14.   
  15. // 保存数据  
  16. javabook.save(null, {  
  17.     url: '/myservice'  
  18. });  

 

 

  在这个例子中,我们在调用save()方法的时候传递了一个配置对象,它包含一个url配置项,最终抓包看到的请求地址是http://localhost/myservice。因此你可以得知,通过调用方法时传递的url参数优先级会高于模型定义时配置的url和urlRoot参数。

 

  在Backbone中,模型对象提供了3个方法用于和服务器保持数据同步:

 

  • save()方法:在服务器创建或修改数据
  • fetch()方法:从服务器获取数据
  • destroy()方法:从服务器移除数据

 

  下面我们将依次介绍这些方法的使用:

 

  save()方法:

  save()方法用于将模型的数据保存到服务器,它可能是一条新的数据,也可能是修改服务器现有的某一条数据,这取决于模型中是否存在id(唯一标识)。

  首先我们来看一个创建数据的例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service'  
  4. });  
  5.   
  6. // 创建实例  
  7. var javabook = new Book();  
  8.   
  9. // 设置初始化数据  
  10. javabook.set({  
  11.     name : 'Thinking in Java',  
  12.     author : 'Bruce Eckel',  
  13.     price : 395.70  
  14. });  
  15.   
  16. // 从将数据保存到服务器  
  17. javabook.save();  

 

 

  在这个例子中,我们创建了一个新的Book实例,并设置了一些数据(实际上它们可能是由用户输入的),我们通过save()方法将这些数据提交到服务器。

  如果你抓包看一下报文头信息,能看到Request Method参数为POST,这是因为模型内部会通过isNew()方法检查是否为客户端新建,如果是客户端新建的数据,会通过POST方式发送。如果是修改服务器现有的数据,则通过PUT方式发送。

  如果服务器接口的报文体中没有返回任何数据,你会发现保存之后的模型较之前没有发生任何变化,在你下一次调用save()方法的时候,它仍然会以POST方式通知服务器创建一条新的数据。

  这是因为模型对象并没有获取到刚刚服务器创建成功的记录id,因此我们希望服务器接口在将数据保存成功之后,同时将新的id返回给我们,就像这样:

 

Js代码  收藏代码
  1. {  
  2.     "id" : "1001",  
  3.     "name" : "Thinking in Java(修订版)",  
  4.     "author" : "Bruce Eckel",  
  5.     "price" : "395.70"  
  6. }  

 

 

  这一段是服务器接口返回的数据,它除了返回新记录的id,还返回了修改后的name数据(当然,你也可以只返回新记录的id,我们常常都是这样做的)。这时我们再来看现在模型中的数据,它多了一个id属性,并且name属性的值也发生了变化,也就是说模型使用服务器返回的最新数据替换了之前的数据。

  我们将代码稍作修改:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service'  
  4. });  
  5.   
  6. // 创建实例  
  7. var javabook = new Book();  
  8.   
  9. // 设置初始化数据  
  10. javabook.set({  
  11.     name : 'Thinking in Java',  
  12.     author : 'Bruce Eckel',  
  13.     price : 395.70  
  14. });  
  15.   
  16. // 将数据保存到服务器  
  17. javabook.save(null, {  
  18.     success : function(model) {  
  19.         // 数据保存成功之后, 修改price属性并重新保存  
  20.         javabook.set({  
  21.             price : 388.00  
  22.         });  
  23.         javabook.save();  
  24.     }  
  25. });  

 

 

  我们修改了save()方法的调用参数,像例子中那样,你可以设置一个success回调函数用来表示保存成功之后将要进行操作(你也可以设置一个error回调函数用来表示保存失败时将要进行的操作)。

  在数据保存成功之后,我们将修改模型的price值,并从新调用save()方法保存数据。

  我们抓包看一下请求头,发生了一些什么变化:

  Request Method变成了PUT。

  请求的接口地址变成了http://localhost/service/1001(这与我们刚刚讨论的URL配置有关,如果不明白可以重新阅读本节)。

  当然,还有提交的数据也变成了我们修改后的。

 

  在调用save()方法时,我们可以传递一个配置项对象,上面我们已经使用它传递了一个success回调函数。

  在配置项中,还可以包含一个wait配置,如果我们传递了wait配置为true,那么数据会在被提交到服务器之前进行验证,当服务器没有响应新数据(或响应失败)时,模型中的数据会被还原为修改前的状态。如果没有传递wait配置,那么无论服务器是否保存成功,模型数据均会被修改为最新的状态、或服务器返回的数据。

  我们还是用一个例子来说明:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     defaults : {  
  4.         name : 'unknown',  
  5.         author : 'unknown',  
  6.         price : 0  
  7.     },  
  8.     urlRoot : '/service'  
  9. });  
  10.   
  11. // 创建实例  
  12. var javabook = new Book();  
  13.   
  14. // 从将数据保存到服务器  
  15. javabook.save({  
  16.     name : 'Thinking in Java',  
  17.     author : 'Bruce Eckel',  
  18.     price : 395.70  
  19. }, {  
  20.     wait : true  
  21. });  

 

 

  请运行这个例子中的代码,并且将服务器接口返回的数据设置为空(或404状态),你能看到在save()方法调用完成之后,模型中的数据被恢复成最初defaults中定义的数据,因为我们在调用save()方法时传递了wait配置。(你也可以试着将wait配置去掉,然后再运行它,你会发现虽然服务器接口并没有返回数据或保存成功,但模型对象中仍然保持着最新的数据)

 

  正如我们最开始所讲得那样,save()方法用于添加一条新的数据到服务器,或修改现有的一条数据。

  其实save()方法也可以同时实现数据修改和保存,例如:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service'  
  4. });  
  5.   
  6. // 创建实例  
  7. var javabook = new Book();  
  8.   
  9. // 从将数据保存到服务器  
  10. javabook.save({  
  11.     name : 'Thinking in Java',  
  12.     author : 'Bruce Eckel',  
  13.     price : 395.70  
  14. });  

 

 

  在本例中,我们在调用时将数据传递给save()方法,而不是先通过set()方法设置数据。当然,你也可以像set()方法一样,只设置某一个值:

 

Js代码  收藏代码
  1. javabook.save('name''Thinking in Java');  

 

 

  无论你通过什么方式来保存数据,它都会自动将数据同步到服务器接口(如果你没有设置url或urlRoot参数,那么所有的操作只会在本地进行)。

 

  我们来讨论另一个问题:上面提到服务器接口返回的数据会被覆盖到当前模型中,在刚刚的例子里,接口返回的数据就是模型需要的数据。而实际开发中往往并没有这么顺利,我们接口返回的数据可能是这样:

 

Js代码  收藏代码
  1. {  
  2.     "resultCode" : "0",  
  3.     "error" : "null",  
  4.     "data" : [{  
  5.         "isNew" : "true",  
  6.         "bookId" : "1001",  
  7.         "bookName" : "Thinking in Java(修订版)",  
  8.         "bookAuthor" : "Bruce Eckel",  
  9.         "bookPrice" : "395.70"  
  10.     }]  
  11. }  

 

 

  你能看到,接口返回的数据无论从结构、还是属性名,都与模型中定义的不一样(有时甚至会返回XML或其它格式)。还好Backbone提供了一个parse()方法,用于在将服务器返回的数据覆盖到模型前,对数据进行解析。

  parse()方法默认不会对数据进行解析,因此我们只需要重载该方法,就可以适配上面的数据格式,例如:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service',  
  4.     // 重载parse方法解析服务器返回的数据  
  5.     parse : function(resp, xhr) {  
  6.         var data = resp.data[0];  
  7.         return {  
  8.             id : data.bookId,  
  9.             name : data.bookName,  
  10.             author : data.bookAuthor,  
  11.             price : data.bookPrice  
  12.         }  
  13.     }  
  14. });  
  15.   
  16. // 创建实例  
  17. var javabook = new Book();  
  18.   
  19. // 从将数据保存到服务器  
  20. javabook.save({  
  21.     name : 'Thinking in Java',  
  22.     author : 'Bruce Eckel',  
  23.     price : 395.70  
  24. });  

 

 

  我们重载了parse()方法,并返回了模型中能够使用的格式,这样就可以将服务器接口返回的数据与模型中的数据连接起来。虽然本例中使用了最简单的方式解析,但实际上你可能还会做一些格式化、转换和逻辑工作。

 

  另外值得注意的一点是:我们常常会在数据保存成功后,对界面做一些改变。此时你可以通过许多种方式实现,例如通过save()方法中的success回调函数。

  但我建议success回调函数中只要做一些与业务逻辑和数据无关的、单纯的界面展现即可(就像控制加载动画的显示隐藏),如果数据保存成功之后涉及到业务逻辑或数据显示,你应该通过监听模型的change事件,并在监听函数中实现它们。虽然Backbone并没有这样的要求和约束,但这样更有利于组织你的代码。

 

  fetch()方法:

  fetch()方法用于从服务器接口获取模型的默认数据,常常用于模型的数据恢复,它的参数和原理与save()方法类似,因此你可以很容易理解它。

  先让我们看一个例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service'  
  4. });  
  5.   
  6. // 创建实例  
  7. var javabook = new Book();  
  8.   
  9. // 从服务器获取默认数据  
  10. javabook.fetch({  
  11.     success : function() {  
  12.         // 获取数据成功后, 重新读取一次  
  13.         javabook.fetch();  
  14.     }  
  15. });  

 

 

  在这个例子中,我们创建了一个空的(没有初始化数据的)Book模型实例,然后通过fetch()方法从服务器接口获取初始化数据,获取数据成功后再次调用fetch()方法重新获取一次。

  我们将服务器接口返回的数据设置为:

 

Js代码  收藏代码
  1. {  
  2.     "id" : "1001",  
  3.     "name" : "Thinking in Java",  
  4.     "author" : "Bruce Eckel",  
  5.     "price" : "395.70"  
  6. }  

 

 

  你需要注意观察两次请求的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事件,所有监听该事件的函数将被调用。

  我们还是通过一个例子来详细了解它:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service'  
  4. });  
  5.   
  6. // 创建实例  
  7. var javabook = new Book({  
  8.     id: '1001'  
  9. });  
  10.   
  11. // 从服务器删除数据  
  12. javabook.destroy();  

 

 

  这个例子非常简单,我们创建一个模型后再调用destroy()方法将它销毁。

  请抓包观察请求地址和Request Method:

  我们看到请求地址为:http://localhost/service/1001,Request Method参数为DELETE。它通过Reuqest Method请求参数通知服务器接口将要进行的操作,而请求地址和save()方法及fetch()方法产生的请求地址是相同的,这正体现了我们最开始所说的REST架构。

  在调用destroy()方法时我们同样可以传递一个配置对象,它除了success和error回调函数外,也能像save()方法一样包含一个wait配置,来看下面的例子:

 

Js代码  收藏代码
  1. // 定义Book模型类  
  2. var Book = Backbone.Model.extend({  
  3.     urlRoot : '/service'  
  4. });  
  5.   
  6. // 创建实例  
  7. var javabook = new Book({  
  8.     id : '1001'  
  9. });  
  10.   
  11. // 监听模型的destroy事件, 在控制台输出字符串  
  12. javabook.on('destroy'function() {  
  13.     console.log('destroy');  
  14. });  
  15.   
  16. // 从服务器删除数据  
  17. javabook.destroy({  
  18.     wait : true  
  19. });  

 

 

  如果你的service服务器接口能正常访问,那么你能看到在控制台输出了“destroy”字符串;如果将你的接口设置为响应失败(例如404),那么控制台就不会有输出。

  当我们传递了wait配置后,模型会先请求服务器接口对数据进行删除,当服务器返回状态成功(状态码200)之后,本地才会进行模型的删除操作,最终触发destroy事件。

 

  如果你想通过Backbone实现数据同步,而不使用RET架构,那么你可以通过重新定义Backbone.sync方法来适配现有的服务器接口。

  在Backbone中,所有与服务器交互的逻辑都定义在Backbone.sync方法中,该方法接收method、model和options三个参数。如果你想重新定义它,可以通过method参数得到需要进行的操作(枚举值为create、read、update和delete),通过model参数得到需要同步的数据,最后根据它们来适配你自己定义的规则即可。

  当然,你也可以将数据同步到本地数据库中,而不是服务器接口,这在开发终端应用时会非常适用。

7.7 小结

  至此,Backbone模型中的核心方法和特性我们都已经讨论完了,我们总结一下本节讨论的主要内容:

  • 模型封装了对象数据,并提供了一系列对数据进行操作的方法
  • 我们可以在定义模型类、实例化模型对象、和调用set()方法来设置模型中的数据
  • 当模型中数据发生改变时,会触发change事件和属性事件
  • 我们可以定义validate方法对模型中的数据进行验证
  • 通过调用save()、fetch()和destroy()方法可以让模型中的数据与服务器保持同步,但在此之前必须设置url或urlRoot属性

  当然,模型类还包含一些实用的方法帮助我们开发,这里就不一一介绍,通过API文档你能轻易地理解它们。

分享到:
评论
Global site tag (gtag.js) - Google Analytics