7. Model (数据模型)
Model是Backbone中所有数据模型的基类,用于封装原始数据,并提供对数据进行操作的方法,我们一般通过继承的方式来扩展和使用它。
如果你做过数据库开发,可能对ORM(对象关系映射)不会陌生,而Backbone中的Model就像是映射出来的一个数据对象,它可以对应到数据库中的某一条记录,并通过操作对象,将数据自动同步到服务器数据库。(下一节即将介绍的Collection就像映射出的一个数据集合,它可以对应到数据库中的某一张或多张关联表)。
7.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中定义的默认状态。
7.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()等方法操作数据。
7.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元.”
- 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配置将在后面“数据验证”章节中介绍)
7.4 数据验证
Backbone模型提供了一套数据验证机制,确保我们在模型中存储的数据都是通过验证的,我们通过下面的例子来说明这套验证机制:
执行这段代码,你会在控制台看到这段信息:“书籍价格不应低于1元.”
在定义模型类时,我们可以添加一个validate方法,该方法会在模型中的数据发生改变之前被自动调用(就像我们通过set()方法修改数据时一样)。
validate方法接收一个参数,表示需要进行验证的数据集合,如果validate方法没有任何返回值(即undefined),则表示验证通过;如果验证不通过,我们常常会返回一个错误字符串或自定义对象。但实际上,当你返回一个false也会被认为验证通过,因为Backbone内部会将validate的返回值转换为布尔类型,如果为false则认为验证通过,反之则认为不通过(虽然这听起来有些别扭)。
当validate验证不通过时,会触发error事件,并将模型对象和validate方法的返回值传递给error事件的监听函数(就像例子中的那样)。
// 定义Book模型类 var Book = Backbone.Model.extend({ validate : function(data) { if(data.price < 1) { return '书籍价格不应低于1元.'; } } }); var javabook = new Book(); // 监听error事件,当验证失败时触发 javabook.on('error', function(model, error) { console.log(error); }); javabook.set('price', 0);
上面的例子中,我们监听了javabook对象的error事件,用于在验证不通过时提示用户。但在某个场景下,我希望以另一种方式提示用户,我可以在error监听函数中判断是否处于这种场景下,然后作出不同的提示,但这显然不是最好的办法。
因此,Backbone提供了另一种方式对error事件进行覆盖,来看看这个例子:
// 定义Book模型类 var Book = Backbone.Model.extend({ validate : function(data) { if(data.price < 1) { return '书籍价格不应低于1元.'; } return false; } }); var javabook = new Book({ price : 50 }); // 监听error事件,当验证失败时触发 javabook.on('error', function(model, error) { console.log(error); }); // 在调用set()方法时,传递了一个配置对象,包含自定义的error处理方法 javabook.set('price', 0, { error : function(model, error) { console.log('自定义错误:' + error); } });
在这段代码中,我们在调用set()方法时,传递了第三个参数,它是一个用于描述配置信息的对象,我们设定了一个error函数。当validate方法验证失败时,会优先调用配置中传递的error函数,如果没有传递error函数,则会触发error事件。
如果我们不希望对数据进行验证,可以在调用set()方法时传递一个silent配置,silent配置用于忽略验证规则,并且它不会触发change和error等事件。
在上面例子的基础上,我们修改一下set()方法的调用方式:
javabook.set('price', 0, { silent : true });
我们设置了一个非法的数据,同时指定了silent配置,结果并没有触发error事件,因为它并没有调用validate方法对数据进行验证。
我们再将set()方法的调用代码修改一下:
javabook.set('price', 0, { silent : true }); javabook.set('name', 'Thinking in Java');
执行这段代码,控制会再次输出“书籍价格不应低于1元.”。
从结果中可以看出,silent配置仅仅是忽略对本次数据的验证,而在下一次数据发生变化时,会重新执行validate方法进行验证。
使用silent配置设置数据时,模型也不会触发change事件,这这意味着数据在业务层面并没有真正被设置到模型中,这有利于我们及时对错误的数据进行恢复和回滚。
我们来看一个数据回滚的例子:
// 定义Book模型类 var Book = Backbone.Model.extend(); // price的初始值为50 javabook = new Book({ price : 50 }); // 我们设置一个非法数据并绕过验证 javabook.set('price', 0.2, { silent : true }); // 检查数据非法,回滚数据 if(javabook.get('price') < 1) { var value = javabook.previous('price'); javabook.set('price', value); }
在本例中,我们在实例化Book模型时设置price的初始化数据为50,然后将price设置为一个非法数据并绕过验证,最后调用previous()方法获取数据的上一个状态,并重新设置。
值得注意的是,在上一节中我们提到,previous()方法只有在change事件的监听函数中才能获取数据的上一个状态。而这里我并没有这样做,因为当我们使用silent配置时,设置的数据仅仅是被临时存放在模型,这种情况下Backbone允许我们随时调用previous()和previousAttributes()方法获取数据的上一个状态。
(数据的上一个状态会在下一次调用set()方法将数据设置到模型中后自动清除)
7.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()方法获取数据的上一个状态。
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方式:
// 定义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.7 小结
至此,Backbone模型中的核心方法和特性我们都已经讨论完了,我们总结一下本节讨论的主要内容:
- 模型封装了对象数据,并提供了一系列对数据进行操作的方法
- 我们可以在定义模型类、实例化模型对象、和调用set()方法来设置模型中的数据
- 当模型中数据发生改变时,会触发change事件和属性事件
- 我们可以定义validate方法对模型中的数据进行验证
- 通过调用save()、fetch()和destroy()方法可以让模型中的数据与服务器保持同步,但在此之前必须设置url或urlRoot属性
当然,模型类还包含一些实用的方法帮助我们开发,这里就不一一介绍,通过API文档你能轻易地理解它们。