Backbone之旅:前端MVC架构初体验(下)

《Backbone之旅:前端MVC架构初体验(上)》,上篇中最后的代码已经完全达到最初提出的几点要求,现在就结合Backbone提供的能力,来继续精简代码。最后的目标就是将上篇中的代码全部重构为BackboneMVC模式。

上篇中最后一次改造就已经使用到了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
35
var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* … */
TodoList.prototype.add = function(todoContent){
$.ajax({
url: '/add',
type: 'POST',
dataType: 'json',
data: { todoContent: todoContent },
success: function(data){
events.trigger('todo:add', data.todoContent);
}
});
};
/* … */
NewTodoView.prototype.addTodo = function(e){
e.preventDefault();

this.todoList.add($('#new-todo').find('textarea').val());
};
/* … */

接下来,咱看看在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中我们频繁使用到了jQueryfind()方法来查找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);
}
});
/* … */

由于BackboneView已经提供了我们实现的$()方法的能力,也叫$(这也是之前我们自己命名的原因);同时BackboneView也提供了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

而且,BackboneCollection已经支持了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
49
var 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()的方法,其可以根据CollectionModel属性创建一个Model的实例,并执行Modelsave()方法。所以我们的TodoList中的add()方法已经可以废去了。我们只需为TodoList提供Model属性的值即可,然后在NewTodoViewaddTodo()方法中,替换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('');
}
});
/* … */

这时,我们的ModelCollectionView都已经齐上阵了。由于BackboneView已经内置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 });
});

至此,完整的基于BackboneModelCollectionView模式就构建好了。如果说还有什么瑕疵的话,应该就是一些表层功夫了,那就是咱们的HTML Elementappend了,需要做一些过滤,比如用户输入JavaScript代码那就糟糕了。
1
2
3
this.$('ul').append('<li>' + todo.get('todoContent') + '</li>');
调整为
this.$('ul').append('<li>' + todo.escape('todoContent') + '</li>');

这样就Perfect了。文章忒长了点,但是为了从一个0变成一个1,我想应该还是很有意思的。