继《Backbone之旅:前端MVC架构初体验(上)》,上篇中最后的代码已经完全达到最初提出的几点要求,现在就结合Backbone
提供的能力,来继续精简代码。最后的目标就是将上篇中的代码全部重构为Backbone
的MVC
模式。
上篇中最后一次改造就已经使用到了callback
的方式,所以我们索性再加上Event
机制吧,因为Backbone
内置了这个能力。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35var events = _.clone(Backbone.Events);
var TodoList = function(){};
TodoList.prototype.add = function(options){
$.ajax({
url: '/add',
type: 'POST',
dataType: 'json',
data: { todoContent: options.todoContent },
success: options.success
});
};
var NewTodoView = function(options){
this.todoList = options.todoList;
events.on('todo:add', this.appendTodo, this);
events.on('todo:add', this.clearTextArea, this);
$('#new-todo form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add({
todoContent: $('#new-todo').find('textarea').val(),
success: function(data){
events.trigger('todo:add', data.todoContent);
}
});
};
/*后面不变*/
现在既然调用add()
时传入的success
属性已经完全不涉及到DOM
操作了,而是单纯的事件触发,那完全可以把这个行为放置到TodoList
原型的add()
方法中去了,这样重用性更高。
1 | /* … */ |
接下来,咱看看在NewTodoView
这个视图中事件订阅所触发的对应方法appendTodo()
和clearTextArea()
中,涉及到的是处在同一级别的不同的DOM
元素节点,也就是说在NewTodoView
这个视图中,我们处理了两个DOM
元素,这似乎和我们之前提到的“单一职责原则”相违背了,所以还有待进一步的改进。
我们分别把新增Todo
的视图和负责展示Todo Item
的视图分开定义,使其符合“单一职责原则”。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34/* 前面不变 */
var NewTodoView = function(options){
this.todoList = options.todoList;
events.on('todo:add', this.clearTextArea, this);
$('#new-todo form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add($('#new-todo').find('textarea').val());
};
NewTodoView.prototype.clearTextArea = function(){
$('#new-todo').find('textarea').val('');
};
/* 用于展示Todo Item */
var TodoView = function(){
events.on('todo:add', this.appendTodo, this);
};
TodoView.prototype.appendTodo = function(todoContent){
$('#todo-list ul').append('<li>' + todoContent + '</li>');
};
/* 应用程序启动 */
$(function(){
var todoList = new TodoList();
new NewTodoView({ todoList: todoList });
new TodoView();
});
现在每个View
里面只依赖一个顶层的HTML Element
了,而在各自的View
里面多次使用到了$('#new-todo')
这样的代码,所以干脆将其在初始化的时候作为View
的一个属性来提供吧。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29/* 前面依旧不变 */
var NewTodoView = function(options){
this.todoList = options.todoList;
this.el = $('#new-todo'); //定义一个el属性ß
events.on('todo:add', this.clearTextArea, this);
this.el.find('form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add(this.el.find('textarea').val());
};
NewTodoView.prototype.clearTextArea = function(){
this.el.find('textarea').val('');
};
var TodoView = function(){
this.el = $('#todo-list');
events.on('todo:add', this.appendTodo, this);
};
TodoView.prototype.appendTodo = function(todoContent){
this.el.find('ul').append('<li>' + todoContent + '</li>');
};
/* 后面不变 */
此时观察发现,两个View
当中还保留着对DOM
节点的依赖,其重用度依然不高,于是可采用实例化View
的时候传入el
参数来解决这个问题。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* 前面不变 */
var NewTodoView = function(options){
this.todoList = options.todoList;
this.el = options.el;
events.on('todo:add', this.clearTextArea, this);
this.el.find('form').submit($.proxy(this.addTodo, this));
};
/* NewTodoView的原型方法也不变 */
var TodoView = function(options){
this.el = options.el;
events.on('todo:add', this.appendTodo, this);
};
/* TodoView的原型方法也不变 */
/* 初始化View的时候传入el */
$(function(){
var todoList = new TodoList();
new NewTodoView({ el: $('#new-todo'), todoList: todoList });
new TodoView({ el: $('#todo-list') });
});View
中我们频繁使用到了jQuery
的find()
方法来查找View
所在el
下面的子元素,所以可以考虑将这作为View
的特性来提供,于是我们为View
定义这样一个名叫$
方法,然后替换掉this.el.find()
这样的写法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37/* … */
var NewTodoView = function(options){
this.todoList = options.todoList;
this.el = options.el;
events.on('todo:add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
};
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();
this.todoList.add(this.$('textarea').val());
};
NewTodoView.prototype.clearTextArea = function(){
this.$('textarea').val('');
};
NewTodoView.prototype.$ = function(selector){
return this.el.find(selector);
};
var TodoView = function(options){
this.el = options.el;
events.on('todo:add', this.appendTodo, this);
};
TodoView.prototype.appendTodo = function(todoContent){
this.$('ul').append('<li>' + todoContent + '</li>');
};
TodoView.prototype.$ = function(selector){
return this.el.find(selector);
};
/* … */
上面的代码越来越多了,看上去好像咱是干的坏事,而不是往好的方向发展啊。是的,如果每个View
都有很多自己的特性(方法),那向上面这样着实太痛苦了。看样子是时候请出Backbone
提供的View
特性了。OK,把我们自己的View
转移到Backbone
的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/* … */
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
this.el = options.el;
events.on('todo:add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.todoList.add(this.$('textarea').val());
},
clearTextArea: function(){
this.$('textarea').val('');
},
$: function(selector){
return this.el.find(selector);
}
});
var TodoView = Backbone.View.extend({
initialize: function(options){
this.el = options.el;
events.on('todo:add', this.appendTodo, this);
},
appendTodo: function(todoContent){
this.$('ul').append('<li>' + todoContent + '</li>');
},
$: function(selector){
return this.el.find(selector);
}
});
/* … */
由于Backbone
的View
已经提供了我们实现的$()
方法的能力,也叫$
(这也是之前我们自己命名的原因);同时Backbone
的View
也提供了this.el
的能力,所以可以把它们从代码中显示的移除了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/* … */
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
events.on('todo:add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.todoList.add(this.$('textarea').val());
},
clearTextArea: function(){
this.$('textarea').val('');
}
});
var TodoView = Backbone.View.extend({
initialize: function(options){
events.on('todo:add', this.appendTodo, this);
},
appendTodo: function(todoContent){
this.$('ul').append('<li>' + todoContent + '</li>');
}
});
/* 启动代码依然不变 */
现在可以回过头来看看ajax
那部分了,由于Backbone
提供了Model
的能力,这个就是用于和服务端打交道的,所以将长长的ajax
代码改写为这一方式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/* … */
var Todo = Backbone.Model.extend({
url: '/add'
});
var TodoList = function(){};
TodoList.prototype.add = function(todoContent){
var todo = new Todo();
todo.save({ todoContent: todoContent },{
success: function(model, data){
events.trigger('todo:add', data.todoContent);
}
});
};
/* … */
同时,Backbone
中还提供了一个Collection
的概念,也就是Model
的集合,比如我们这个案例中,每次创建单条的Todo
,然后形成Todo List
。当然,我们的任何数据都应该是以多条记录的方式存在的。所以,我们同时将上面的TodoList
的实现改为Collection
。
而且,Backbone
的Collection
已经支持了Event
机制,所以我们也无需自定义events
了,于是开头的events
变量也一并移除了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49var Todo = Backbone.Model.extend({
url: '/add'
});
var TodoList = Backbone.Collection.extend({
add: function(todoContent){
var todo = new Todo(),
that = this;
todo.save({ todoContent: todoContent },{
success: function(model, data){
that.trigger('add', data.todoContent);
}
});
}
});
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
this.todoList.on('add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.todoList.add(this.$('textarea').val());
},
clearTextArea: function(){
this.$('textarea').val('');
}
});
var TodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
this.todoList.on('add', this.appendTodo, this);
},
appendTodo: function(todoContent){
this.$('ul').append('<li>' + todoContent + '</li>');
}
});
$(function(){
var todoList = new TodoList();
new NewTodoView({ el: $('#new-todo'), todoList: todoList });
new TodoView({ el: $('#todo-list'), todoList: todoList });
});Collection
提供了一个名叫create()
的方法,其可以根据Collection
的Model
属性创建一个Model
的实例,并执行Model
的save()
方法。所以我们的TodoList
中的add()
方法已经可以废去了。我们只需为TodoList
提供Model
属性的值即可,然后在NewTodoView
的addTodo()
方法中,替换this.todoList.add()
方法为this.todoList.create()
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* … */
var TodoList = Backbone.Collection.extend({
model: Todo
});
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.todoList = options.todoList;
this.todoList.on('add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.todoList.create({ todoContent: this.$('textarea').val() }); //替换为create方法
},
clearTextArea: function(){
this.$('textarea').val('');
}
});
/* … */
这时,我们的Model
、Collection
、View
都已经齐上阵了。由于Backbone
的View
已经内置collection
属性,使得我们可以设置、获取View
对应的Collection
,所以我们完全无需手动在View
的内部来定义一个todoList
的变量了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/* … */
var NewTodoView = Backbone.View.extend({
initialize: function(options){
this.collection.on('add', this.clearTextArea, this);
this.$('form').submit($.proxy(this.addTodo, this));
},
addTodo: function(e){
e.preventDefault();
this.collection.create({ todoContent: this.$('textarea').val() });
},
clearTextArea: function(){
this.$('textarea').val('');
}
});
var TodoView = Backbone.View.extend({
initialize: function(options){
this.collection.on('add', this.appendTodo, this);
},
appendTodo: function(todo){
this.$('ul').append('<li>' + todo.get('todoContent') + '</li>');
}
});
$(function(){
var todoList = new TodoList();
new NewTodoView({ el: $('#new-todo'), collection: todoList });
new TodoView({ el: $('#todo-list'), collection: todoList });
});
至此,完整的基于Backbone
的Model
、Collection
、View
模式就构建好了。如果说还有什么瑕疵的话,应该就是一些表层功夫了,那就是咱们的HTML Element
的append
了,需要做一些过滤,比如用户输入JavaScript
代码那就糟糕了。1
2
3this.$('ul').append('<li>' + todo.get('todoContent') + '</li>');
调整为
this.$('ul').append('<li>' + todo.escape('todoContent') + '</li>');
这样就Perfect了。文章忒长了点,但是为了从一个0
变成一个1
,我想应该还是很有意思的。