锻造车间 - Backbone.js

作者:Reuven M. Lerner

JavaScript 正在变化。实际上,我不确定这在多大程度上是真实的;多年来,底层语言并没有发生太大变化。但是,即使语言本身没有改变,关于它的一切都在改变。人们对服务器端 JavaScript 的兴趣日益浓厚,基于 Node.JS 的高速应用程序(如上个月的专栏所述)。基于浏览器的 JavaScript 不仅非常标准,而且执行效率也非常高。当然,还存在许多高质量的开源 JavaScript 库,例如 jQuery、MooTools 和 Prototype,它们使在浏览器中使用 JavaScript 变得容易。

那么,我们的 JavaScript 恶魔是否已被永远驱除?完全没有。随着应用程序迁移到浏览器,人们对使它们更像桌面应用程序的兴趣日益浓厚。当然,JavaScript UI 库使实现桌面式功能(例如拖放)相对容易。但是,如果您想创建一个真正复杂、类似桌面的应用程序,您会发现自己迷失在事件回调的森林中,更不用说可能不适合此类应用程序的小部件了。

因此,在过去一两年中,出现了一种新型 Web 应用程序也就不足为奇了——这种应用程序几乎完全用 JavaScript 编写,在浏览器内部执行,并且仅偶尔与服务器联系。这颠覆了通常的 Web 开发模型——在这种模型中,大部分处理发生在服务器上,发出 HTML 和 JavaScript 来处理事情,直到下一次调用服务器——使服务器几乎只是一个存储设施,用于存储和检索由浏览器应用程序确定的信息。

您可能会争辩说,Google 地图、Gmail 和 Google 文档——选择这三个著名的例子,但绝不是唯一的例子——已经展示了这种能力好几年了。但是直到最近,普通开发人员创建严重依赖 JavaScript 的应用程序还是相对困难的。

幸运的是,情况已经发生了变化,而且变化很大。如果您想创建一个丰富的 JavaScript 应用程序,您有各种工具包可供选择。问题不再是您是否可以创建这样的应用程序,而是您将使用哪些工具来创建它以及它将如何与服务器通信。仅仅凭我的记忆,我可以回忆起 Backbone.js、Knockout、JavaScript MVC、SproutCore、Closure 和 Cappuccino,您可以确信我只提到了现有工具包的一小部分。如今,这可能是不言而喻的,但我应该补充一点,领先的工具包都以开源许可证发布,这使得下载、试用和探索这些库成为可能,而无需担心下载或部署它们时的许可限制。

本月,我将开始撰写一系列关于这些浏览器内应用程序开发框架以及如何使用它们创建更丰富、更有趣的 Web 应用程序的专栏文章。在每篇文章中,我将探讨框架的入门难易程度、其相对优势和劣势,并讨论您可能如何使其与服务器上的数据交互。

在过去的十年中,我们已经看到服务器端提供 RESTful API 的 MVC 框架的明显趋势。Ruby on Rails 并非唯一推广这种开发风格的框架,但它肯定在这些方向上大力推动了开发人员,使得非 REST 和非 MVC 开发令人沮丧地困难。事实证明,许多新的、现代的 JavaScript 框架也采用了 MVC 模型,每个框架都有自己的方式,并且始终与 Rails 开发人员可能期望的服务器端模型存在差异。

在浏览器和服务器上使用 MVC(我喜欢称之为 MVC 平方,但这可能只是我个人看法)将 Web 应用程序变成了两个独立的软件系统:一个在服务器上,基本上向世界公开 RESTful JSON API,另一个在浏览器中,从服务器使用 RESTful JSON API。将程序分解为这两个部分,可以更轻松地将开发工作分配给两个人或团队,并以更智能的方式组织代码。在未来的几个月里,当我将 JavaScript 应用程序连接到后端存储系统时,我将对此进行更多说明。

本月,我将初步了解 Backbone.js,这是一个非常小的 JavaScript 库,最近受到了相当多的媒体关注。并且,我将解释如何使用 Backbone.js 构建一个简单的应用程序,创建完全存在于浏览器中的功能。

基础知识

正如我上面指出的,Backbone.js 遵循模型-视图-控制器 (MVC) 范例,该范例已被软件开发人员使用了几十年,并且在过去几年中已在服务器端 Web 开发中变得无处不在。MVC 应用程序有三个不同的部分:模型(提供数据本身的接口)、视图(向用户呈现信息)和控制器(将用户的请求定向到正确的模型,然后在视图中呈现结果)。通过沿着这些思路划分程序逻辑,每个功能应该放在哪里变得相当明显,从而使代码更易于维护。

