应用分层 & 解耦合 Model & Collection & View
目录
1. 简介 ⬆
一群无法无天的猪从无辜的小鸟那里偷走了所有的前端架构,现在小鸟们要把它们夺回来!一队特殊的小鸟英雄将攻击这些卑鄙的猪,直到夺回原本属于它们的前端 JavaScript 架构!
小鸟们最终会成功吗?它们会打败那些培根味儿的敌人吗?让我们一起揭示 JavaScript 之愤怒的小鸟系列的另一个扣人心弦的章节!
译注:翻译“bacon flavored foe”时,想起来了《少林足球》里的“做人如果没有梦想,那跟咸鱼有什么区别?”,就翻译成了“咸猪敌人”,@sunnylost 建议翻译为“培根味儿的敌人”,应该更准确和有趣些。
阅读系列介绍文章,查看所有小鸟以及它们的攻击力。
1.1 战报 ⬆
1.2 黑鸟的攻击 ⬆
在这边文章中,我们将看看黑色小鸟,它用有组织条理的 Backbone.js 炸弹对付这些小肥猪。渐渐的,小鸟们将一个接一个地夺回属于它们的东西!
2. 猪偷走了什么? ⬆
小鸟们过去经常把 jQuery 代码写得像是一个令人纠结的蠕虫大杂烩。它们把视图、模型、展现逻辑混淆在了一起。过了一段时间之后,一只黑色小鸟,它们的祖先之一,引入了 Backbone.js 库,并向小鸟们展示了一种思考 Web 前端应用开发的不同方式。然而,在最近的入侵中,猪群从小鸟哪里偷走了 Backbone.js,并把它抬回了它们那肮脏的猪圈。
一只黑色小鸟被派去夺回被盗的 Backbone.js。它将用爆炸性的组织之力(power of organization)摧毁猪群,夺回属于它们的东西。
3. 纠结的蠕虫大杂烩 ⬆
让我们再看看下面的应用,Blue Bird 在之前的攻击中已经处理过。通过增加消息可以理清混乱,但这里要介绍的是如何使用 Backbone.js 达到同样的目的。下面是正在运行的程序...
有时 Plunker 不能正确的嵌入。这个应用是一个简单的 Netflix 搜索接口,会展示 Netflix 返回的结果。如果 Plunker 不能开始工作,我会把这个演示很快转移到别处。很抱歉给你带来不便。
再次提醒你,这是上面的 Web 应用的实现代码。你应该注意的是,很多关注点被混合在了一起(DOM 事件、修改视图、Ajax 通信等)
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 |
$( document ).on( "click", ".term", function( e ) {
e.preventDefault();
$( "input" ).val( $( this ).text() );
$( "button" ).trigger( "click" );
});
$( document ).ready( function() {
$( "button" ).on( "click", function( e ) {
var searchTerm = $( "input" ).val();
var url = "http://odata.netflix.com/Catalog/Titles?$filter=substringof('" +
escape( searchTerm ) + "',Name)&$callback=callback&$format=json";
$( ".help-block" ).html( function( index, html ) {
return e.originalEvent ? html + ", " + "<a href='#' class='term'>" +
searchTerm + "</a>" : html;
});
$.ajax({
dataType: "jsonp",
url: url,
jsonpCallback: "callback",
success: function( data ) {
var rows = [];
$.each( data.d.results, function( index, result ) {
var row = "";
if ( result.Rating && result.ReleaseYear ) {
row += "<td>" + result.Name + "</td>";
row += "<td>" + result.Rating + "</td>";
row += "<td>" + result.ReleaseYear + "</td>";
row = "<tr>" + row + "</tr>";
rows.push( row );
}
});
$( "table" ).show().find( "tbody" ).html( rows.join( "" ) );
}
});
e.preventDefault();
});
});
|
你发现了问题了吗?编写类似上面的代码非常诱人,但我希望你能看到,协同和维护这样的代码将是一种负担。不过不要担心,我们都写过类似上面的代码。好消息是我们不必再用那种方式写代码。让我们来看看 Backbone.js 是什么,以及在这种情况下它如何帮助我们。
还有很多其他的前端 MV* 框架(Knockout、AngularJS、EmberJS 及其他)可以结构化上面的代码。我鼓励你选择一个工具,使用并适应它。
4. Backbone.js 基础 ⬆
Backbone.js has several pieces that can all work together to make a web application. You don't have to use all of these components, but they are available if you choose to use them. Backbone.js 含有几个组件,它们可以协同合作构建一个 Web 应用。我不必使用所有这些组件,但是你如果选择使用它们,它们就是有用的。
- 模型 Model - 表示数据,以及数据相关的逻辑
- 集合 Collection - 模型的有序集合
- 视图 View - 一个模块,带有渲染方法、依赖一个模型
- 路由 Router - 提供可链接和可分享 URL 的机制
- 事件 Event - 观察者事件模型
- 路由器 History - 提供操作历史的能力(支持后退按钮)
- 同步 - 可扩展组件,提供与服务端的 RESTful 通信
5. 重构紧耦合代码 ⬆
让我们试着重构上面的 jQuery 代码,并且使用 Backbone.js 分离各种关注点。
在这篇文章中,我不打算涉及所有上述组件,只会专注于 3 个主要组件(模型、集合、视图)。我会涉及到一些 Sync 组件,但它是作为其他主题的一部分出现。如果你想深入钻研这些主题,我在末尾列出了一些资源。
5.1 RequireJS ⬆
在我们开始模型、集合和视图之前,我想先展示如何使用 RequireJS ,来帮助我们从 index.html 页面中提取出所有脚本。
如果你之前从没见过 RequireJS,你可能需要阅读之前关于 RequireJS 的愤怒的黄色小鸟一文。
main.js ⬆
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 |
require.config({
paths: {
jquery: "https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min",
underscore: "http://underscorejs.org/underscore",
backbone: "http://backbonejs.org/backbone",
postal: "http://cdnjs.cloudflare.com/ajax/libs/postal.js/0.8.2/postal.min",
bootstrap: "http://netdna.bootstrapcdn.com/twitter-bootstrap/2.2.0/js/bootstrap.min"
},
shim: {
underscore: {
exports: "_"
},
backbone: {
deps: [ "jquery", "underscore" ],
exports: "Backbone"
},
bootstrap: {
"deps" : [ "jquery" ]
}
}
});
require( [ "jquery", "search-view", "search", "movie-view", "movies" ],
function( $, SearchView, Search, MovieView, Movies ) {
$( document ).ready( function() {
var searchView = new SearchView({
el: $( "#search" ),
model: new Search()
});
var movieView = new MovieView({
el: $( "#output" ),
collection: new Movies()
});
});
});
|
上面的代码定义了 jQuery、Underscore、Backbone、Postal 和 Bootstrap 的路径。我们需要为 Underscore、Backbone、Bootstrap 使用垫片,因为它们没有被定义为 AMD 模块。
然后,函数 require
被调用,用来在回调函数被调用之前请求一组依赖库。到那时,jQuery 和所有其他视图和模型将准备就绪。
5.2 模型 ⬆
我们将创建 2 个模型(Seach 和 Movie)来表示上面的应用。
下面的 Search 模型相当简单,它的主要任务是响应 term
属性的变化。我们使用 Backbone 的事件(观察者事件)来监听模型的变化,然后传播消息到 Postal.js(中介事件)。关于这些术语的更多信息以及它们的不同之处,可以参考关于事件的愤怒的蓝色小鸟一文。
search.js ⬆
1 2 3 4 5 6 7 8 9 10 11 |
define( [ "backbone", "channels" ], function( Backbone, channels ) {
var Model = Backbone.Model.extend({
initialize: function() {
this.on( "change:term", function( model, value ) {
channels.bus.publish( "search.term.changed", { term: value } );
});
}
});
return Model;
});
|
下面的 Movie 模型也没有很多事情要做。它的主要目的是解析服务端返回的数据,并把结果映射为更易于管理的结构。这样我们只需要关心 releaseYear
, rating
, 和 name
属性。
movie.js ⬆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
define( [ "backbone" ], function( Backbone ) {
var Model = Backbone.Model.extend({
defaults: { term: "" },
parse: function( data, xhr ) {
return {
releaseYear: data.ReleaseYear,
rating: data.Rating,
name: data.Name
};
}
});
return Model;
});
|
5.3 集合 ⬆
正如我们在上面描述的,集合只是一组模型。下面的代码也只是一组 Movie 模型。集合定义了从服务器的哪个位置获取模型列表。这个应用的后端是 Netflix,并且稍微有点复杂,因为我们需要用一个函数来动态建立 URL。我们还定义了一个 parse
方法,用来直接获取内容数组,内容将被映射到 Movie
模型。因为这个 Ajax 使用的是 JSONP,我们还需要覆盖 sync
方法提供一些额外选项。
movies.js ⬆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
define( [ "backbone", "movie" ], function( Backbone, Movie ) {
var Collection = Backbone.Collection.extend({
model: Movie,
url: function() {
return "http://odata.netflix.com/Catalog/Titles?$filter=substringof('" +
escape( this.term ) + "',Name)&$callback=callback&$format=json";
},
parse : function( data, xhr ) {
return data.d.results;
},
sync: function( method, model, options ) {
options.dataType = "jsonp";
options.jsonpCallback = "callback";
return Backbone.sync( method, model, options );
}
});
return Collection;
});
|
5.4 视图 ⬆
I see the View as more of a Presenter than the typical MVC View you might normally think of. Anyway, We have 2 views in this application that we will briefly look at. 我认为视图更像一个 Presenter,与通常认为的传统 MVC 视图相比。这个应用有 2 个视图,我们简要地看看。
下面的 SearchView
处理与 DOM 和模型的交互。属性 events
主要用于监听 DOM 事件,在这里是等待点击按钮或点击搜索记录链接。这些元素的改变将存在在模型中,属性名为 term
。方法 initialize
设置了一些事件来监听属性 term
的变化。如果 term
发生变化,部分 UI 会相应的发生变化。
search-view.js ⬆
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 |
define( [ "jquery", "backbone", "underscore", "channels" ],
function( $, Backbone ) {
var View = Backbone.View.extend({
events: {
"click button": "searchByInput",
"click .term": "searchByHistory"
},
initialize: function() {
this.model.on( "change", this.updateHistory, this );
this.model.on( "change", this.updateInput, this );
},
searchByInput: function( e ) {
e.preventDefault();
this.model.set( "term", this.$( "input" ).val() );
},
searchByHistory: function( e ) {
var text = $( e.target ).text();
this.model.set( "term", text );
},
updateHistory: function() {
var that = this;
this.$el.find( ".help-block" ).html( function(index, html) {
var term = that.model.get( "term" );
return ~html.indexOf( term ) ? html :
html + ", " + "<a href='#' class='term'>" + term + "</a>";
});
},
updateInput: function() {
this.$el.find( "input" ).val( this.model.get("term") );
}
});
return View;
});
|
下面的 MovieView
与上面的视图略有不同。首先要指出的是怪异的 text!movie-template.html
。我使用了 RequireJS 的 text.js
插件,允许将文本资源作为依赖练的一部分。这对于使用标记文件的场景很有用,可能是使用模板引擎时,也可能是使用与某个特定组件有关的 CSS 文件时。在方法 initialize
内部,我们订阅了 term 的变化消息,然后要求集合从服务器获取(fetch
)信息。从服务器检索到数据之后,方法 render
被调用,并且使用 Underscore 将结果模板化为 DOM。
movie-view.js ⬆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
define( [ "jquery", "backbone", "underscore", "channels", "text!movie-template.html" ],
function( $, Backbone, _, channels, template ) {
var View = Backbone.View.extend({
template: _.template( template ),
initialize: function() {
var that = this;
_.bindAll( this, "render" );
channels.bus.subscribe( "search.term.changed", function( data ) {
that.collection.term = data.term;
that.collection.fetch({
success: that.render
});
});
},
render: function() {
var html = this.template({ movies: this.collection.toJSON() });
this.$el.show().find( "tbody" ).html( html );
}
});
return View;
});
|
下面是模板文件,假如你正疑惑的话。我使用了 Underscore 的模板引擎,类似于 John Resig 在数年前写的的 Micro-Templating 实现。还有其他可用的模板库,我选择这个是因为它所在的 Underscore 是 Backbone 的先决条件。如果我需要更多功能,我会选择 Handlebars,不过,那是另一只愤怒的小鸟的故事了 ;)
movie-template.html ⬆
1 2 3 4 5 6 7 |
<% _.each( movies, function( movie ) { %>
<tr>
<td><%= movie.name %></td>
<td><%= movie.rating %></td>
<td><%= movie.releaseYear %></td>
</tr>
<% }); %>
|
6. 附加资源 ⬆
关于 Backbone.js 可以做的事情,我只涉及到一些皮毛。如果有兴趣了解关于这些概念的更多信息,看看下面的资源。
下面的资源从博客文章 Beginner HTML5, JavaScript, jQuery, Backbone, and CSS3 Resources 获得。
- Backbone.js API
- Annotated Backbone.js Code
- Backbone Extensions, Plugins, & Resources
- Backbone Boilerplate
- Backbone Fundamentals eBook by Addy Osmani(@addyosmani)
- Peep Code: Backbone.js Video Series by Geoffery Grosenbach(@topfunky) and David Goodlad(@dgoodlad)
- The Pragmatic Bookshelf: Hands-on Backbone.js by Derick Bailey(@derickbailey)
- Backbone.js Screencasts by Joey Beninghove
- Pluralsight: Backbone.js Fundamentals by Liam McLennan(@liammclennan)
- The Skinny on BackboneJS by Ben Howdle(@benhowdle)
- Backbone Tutorials
- Backbone.js Tutorials via Nettuts
- Exploring Backbone.js Series by Jack Franklin(@jack_franklin)
7. 攻击! ⬆
下面是一个用 boxbox 构建的简版 Angry Birds,boxbox 是一个用于 box2dweb 的框架,由 Bocoup 的 Greg Smith 编写。
按下
空格键
来发射黑色小鸟,你也可以使用方向键。
<img border="0" src="http://4.bp.blogspot.com/-QBX6E-gJmzE/UVuGtHDEmMI/AAAAAAAAZuM/xXbC6KMc3OU/s1600/black-brid-game.png" />
8. 结论 ⬆
Web 前端应用可以快速变得相当复杂。在你知道这点之前,如果你不小心就会导致关系混乱。值得庆幸的是,Backbone.js 提供了组件来帮助你把应用分割为可用的小模块,并且每个小模块各司其职。感谢黑色小鸟为小鸟们夺回了 Backbone。事情被有条理地组织起来,并安排在合适的位置上,现在,小鸟们可以安心休息了。
还有许多其他的前端架构技术被猪群偷走了。在下篇文章中,另一只愤怒的小鸟将继续复仇!Dun, dun, daaaaaaa!
@sunnylost 补充:Dun, dun, daaaaaaaaaa! 应该是在模拟背景音乐,类似于这种 http://missingno.ocremix.org/musicpages/game_on.html