参考文档
Step01
虽然文章中要求下载所有外库到本地,但这里还是选择了比较快的引用外链,引用外链有挺多缺点的:多个HTTP请求影响性能,第三方库的安全性考虑等等,但这里为了方便用引用外链的方式。更多关于第三方库的探究在这里。同时还需要添加CSS文件。
<!doctype html>
<html lang="en">
<body>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.0/backbone-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone-localstorage.js/1.1.16/backbone.localStorage-min.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/collections/todos.js"></script>
<script src="js/views/todo-view.js"></script>
<script src="js/views/app-view.js"></script>
<script src="js/routers/router.js"></script>
<script src="js/app.js"></script>
</body>
</html>
Step02
定义HTML部分,输入框输入每一个事项,然后由列表显示,列表id="todo-list"
<!doctype html>
<html lang="en">
<body>
<section id="todoapp">
<header id="header">
<input id="new-todo" placeholder="What needs to be done?">
</header>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul>
</section>
</section>
</body>
</html>
Step03 添加模版
<script type="text/template" id="item-template">
<div class="view">
<input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
<label><%- title %></label>
<button class="destroy"></button>
</div>
<input class="edit" value="<%- title %>">
</script>
<%= <%-是来自underscore的模版 也可以自定义 接下来还需要定义#stats-template
<script type="text/template" id="stats-template">
<span class="todo-count"><strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<% if (completed) { %>
<button class="clear-completed">Clear completed</button>
<% } %>
</script>
Step04
模型(model)包括两个默认属性:标题和完成状态
此外还包括一个开关方法toggle
// js/models/todo.js
var app = app || {};
app.Todo = Backbone.View.extend({
default: {
title: '',
completed: false
},
toggle: function() {
this.save({
completed: !this.get('completed')
});
}
});
Model.save方法是通过代理Backbone.sync的方式,把一个模型保存到数据库中(或者是其他的存储数据层,这儿是浏览器的localStorage)。如果验证成功,它返回一个jqXHR对象,否则返回false。
可传递的参数包括两个:
model.save([attributes], [options])
attributes 是你需要修改的属性值,没有列出来的属性不会被修改,但是整个完整对象会一起传到服务端。
options可以接收success 和 error的回调处理:
model.save("author", "F.D.R.", {error: function(){ ... }});
Model.get方法则从model中读取相应的属性值。
所以这里当调用开关(toggle)方法时,我们读取完成的状态,然后取反,最后再把它保存。
Step05
待办事项集合(Todo collection) 是模型的集合。我们使用LocalStorage adapter将我们的数据存储在浏览器本地(localStorage)。
// js/collections/todos.js
var app = app || {};
var TodoList = Backbone.Collection.extend({
model: app.Todo,
localStorage: new Backbone.LocalStorage('todos-backbone'),
completed: function() {
return this.filter(function (todo) {
return todo.get('completed');
});
},
remaining: function () {
return this.without.apply(this, this.completed());
},
nextOrder: function () {
if (!this.length) { return 1; }
return this.last().get('order') + 1;
},
comparator: function (todo) {
return todo.get('order');
}
});
app.Todos = new TodoList();
集合的completed() 和 remaining()方法分别返回已完成的和未完成的事项。
下一个nextOrder方法按照顺序生成下一个事项。
comparator则按照添加顺序给它们排列。
P.S this.filter, this.without 和 this.last 都是underscore的方法。
Step06
视图(View)处理应用的逻辑并且负责展示。我们的应用包含两个视图:AppView负责首次渲染列表并且处理新增加的事项,TodoView则负责具体每一条事项的编辑更新和删除。
var app = app || {};
app.AppView = Backbone.View.extend({
el: '#todoapp',
statsTemplate: _.template($('#stats-template').html()),
initialize: function () {
this.listenTo(app.Todos, 'add', this.addOne);
this.listenTo(app.Todos, 'reset', this.addAll);
},
addOne: function(todo){
var view = new app.TodoView({model: todo});
$('#todo-list').append(view.render().el);
},
addAll: function(){
this.$('#todo-list').html('');
app.Todos.each(this.addOne, this);
}
});
AppView里面的逻辑比较多,所以我们分两步。第一步先添加statsTemplate和initialize方法,addOne和addAll方法。
el (element) 属性连接DOM结构里的id="todoapp"。
_.template是使用underscore的模版。
initialize方法里面:
当add事件被触发时,会调用addOne方法。addOne方法创建一个新的TodoView实例,然后把它添加到列表 id="todo-list"
<ul id="todo-list"></ul>
View.render方法 默认是no-op,我们用自己的逻辑重写覆盖它。 View.el可以连接DOM选择器字符串或者一个元素,其他时候会根据标签名类名ID或属性进行匹配。
Step07
光有这两个方法是不够的,我们还要再添加更多事件和方法。 事件包括:
events: {
'keypress #new-todo': 'createOnEnter',
'click #clear-completed': 'clearCompleted',
'click #toggle-all': 'toggleAllComplete'
},
当用户在输入框里点击回车按钮调用createOnEnter,此方法会新建一个model并且在把它保存在浏览器本地。app.Todos在集合文件里(js/collections/todos.js)被赋值:
app.Todos = new TodoList();
create方法是集合上新建一个model实例的便捷方式。
newAttributes: function(){
return {
title: this.$input.val().trim(),
order: app.Todos.nextOrder(),
completed: false
};
},
createOnEnter: function( event ) {
if ( event.which !== ENTER_KEY || !this.$input.val().trim() ) {
return;
}
app.Todos.create( this.newAttributes() );
this.$input.val('');
},
clearCompleted方法会隐藏所有已完成的事项。这个按钮选项是模版通过#stats-template渲染出来。 toggleAllComplete方法允许用户一次性将所有事项标记为完成。
clearCompleted: function(){
_.invoke(app.Todos.completed(), 'destroy');
return false;
},
toggleAllComplete: function(){
var completed = this.allCheckbox.checked;
app.Todos.each(function(todo){
todo.save({
'completed': completed
});
});
}
在初始化initialize方法里,我们给事件绑定一些回调方法:
this.listenTo(app.Todos, 'changed:completed', this.filterOne);
this.listenTo(app.Todos, 'filter', this.filterAll);
this.listenTo(app.Todos, 'all', this.render);
当我们单独过滤一个事项,我们调用filterOne方法;全部事项一起过滤时,我们调用filterAll方法。还有特殊的all事件绑定在事项集合上,下面会更详细讨论。
filterOne: function(){
todo.trigger('visible');
},
filterAll: function(){
app.Todos.each(this.filterOne, this);
},
初始化方法最后一步是从浏览器存储中读取之前保存的数据。
app.Todos.fetch();
在渲染方法里,同样做了很多逻辑处理:
render: function() {
var completed = app.Todos.completed().length;
var remaining = app.Todos.remaining().length;
if(app.Todos.length){
this.$main.show();
this.$footer.show();
this.$footer.html(this.statsTemplate({
completed: completed,
remaining: remaining
}));
this.$('#filter li a')
.removeClass('selected')
.filter('[href="#/' + (app.TodoFilter || '') + '"]')
.addClass('selected');
} else {
this.$main.hide();
this.$footer.hide();
}
this.allCheckbox.checked = !remaining;
},
#main和#footer部分会根据是否有事项列表来显示。我们会在路由中设置 app.TodoFilter的值,该值会决定selected这个类,进而影响样式的显示。
Step08 TodoView
var app = app || {};
app.TodoView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#item-template').html()),
events: {
'dbclick label': 'edit',
'keypress .edit': 'updateOnEnter',
'blur .edit': 'close'
},
initialize: function(){
this.listenTo(this.model, 'change', this.render);
},
render: function(){
this.$el.html(this.template(this.model.attributes));
this.$input = this.$('.edit');
return this;
},
edit: function(){
this.$el.addClass('editing');
this.$input.focus();
},
close: function(){
var value = this.$input.val().trim();
if(value){
this.model.save({title: value});
}
this.$el.removeClass('editing');
},
updateOnEnter: function(e){
if (e.which === ENTER_KEY){
this.close();
}
}
});
在initialize构造函数里,我们监听事项的变化。每当事项一更新,应用都会重新渲染并且在视图上表现出来。
在render() 方法里面, 我们使用 Underscore.js #item-template渲染。
我们的事件包括三个回调方法: edit将当前模式调整为编辑模式,允许用户修改事项的内容。 updateOnEnter检查用户是否按了回车键。 close会清理输入框中的内容,如果是有效信息,我们保存此前的修改。
Step09 Startup
现在我们有里两个视图 AppView 和 TodoView,我们要借助jQuery’s ready() 方法把 AppView 在app.js 文件中执行。 至此,一个初步的应用就已经出来了。我们可以在浏览器打开index.html页面来验证,在控制台执行一条添加指令。
app.Todos.create({ title: 'My first Todo item'});

