应用分层 & 解耦合 Model & Collection & View


21 April 2013

原文:Angry Birds of JavaScript: Black Bird - Backbone

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();
});
});
view raw jquery-worms.js hosted with ❤ by GitHub

你发现了问题了吗?编写类似上面的代码非常诱人,但我希望你能看到,协同和维护这样的代码将是一种负担。不过不要担心,我们都写过类似上面的代码。好消息是我们不必再用那种方式写代码。让我们来看看 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()
});
});
});
view raw main.js hosted with ❤ by GitHub

上面的代码定义了 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;
});
view raw search.js hosted with ❤ by GitHub

下面的 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;
});
view raw movie.js hosted with ❤ by GitHub

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;
});
view raw movies.js hosted with ❤ by GitHub

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;
});
view raw search-view.js hosted with ❤ by GitHub

下面的 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;
});
view raw movie-view.js hosted with ❤ by GitHub

下面是模板文件,假如你正疑惑的话。我使用了 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 获得。

7. 攻击!

下面是一个用 boxbox 构建的简版 Angry Birds,boxbox 是一个用于 box2dweb 的框架,由 BocoupGreg 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

posted @ 2013-10-19 12:05  agile30353  阅读(154)  评论(0编辑  收藏  举报