`

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

阅读更多

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文档你能轻易地理解它们。

 

8
2
分享到:
评论
11 楼 13770586272 2015-11-25  
10 楼 jia58960 2014-10-27  
klamtine 写道
文章的节奏进度同同类Backbone教程相比拿捏的要好很多,可能是由于版本问题,7.4数据验证部分,Backbone1.1.2下 set 默认不触发 validate事件,需要传额外的参数:

javabook.set('price', 0, [color=darkred]{validate: true}[/color]);


另外,validate未通过默认触发的事件是 invalid,而非 error。所以回调事件代码如下:

javabook.on('invalid', function () { //do something here })


以上。

一开始没看评论,调试了老半天。楼上正解!
9 楼 klamtine 2014-05-31  
文章的节奏进度同同类Backbone教程相比拿捏的要好很多,可能是由于版本问题,7.4数据验证部分,Backbone1.1.2下 set 默认不触发 validate事件,需要传额外的参数:

javabook.set('price', 0, [color=darkred]{validate: true}[/color]);


另外,validate未通过默认触发的事件是 invalid,而非 error。所以回调事件代码如下:

javabook.on('invalid', function () { //do something here })


以上。
8 楼 wdydxf 2014-01-02  
       
7 楼 fansfirst2008 2013-10-27  
刚看了下官方文档:
  "error" (model, xhr, options) — when a model's save call fails on the server.
"invalid" (model, error, options) — when a model's validation fails on the client.

但是那个自定义的错误响应,还真不晓得怎么实现?
6 楼 fansfirst2008 2013-10-27  
感谢楼主的贡献,支持

接2楼, 好像监听不到error事件吧? 我在chrome里面测试,需要改成invalid.
楼主是哪个版本测试的?
5 楼 duni_读你 2013-09-03  
楼主辛苦了,看了你的文章,真是受益匪浅啊。 支持一下
4 楼 nowboy11427 2013-08-29  
感谢楼主的贡献,我感觉非常适合作为 backbone 的入门教材
3 楼 rn131456789 2013-04-18  
Upgrading to 1.0 

Model validation is now only enforced by default in save — not in set unless the {validate:true} option is passed. Model validation now fires an "invalid" event instead of "error".
2 楼 asdf6339 2013-03-21  
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.set({'price': 0}, { validate : true});


//上面的代码是不会进行数据验证的,set方法默认是不去执行validate方法的,必需加入validate这个属性,本人测试的Backbone v1.0
1 楼 bluedest 2012-12-16  
赞,希望楼主有空研究下emberjs,也写点指南出来。

相关推荐

Global site tag (gtag.js) - Google Analytics