在 Backbone.js 的 MVC 世界中,拆分方式类似。您在模型对象中检索和存储数据,在控制器对象中定义单个方法(和 URL 路由),视图在用户的浏览器中显示内容。

但是,如果您来自服务器端世界,则服务器端和客户端 MVC 之间存在一些细微(和非细微)的差异。首先,在服务器端程序中,模型从数据库(关系型或非关系型)检索数据是非常清楚的。相比之下,JavaScript 应用程序中的模型可以来自...嗯,它可以来自各种来源,其中之一是服务器端 Web 应用程序。下个月我将更深入地研究这一点;对于我本月的示例,让我们假设数据已经在 JavaScript 中,不需要从任何地方加载。

在服务器端应用程序中,视图实际上是 HTML、CSS 和 JavaScript 的组合,而不是单个文件或格式。实际上,视图不必是 HTML;它也可以是 XML、JSON 或各种其他格式,从 CSV 到 PDF 到图像。相比之下,Backbone.js 应用程序中的视图通常会重写当前页面的单个部分,而不是加载全新的页面。

考虑到这一点,让我们创建一个基本的 Backbone.js 应用程序。我已决定加入社交潮流,开发一个微型应用程序,让人们查看食谱标题列表,单击听起来有趣的标题,然后阅读相关食谱的内容。相同的原理可以应用于地址簿、日记甚至格式不寻常的博客。

因此,让我们从数据模型开始。在 Ruby on Rails 中(使用 ActiveRecord)创建数据模型很容易。您定义 ActiveRecord 的子类,从而继承其所有功能。当然,JavaScript 没有具有类和继承的传统对象模型,因此 Backbone.js 需要使用不同的范例。相反,您在 Backbone.js 中所做的是在 Backbone.Model 上调用“extend”函数。传递给 Backbone.Model.extend 的属性要么被视为数据字段,要么被视为方法,具体取决于它们是数据还是函数。例如,如果您想对单个约会进行建模,您可以按如下方式操作

Appointment = Backbone.Model.extend({
    person: null,
    meeting_at: null,
    note: null
});

请注意,您还可以定义一个“initialize”属性,它将取代默认的构造函数方法。在这种特殊情况下,我不打算做任何花哨的事情,这意味着我可以使用默认值。要创建一个新的约会,您可以说

var new_appointment =
new Appointment({person: 'Barak Obama',
         meeting_at: '2011-jul-14',
         note: 'Meet with the president'});

您还可以替换约会中的单个属性

new_appointment.set({person: 'Joe Biden'});

或者,您可以从约会中检索属性

new_appointment.get('person');
集合和控制器

当然,大多数人都必须安排多个约会,这意味着此示例程序需要跟踪多个约会。现在,您通常可能会假设您可以简单地将多个约会存储在 JavaScript 数组中。但是,在 Backbone.js 的世界中,您实际上使用一种特殊的对象(称为集合)来存储约会。

为什么要使用集合而不是简单的数组?主要是因为它与 Backbone.js 中的其他项目协同工作。例如,您可以设置这样的内容:每当您向集合添加或删除元素时,它都会自动调用另一个方法。另一方面,集合对象合并了用于 JavaScript 的 Underscore 库,该库定义了许多来自函数式编程的方法,例如 map 和 pluck,因此从集合中检索信息非常简单。

正如您通过扩展 Backbone.Model 定义模型一样,您可以通过扩展 Backbone.Collection 定义集合

Appointments = Backbone.Collection.extend({
  });

然后,您在集合上定义的任何属性都可以在此类型的集合对象上作为数据或函数使用。在这种特殊情况下,我定义了两个不同的属性,即initialize构造函数和update_appointment_counter方法

Appointments = Backbone.Collection.extend({

  update_appointment_counter: function() {
      $("#number-of-appointments").html(this.length);
  },

    initialize: function(models, options) {
      $("#number-of-appointments").html(this.length);

      this.bind("add", options.view.add_appointment_row);
      this.bind("add", this.update_appointment_counter);
  }

});