如果没有什么差错,这条指令会在页面添加一条事项。同时它会保存在浏览器本地存储,页面刷新后也依然存在。
app.Todos.create()会执行集合上的create方法,它会初始化一个model事项。
// js/collections/todos.js
var TodoList = Backbone.Collection.extend({
model: app.Todo // collection.create() 用来创造的模型
...
)};
我们通过以下指令来验证新创造的模型是app.Todo 的实例:
var secondTodo = app.Todos.create({ title: 'My second Todo item'});
secondTodo instanceof app.Todo // returns true
Step10 继续完善
这一步我们要做到是,完成了某项任务和删除任务。 我们会添加两个方法togglecompleted() clear() 和相应的事件触发。 这两个事件都是针对具体的每一个事项,所以我们在TodoView里作相应添加。
首先添加两个事件:
events: {
'click .toggle': 'toggleCompleted',
'click .destroy': 'clear',
},
初始化方法中添加监听:
initialize: function(){
...
this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'visible', this.toggleVisible);
},
render方法中需要根据完成状态显示不同的列表:
render: function(){
this.$el.toggleClass('completed', this.model.get('completed'));
this.toggleVisible();
...
}
toggleVisible方法调用isHidden来判断任务是否完成,是否显示。
当我们点击了任务的叉按钮时,clear 方法会调用 destroy, 任务就会从本地存储中被删除。
toggleVisible: function(){
this.$el.toggleClass('hidden', this.isHidden());
},
isHidden: function(){
var isCompleted = this.model.get('completed');
return (
(!isCompleted && app.TodoFilter === 'completed')
|| (isCompleted && app.TodoFilter === 'active')
);
},
togglecompleted: function(){
this.model.toggle();
},
clear: function(){
this.model.destroy();
}
Step11 Routing
最后我们添加路由,根据路由可以快速过滤出我们想要的列表:
#/ (all - default)
#/active
#/completed
var app = app || {};
var Workspace = Backbone.Router.extend({
routes: {
'*filter': 'setFilter'
},
setFilter: function(param){
if (param) {
param = param.trim();
}
app.TodoFilter = param || '';
app.Todos.trigger('filter');
}
});
app.TodoRouter = new Workspace();
Backbone.history.start();