2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Backbone.js](http://backbonejs.org/)

Last updated at Posted at 2013-03-23

Simple Starting Point

创建 Model

var Person = Backbone.Model.extend();

实例化 && 读写

var person = new Person({
	id: 1,
	name: "Albert Yu"
});

person.get("name"); // "Albert Yu"

person.set({name: "Yu Fan"});

创建 View

var PersonView = Backbone.View.extend({
	render: function() {
		var person = "<p>" + this.model.get("user") + "</p>";
		$(this.el).html(person);
	}
});

最后一行的 this.el 蕴含了丰富的意义:

  1. 每一个 View Model 事实上创建了一个页面作用域(Page Scope),el 即是该作用域下的顶级标签,其默认标签是 <div></div>

  2. this 在这里就是指代了此 View Model 所定义的作用域;

  3. render 方法里生成的页面元素都会被包裹在 el 之内,并受到 this 的控制。

实例化 && 添加页面元素(获取 Model 的数据)

var personView = new PersonView({
	model: person;
})

personView.render();
console.log(personView.el); // check the el

Better Model

缺省数据

在创建 Model 的时候,可以指定缺省数据:

var Person = Backbone.Model.extend({
	defaults: {
		name: "unnamed";
		dob: new Date()
	}
});

注意:在 Javascript 中,对象是通过引用传递的,这就意味着动态生成的数据会保持第一次执行时的状态。像上面那个例子,每次初始化一个 Person 的实例都会产生相同的 dob 数字。可以把 defaults 定义为一个函数来解决这个问题。如下:

var Person = Backbone.Model.extend({
	defaults: function() {
		return {
			name: "unnamed";
			dob: new Date()
		}
	}
});

从服务器获得数据(RESTful way)

var Person = Backbone.Model.extend({urlRoot: "/people"}); // 指定数据来源

var person = new Person({id: 1}); // 指定对象目标

person.fetch(); // GET /people/1

person.set({name: "Albert Yu"}); // 修改数据属性
person.save(); // PUT /people/1

var newPerson = new PersonModel(); // 生成新对象
newPerson.set({name: "John Doe"}); // 设置对象属性
newPerson.save(); // POST /people

newPerson.get("id"); // 2
newPerson.destroy(); // DELETE /people/2

监听对象的变化(并做出响应)

var person = new Person({id: 1}); // 生成实例对象

person.on("change", function() {
	alert("Something changed on " + this.get("name") + " you!");
});

注册在 person 对象下的 change 事件是 Backbone.js 的内置事件之一。

  • 另外,我们还可以只针对对象的个别属性进行监听:
var person = new Person({id: 1});

person.on("change:name", function() { // 现在只监听 name 属性是否变化
	alert("Your name has changed!");
});

将数据转化成 JSON 对象格式

var person = new Person({id: 1});

console.log(person.toJSON());

// or, maybe you need this:
console.log(JSON.stringify(person));

Better View

定义 View 的顶级标签(el)属性

var PersonView = Backbone.View.extend({
	tagName: "article",
	id: "personOne",
	class: "person",
	attributes: {
		title: this.model.get("name")
	},

	render: function() {
		var person = "<p>" + this.model.get("name") + "</p>";
		$(this.el).html(person);
	}
});

这将会生成下面这样的 HTML 片段:

<article id="personOne" class="person" title="Albert Yu">
	<p> Albert Yu </p>
</article>

缓存化的 jQuery 对象

像上面的 HTML 代码,如果要使用 jQuery 操作它们,那就像这样:

$("#personOne").html();

当然,Backbone.js 提供了更好的选择:

$(this.el).html();

甚至更好:

this.$el.html();

$el 是 Backbone.js 为 el 生成的 jQuery 对象的缓存,使其可以反复使用而不用担心产生多个 jQuery 的实例对象。

内置的模板引擎(underscore template)

之前的 render 方法可不怎么优雅,好在 Backbone.js 内置了 Underscore.js 库,其中包含了一个简洁的模板引擎:

var PersonView = Backbone.View.extend({
	tagName: "article",
	id: "personOne",
	class: "person",
	attributes: {
		title: this.model.get("name")
	},

	template: _.template('<p><%= name %></p>'),

	render: function() {
		data = this.model.toJSON();
		$(this.el).html(template(data));
	}
});

Cool! 这样就好看多啦~

如果内置模板引擎不合你的胃口,你当然可以选用其他的方案,比如说:

视图事件

Backbone.js 能够监视数据对象的变化并为其注册事件及回调函数,当然对于视图元素也是一样的,并且由于视图对象为我们提供了页面作用域的控制,使得我们可以更简单地和作用域内的对象交互而不至于发生混乱。

继续扩展上面的例子:

var PersonView = Backbone.View.extend({
	tagName: "article",
	id: "personOne",
	class: "person",
	attributes: {
		title: this.model.get("name")
	},

	template: _.template('<p><%= name %></p>'),

	render: function() {
		data = this.model.toJSON();
		$(this.el).html(template(data));
	},
	
	events: {
		"mouseover" : "showMore",
		"click p"	: "greeting"
	},

	showMore: function() {...},
	greeting: function() {...}
});

视图事件的语法结构是:"<event> <selector>: <method>",解读一下上面注册的两个事件:

  1. mouseover 事件被注册在作用域内的顶级标签上(即 el,也就是本例中的 <article></article>),于是 showMore 方法会在鼠标经过 this.el 标签的时候触发执行;

  2. 类似的,click 事件被注册在 <p></p> 标签上……等一下,哪一个 p 标签?

OK,这就是作用域的体现了,事实上 Backbone.js 执行此事件调用时的后台代码差不多是这样的:

$(this.el).delegate('p', 'click', greeting);

注:现在的 jQuery 早已经全面采用 on 方法来执行事件委托调用了,这个例子只是为了强调这是一种委托调用而已。

我们可以看到,事件的委托方法(delegate)执行在 this.$el 这个 jQuery 对象上,因此 p 就是被包裹在 this.el 之内的那一个;事实上你可以用完全兼容的 CSS 选择符来指定你要的元素,这和使用 jQuery 是一样的!只不过 Backbone.js 事先帮你选择了一个上级作用域,使得筛选工作变得更简单,更具针对性。

进阶:模型与视图的交互

是时候了解些复杂的东东了!首先我们根据目前所学,知道了 Backbone.js 的(部分)内部构造:

服务器 <=(获取)模型(数据)=> 视图(渲染)=> DOM

这个过程当然也可以反向回来形成一个回圈。

视图的变化通知模型

我们先让视图能够接受用户交互并产生变化

var PersonView = Backbone.View.extend({
	// ...
	input: _.template('<input type="text"><%= name %></input>'),
	submit: _.template('<button type="submit">Change Name</button>),

	render: function() {
		data = this.model.toJSON();
		$(this.el).html(input(data)).html(submit);
	}
});

现在咱们可以改名了,问题是模型如何知道数据发生了改变呢?这就需要视图通过事件通知它了:

var PersonView = Backbone.View.extend({
	// ...
	events: {
		"submit button": "updateName"
	}

	updateName: function() {
		newName = $(this).val(); // this = button!

		if (newName !== this.model.get('name')) {
			this.model.set({name: newName});
		} else {
			return;		
		}
	}
});

Well… 这么做的确能行,但问题是应该属于 Model 处理的逻辑现在散落在 View 当中,这样不太妥呀!没关系,我们重构一下,这一次 View 的工作仅仅是通知:

var PersonView = Backbone.View.extend({
	// ...
	events: {
		"submit button": "updateName"
	}

	updateName: function() {
		newName = $(this).val();
		this.model.updateName(newName);
	}
});

var Person = Backbone.Model.extend({
	// ...
	updateName: function(newName) {
		if (newName !== this.get('name')) {
			this.set({name: newName});
			this.save(); // 通知服务器保存
		} else {
			return;		
		}
	}
});

就像这样,我们通过参数传递把改变的值转交给 Model,再由 Model 做进一步的处理就是了。

现在,回路模型就像这样:

服务器 <=(更新)模型(处理)<=(通知)视图 <=(用户交互)DOM

不过事情还没有结束:如果模型的数据变化了,视图又如何知道呢?你或许立刻想到可以在视图完成通知之后立刻执行 render 方法:

var PersonView = Backbone.View.extend({
	// ...
	updateName: function() {
		newName = $(this).val();
		this.model.updateName(newName);
		this.render();
	}
});

嗯,好主意……不过它并不总是有用的,因为模型的值很有可能会在别处发生改变,比如在另外一个视图里。那么,要如何通知指定的视图响应模型数据的变化呢?

这件事情需要在视图实例化的时候就去做,让视图去监听模型的变化吧:

var PersonView = Backbone.View.extend({
	// ... 省略其他的代码,添加以下初始化代码
	initialize: function() {
		this.listenTo(this.model, "change", this.render);
		// this.model.on("change", this.render, this);
	}
});

被注释掉的那一行是以前的另外一种写法,也能达到目的,但是稍微难以理解一点——最后一个参数传递的是视图实例对象本身。

数据集合

收集多个模型数据

当模型的实例越来越多的时候,为了方便的处理它们,我们通常会想到用数组把它们集中起来。Backbone.js 提供了一个 Collection 模块来帮助我们简化这些事情:

var People = Backbone.Collection.extend({
	model: person // 之前已经生成的模型实例
});

看起来挺像 Model 的,不过这一次获得的数据都是数组了。我们需要用处理数组的办法来处理集合里的数据(当然,Backbone.js 提供了许多内置方法):

var people = new.People();

people.add([
	{ name: "John Doe", id: 1 },
	{ name: "Jane Smith", id: 2 }
]);

people.length // => 2

people.get(2) // => { name: "Jane Smith", id: 2 }

people.at(0) // => { name: "John Doe", id: 1 }

如果数据来自于服务器,那就需要指明 url

var People = Backbone.Collection.extend({
	model: Person,
	url: '/people'
});

var people = new.People();
people.fetch(); // 可以批量获取了

people.add(person1);

people.reset(); // reset 方法可以重置发生了变化的集合实例,确保里面的数据是完整的

集合事件

集合事件的注册及使用和模型非常相似,只不过集合多了几个专属的事件,这些差别可以通过查阅文档来获知。

我们可以在触发事件的时候传递 silent 参数进去,阻止事件回调函数的执行:

people.on("reset", function() {
	alert("You have " + this.length + " people now!");
});

people.fetch(); // will alert
people.fetch({silent: true}); // will not alert

集合视图

集合视图的故事也没什么特别之处,只不过这一次视图里要处理的是一组数据,所以免不了要渲染两个层级,一层是集合,一层是遍历集合里的模型:

var PeopleView = Backbone.View.extend({
	render: function() { // 集合视图的 render 当然要渲染整个集合了
		this.collection.forEach(this.addPerson, this)
	},

	addPerson: function(person) { // 这还是老的模型视图那一套
		var personView = new PersonView({model: person});
		this.$el.append(personView.render().el);
	}
});

var peopleView = PeopleView.new({
	collection: people; // people 是之前定义过的集合实例
})

监听集合的变化

Same old stories...

var PeopleView = Backbone.View.extend({
	initialize: {
		this.collection.on("change", this.render, this), // 集合发生了变化,比如 reset, fetch
		this.collection.on("add", this.addPerson, this), // 集合里添加了新的数据
		this.model.on("hide", this.removePerson, this)   // 集合里某一个数据被移除了
	},

	render: function() {
		this.collection.forEach(this.addPerson, this)
	},

	addPerson: function(person) {},
	
	removePerson: function() {
		this.$el.remove();
	}
});

...but not always as old as

前两个事件处理都不难理解,关键是第三个:

  1. 集合中的某个数据被移除了(注意,不是这个数据在服务器那里删除了,而是把它从集合中移出去了),这件事情发生在 collection.remove(item) 的时候,按道理我们应该监听 collection 的;
  2. 然而在视图里真正发生改变的仅仅是那个数据,而不是全部集合,换句话说,视图里执行回调函数接受的 this 是那个数据,而不是整个集合;
  3. 这就是为什么用 this.model.on 而不是 this.collection.on 的原因。但是——数据是被 collection.remove(item) 移出,而不是它自身的方法调用,我们监听 this.model.on 要怎么知道它确切发生了呢?

OK,这是一件稍微复杂一点的事情,在 collection 内部事实上发生了以下的变化:

var People = Backbone.Collection.extend({
	initialize: {
		this.on("remove", this.hideModel)
	},

	hideModel: function(model) {
		model.trigger("hide"); // 我们让 model 触发自定义事件 hide
	}
});

就这样,在视图里我们不监听 collection 的 remove 事件(因为我们不打算改变 collection),当 collection.remove(item) 发生的时候,collection 内部会让那个被移除的 item 触发它自己的自定义事件 hide。于是,我们就可以在视图里监听这个自定义事件,一旦监听到就说明 collection.remove(item) 确实发生了,然后就把 item 给 remove 掉。

让 App 飞~

So far so good, 但是我们始终在一页里面动作,这对用户来讲是远远不够的。如果 url 发生了改变会怎样?

在浏览器的世界里,使用 javascript 来控制 url 的历史记录有两种主要方式:

Hash mark(#people/1)

当有这样的链接存在时:

<a href="#people/1">Albert Yu</a>

点击后,浏览器的 url 会是这个样子:

http://www.example.com/#people/1

于是,我们可以获得用户的 url 历史记录并控制它们。但是,这样并不够好,我们真正想要的是这样的 url:

http://www.example.com/people/1

只不过若是没有任何处理的话,访问这样的 url 会造成浏览器刷新,所有数据重新载入,这就体现不出我们使用现代 javascript 技术的优势了。

Backbone.js 可以处理 Hash mark 的 url 历史记录,但我们重点要讲的是另外一种方式,来自新的 HTML5 API

HTML5 Push State API

使用 Push State,我们可以截取常规的 url 链接,然后专用 javascript 去处理它们,而不是让整个浏览器刷新。要启用对 Push State 的支持,我们需要调用 Backbone.js 的 history api:

Backbone.history.start({pushState: true});

要截取 url 的变化转交由 Backbone.js 来处理,我们就要注册 routes:

var App = Backbone.Router.extend({
	routes: {
		"people"				: "index", // /people
		"people/:id"			: "show", // people/1
		"help/:subject/p:page"	: "help" // help/intro/p3
	},

	index: function() {
		...
	},

	show: function(id) {
		...
	},

	help: function(subject, page) {
		...
	}
});

那些忽略了内部实现的方法,事实上就是之前我们所学到的一切,这要根据你的应用程序来定。比如说 index 方法,我们就是想要获取 people collection ,然后把它们都显示出来,于是:

var App = Backbone.Router.extend({
	...

	index: function() {
		var people = new People();
		people.fetch();
		var peopleView = new PeopleView({collection: people});
		$("#app").append(peopleView.render().el);
	},

	...
});

或者,我们可以直接初始化集合与视图的实例:

var App = Backbone.Router.extend({
	initialize: function() {
		this.people = new People();
		this.peopleView = new PeopleView({collection: this.people});
		$("#app").append(this.peopleView.el);
	},

	index: function() {
		this.people.fetch();
	},

	...
});

Wrap up!

重新组织一下我们应用的入口吧,与其创建一堆类然后再分别实例化,我们完全可以一起做了:

var App = new (Backbone.Router.extend({
	initialize: function() {
		this.people = new People();
		this.peopleView = new PeopleView({collection: this.people});
		$("#app").append(this.peopleView.el);
	},

	index: function() {
		this.people.fetch();
	},

	...

	start: function() { // 封装一下 Backbone.history API
		Backbone.history.start({pushState: true});
	}
}));

// 于是,我们可以这样初始化整个应用程序

$(function() { App.start(); });

最后,一休哥说了:“就到这里,再见吧!”

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?