在这种情况下,构造函数使用 jQuery 初始化约会长度计数器(为零,因为集合现在才被初始化),然后向“add”事件添加两个处理程序。每次您向此集合添加新约会时,都会触发两个不同的函数。其中一个 (options.view.add_appointment_row) 将向包含约会列表的 HTML 表格添加新行,另一个 (this.update_appointment_counter) 更新计数器。正如您所看到的,这些函数可以在不同的位置定义;将这两种方法都放在视图上可能更有意义。

经验丰富的 JavaScript 程序员知道“this”是什么;因此,this.update_appointment_counter是有道理的。但是,什么是options.view?好吧,查看如何在视图构造函数中创建集合可能会有所帮助

initialize: function() {
  this.appointments = new Appointments(null, {view:this});
},

基本上,您要说的是appointments视图的属性是一个 Appointments 集合,从没有数据开始。传递第二个参数允许您在 JavaScript 对象中设置一个或多个选项,然后这些选项可以作为“options”使用。由于视图在创建集合时将自身 (!) 作为“view”选项传递,因此您可以从集合中作为 options.view 访问视图。

结果是,您的视图因此可以访问您的集合(作为this.appointments),并且您的集合可以访问我们的视图(作为options.view)。这种简单的双向通信是 Backbone.js 的典型特征,它力求使事情尽可能简单和简短。

代码不包含控制器。这是因为只有当您想提供许多不同的 URL(嗯,URL 末尾的片段)来调用不同的方法时,才需要控制器。目前,您可以不用它,但是更大的应用程序肯定需要它。

视图

与 MVC 范例中始终一样,视图是将内容显示给最终用户(并与之交互)的地方。在 Rails 世界中,视图几乎总是由控制器呈现的;您的应用程序不需要显式创建它。在 Backbone.js 世界中,视图只是另一个可以创建的对象,通常由模型创建,并且具有许多类似控制器的功能。您可以像预期的那样使用以下代码创建它

AppView = Backbone.View.extend({
});

因此,您可以将 Backbone.js 视图视为添加到当前页面的 HTML 片段,以及您可能与控制器关联的某些功能。每个视图都与一个 DOM 元素关联。默认情况下,它是一个常规的“div”元素,但您可以将其设置在一个位置(使用“el”属性),也可以使用“tagName”、“className”和“id”属性的组合来设置它。

与模型和集合一样,您可以使用“initialize”构造函数来设置一个或多个对象。在本示例应用程序中,您将初始化 Appointments 集合,而不包含任何元素成员,正如您在上面讨论该集合时看到的那样。

您还将定义一个事件处理程序,以便单击“添加约会”按钮将执行此操作

events: {
  "click #add-appointment": "add_appointment"
},

当您单击按钮时,将执行以下代码

add_appointment: function() {
var person = $("#new-appointment td input[name=person]").val();
var meeting_at = $("#new-appointment td 
 ↪input[name=meeting_at]").val();
var note = $("#new-appointment td input[name=note]").val();

this.appointments.add({person: person, meeting_at: meeting_at, 
 ↪note: note});
},

换句话说,当您单击“添加约会”按钮时,“click”事件处理程序将执行 add_appointment 函数。此函数从小的表单中获取值,并使用这些值实例化新的约会,将其添加到约会集合中。

但是,您还在集合上运行事件处理程序!第一个处理程序更新约会计数器,第二个处理程序向约会表添加新行。它通过稍微作弊的方式添加行。虽然拥有第二个以“tr”元素作为元素的新视图会更优雅,从而添加新行,但我决定模仿我看到的一些在线教程,以稍微简单的方式添加新行——即,一个丑陋的文本字符串。

如果我对创建全新的视图不感兴趣,我可以使用 Backbone.js 从 underscore.js 继承的“template”函数,为我提供可以更好地填充的类似 ERb 的模板。我可以做的另一件事是将此应用程序分解为更小的部分。虽然在处理小型项目时将所有内容放在单个文件中很不错,但更大的 Backbone.js 应用程序可以很好地放入多个文件中,每个文件定义一个不同的对象。具有任何现代服务器端 MVC 框架(例如 Rails 或 Django)经验的开发人员将理解将内容放入单独文件中的优势。

清单 1. appointments.html

<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/
↪1.4.4/jquery.min.js"></script>
<script src="http://ajax.cdnjs.com/ajax/libs/underscore.js/
↪1.1.4/underscore-min.js"></script>
<script src="http://ajax.cdnjs.com/ajax/libs/backbone.js/
↪0.3.3/backbone-min.js"></script>

<title>Appointments</title>
</head>
<body>
<h1>Appointments</h1>

<table>
<tr>
<th>Person</th>
<th>Date/time</th>
<th>Note</th>
</tr>
<tr id="new-appointment">
<td><input type="text" name="person" /></td>
<td><input type="text" name="meeting_at" /></td>
<td><input type="text" name="note" /></td>
</tr>
<tr align="center">
<td colspan="3"><input type="button" id="add-appointment" 
 ↪value="Add Appointment"/ ></td>
</tr>
</table>

<hr />

<p>Number of appointments: <span id="number-of-appointments">
 ↪</span></p>

<table id="appointments">
<tr>
<th>Person</th>
<th>Date/time</th>
<th>Note</th>
</tr>
</table>

<script type="text/javascript">
     (function ($) {

     Appointment = Backbone.Model.extend({
         person: null,
         meeting_at: null,
         note: null
     });

     Appointments = Backbone.Collection.extend({

       update_appointment_counter: function() {
           $("#number-of-appointments").html(this.length);
       },

         initialize: function(models, options) {
           $("#number-of-appointments").html(this.length);

           this.bind("add", options.view.add_appointment_row);
           this.bind("add", this.update_appointment_counter);
       }

     });

     AppView = Backbone.View.extend({
       el: $("body"),

       initialize: function() {
         this.appointments = new Appointments(null, {view:this});
       },

       events: {
         "click #add-appointment": "add_appointment"
       },

       add_appointment: function() {
           var person = $("#new-appointment 
           ↪td input[name=person]").val();
           var meeting_at = $("#new-appointment 
           ↪td input[name=meeting_at]").val();
           var note = $("#new-appointment 
           ↪td input[name=note]").val();

           this.appointments.add({person: person, 
           ↪meeting_at: meeting_at, note: note});
       },

       add_appointment_row: function(model) {
           $("#appointments").append("<tr><td>" + 
           ↪model.get('person') + "</td>" +
   "<td>" + model.get('meeting_at') + "</td>" +
   "<td>" + model.get('note') + "</td></tr>");
       }
     });

     var appview = new AppView;

     })(jQuery);
</script>

</body>
</html>
结论

Backbone.js 是用于 JavaScript 应用程序的最小且最易于理解的 MVC 框架之一。它已经变得非常流行,过去几个月关于它的博客文章数量就证明了这一点。其作者 Jeremy Ashkenas 和 DocumentCloud 的其他人在为许多 Backbone.js 用户提供的支持方面也令人印象深刻。

尽管本专栏显然没有深入探讨 Backbone.js,但此应用程序中的一个缺点应该很明显。当用户想要存储数据时会发生什么?目前,约会日历不仅在界面和执行方面都很简单(例如,无法仅查看今天的约会,更不用说删除或编辑现有约会了),而且它也未能提供持久存储。

下个月,我将讨论如何将 Backbone.js 应用程序连接到持久的后端数据库或服务器端 MVC 应用程序(从而提供 MVC 平方解决方案),从而为用户和开发人员提供两全其美的优势——使用动态 JavaScript 进行灵活开发,但具有可以轻松持久化数据的强大后端。

资源

Backbone.js 的主页位于 GitHub 上,网址为 documentcloud.github.com/backbone。此页面不仅指向代码,还指向一些教程和文档。我希望许多其他作者能够效仿,Backbone.js 的作者以精美的格式发布了源代码副本,并对其进行了详尽的注释,网址为 documentcloud.github.com/backbone/docs/backbone.html

我鼓励任何对 Backbone.js 感兴趣的人阅读代码和注释。我当然通过阅读此代码学到了一些关于 Backbone.js 特别是 JavaScript 的知识。

许多教程和博客文章描述了如何使用 Backbone.js 做有趣的事情。一个简短而切中要点的教程位于 www.plexical.com/blog/2010/11/18/backbone-js-tutorial

Alex Rothenberg 的一个更深入的示例(他将这项工作打包成 Ruby gem)位于 www.alexrothenberg.com/2011/02/11/backbone.js-makes-building-javascript-applications-fun.html

最后,在 liquidmedia.ca/blog/2011/01/backbone-js-part-1liquidmedia.ca/blog/2011/01/an-intro-to-backbone-js-part-2-controllers-and-views 上提供了关于 Backbone.js 的出色两部分教程。

Reuven M. Lerner 是一位资深的 Web 开发人员、架构师和培训师。他是西北大学学习科学专业的博士候选人,研究协作在线社区的设计和分析。Reuven 与妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论