Backbone-js-秘籍-全-

Backbone.js 秘籍(全)

原文:zh.annas-archive.org/md5/7a1480849b08fe64b97a1e5db11667fa

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

欢迎使用 Backbone.js 烹饪书。我们将学习如何使用轻量级的 JavaScript 框架Backbone.js以及现代浏览器的强大渲染能力来创建出色的 Web 应用程序。

Backbone.js 烹饪书 包含一系列食谱,提供了一系列实用、分步的解决方案,用于解决在前端应用程序开发过程中可能遇到的问题,使用 MVC 模式和 REST 风格的通信。您将学习如何利用流行的 Backbone 扩展构建 Backbone 应用程序,以及如何将您的应用程序与不同的第三方库集成。您还将学习如何满足最具挑战性的任务的要求。

本书涵盖的内容

第一章 理解 Backbone 介绍了 MVC 模式以及Backbone.js框架。您将学习如何从 MVC 的角度设计 Backbone 应用程序,并能够使用模型、视图和路由器创建您的第一个 Backbone 应用程序。

第二章 模型 帮助您了解Backbone.Model,这是您应用程序的主要构建块,它存储数据并提供业务逻辑。

第三章 集合 教您如何组织模型到可管理的集合中,这些集合允许您执行不同的方法,如排序、过滤、迭代等。

第四章 视图 帮助您学习如何使用 Backbone 视图来渲染模型和集合,以及如何拦截 DOM 事件。

第五章 事件和绑定 介绍了Backbone.js中使用的的事件系统,并演示了事件绑定技术。

第六章 模板和 UX 糖 致力于前端增强,使 Backbone 应用程序看起来更好,编程更简单。

第七章 REST 和存储 专注于Backbone.js如何与 RESTful 后端同步模型和集合或将它们存储在 HTML5 本地存储中。

第八章 特殊技术 帮助您学习如何在 Backbone 开发过程中解决最具有挑战性的问题,例如创建扩展、测试您的应用程序、创建移动应用程序以及进行搜索引擎兼容性。

您需要为本书准备的内容

本书中的大多数食谱不需要使用特殊软件。您需要的是一个浏览器和一个文本编辑器或 IDE 来编辑 HTML、JavaScript 和 CSS 文件。第七章 REST 和存储 和第八章 特殊技术 中的某些食谱要求您安装 GIT、Node.js 和 NPM。它还假设您可以使用类 Unix 的 shell。

本书面向的对象

这本书是为熟悉 JavaScript、HTML 和 CSS 的前端开发者所编写的。它假设你已对面向对象编程(OOP)有良好的理解,并且对 jQuery 库有一些实践经验。

惯例

在这本书中,你会找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称将如下所示:“要检查模型是否有属性,请使用has()方法。如果属性存在,则返回true,否则返回false。”

代码块设置如下:

if (!invoiceItemModel.has('quantity'))
  {
    console.log('Quantity attribute does not exists!')
  }

当我们希望引起你对代码块中特定部分的注意时,相关的行或项目将以粗体显示:

var InvoiceItemModel = Backbone.Model.extend
  ({
    // Define validation criteria.
    validate: function(attrs) {
 if (attrs.quantity <= 0) {
         return "quantity can't be negative or equal to zero";
      }
    }
  });

任何命令行输入或输出将如下所示:

$ npm install -g requirejs

新术语重要词汇将以粗体显示。你会在屏幕上看到这些词汇,例如在菜单或对话框中,文本将如下所示:“当用户点击添加按钮时,将生成以下弹出窗口并显示给用户:”。

注意

警告或重要提示将以如下框中的形式出现。

技巧

技巧和窍门将如下所示。

读者反馈

我们读者的反馈总是受欢迎的。让我们知道你对这本书的看法——你喜欢什么或可能不喜欢什么。读者反馈对我们开发你真正能从中获得最大价值的标题非常重要。

要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在某个主题上具有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲所有者了,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过选择您的标题从 www.packtpub.com/support 查看任何现有勘误。

盗版

在互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。

如果您发现了疑似盗版材料,请通过 <copyright@packtpub.com> 联系我们,并提供链接。

我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。

问题

如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. 理解 Backbone

在本章中,我们将涵盖以下内容:

  • 使用 MVC 模式设计应用程序

  • 使用模型和集合定义业务逻辑

  • 使用视图和路由器模拟应用程序的行为

  • 从零开始创建应用程序结构

  • 编写你的第一个 Backbone 应用程序

  • 在你的应用程序中实现 URL 路由

  • 使用插件扩展应用程序

  • 为 Backbone 项目做出贡献

简介

Backbone.js 是一个基于模型-视图-控制器(MVC)模式的轻量级 JavaScript 框架,允许开发者创建单页网页应用程序。使用 Backbone,可以通过 REST 方法快速更新网页,同时客户端和服务器之间传输的数据量最小。

Backbone.js 正在日益流行,并被广泛应用于网页应用程序和 IT 创业公司;以下是一些例子:

  • Groupon Now!:团队决定他们的第一个产品将侧重于 AJAX,但仍应具有可链接性和可分享性。尽管他们对 Backbone 完全陌生,但他们发现其学习曲线非常快,因此他们能够在两周内交付可工作的产品。

  • Foursquare:这个项目使用了 Backbone.js 库来为 foursquare 中的实体创建模型类(例如,地点、签到和用户)。他们发现 Backbone 的模型类提供了一个简单且轻量级的机制来捕获对象的数据和状态,并包含经典继承的语义。

  • LinkedIn 移动版:这个项目使用了 Backbone.js 来创建其下一代 HTML5 移动网页应用程序。Backbone 使应用程序模块化、组织化和可扩展变得容易,因此可以编程 LinkedIn 用户体验的复杂性。此外,他们正在使用相同的代码库在 iOS 和 Android 平台上的移动应用程序中。

  • WordPress.com:这是 WordPress 的 SaaS 版本,并在其通知系统中使用了 Backbone.js 的模型、集合和视图,并将 Backbone.js 集成到主页的统计标签和其他功能中。

  • Airbnb:这是一个用户可以列出、发现和预订世界各地独特空间的社区市场。其开发团队在许多最新产品中使用了 Backbone。最近,他们使用 Backbone.js 和 Node.js 重建了一个移动网站,并与名为 Rendr 的库结合在一起。

你可以通过以下链接了解 Backbone.js 的其他使用示例:

backbonejs.org/#examples

Backbone.js 由 DocumentCloud 的 Jeremy Ashkenas 于 2010 年启动,现在全世界许多开发者正在使用 Git,这个分布式版本控制系统,来使用和改进它。

在本章中,我们将提供一些如何使用 Backbone.js 的实用示例,并按照 MVC 和 Backbone 模式为名为计费应用程序的程序设计一个结构。我们还将在此书后面的章节中参考这个结构。如果你是 Backbone.js 的新手开发者,阅读本章特别有用。如果你觉得自己是经验丰富的开发者,可以跳过这一章。

使用 MVC 模式设计应用程序

MVC 是一种广泛用于面向用户软件(如 Web 应用程序)的设计模式。它的目的是以方便用户交互的方式分割数据并展示数据。为了理解它做什么,理解以下内容:

  • 模型:它包含数据和提供用于运行应用程序的业务逻辑

  • 视图:它向用户展示模型

  • 控制器:它通过更新模型和视图来响应用户输入

MVC 实现可能会有所不同,但通常它符合以下方案:

使用 MVC 模式设计应用程序

全球实践表明,使用 MVC 模式为开发者提供了各种好处:

  • 遵循关注点分离的原则,将应用程序分割成独立的部分,这使得修改或替换变得更加容易。

  • 它通过在不同的视图中渲染模型来实现代码重用,而无需在每个视图中实现模型功能

  • 对于组织中的新开发者来说,它需要更少的培训,并且启动时间更快

为了更好地理解 MVC 模式,我们将设计一个计费应用程序。在学习特定主题时,我们将参考这个设计。

我们的计费应用程序将允许用户生成发票、管理它们并将它们发送给客户。根据全球惯例,发票应包含参考编号、日期、买方和卖方信息、银行账户详情、提供的产品或服务列表以及发票总额。让我们看一下下面的截图,以了解发票的外观:

使用 MVC 模式设计应用程序

如何操作...

让我们遵循以下步骤来为计费应用程序设计 MVC 结构:

  1. 让我们为这个应用程序列出功能需求。我们假设最终用户可能希望能够执行以下操作:

    • 生成发票

    • 将发票通过电子邮件发送给买方

    • 打印发票

    • 查看现有发票列表

    • 管理发票(创建、读取、更新和删除)

    • 更新发票状态(草稿、已发行、已支付和已取消)

    • 查看年度收入图表和其他报告

  2. 为了简化创建多个发票的过程,用户可能希望在创建发票之前,在应用程序的特定部分管理买家的信息和其个人详细信息。因此,我们的应用程序应向最终用户提供以下附加功能:

    • 查看买家列表并在生成发票时使用它的能力

    • 管理买家(创建、读取、更新和删除)的能力

    • 查看银行账户列表并在生成发票时使用它的能力

    • 管理他/她的银行账户(创建、读取、更新和删除)的能力

    • 编辑个人详细信息并在生成发票时使用它们的能力

    当然,我们可能想要有更多的功能,但这足以展示如何使用 MVC 模式设计应用程序。

  3. 接下来,我们使用 MVC 模式设计应用程序。

    在我们定义了应用程序的功能之后,我们需要了解哪些更与模型(业务逻辑)相关,哪些更与视图(展示)相关。让我们将功能分成几个部分。

  4. 然后,我们学习如何定义模型。

    模型展示数据和提供特定于数据的数据逻辑。模型可以相互关联。在我们的案例中,它们如下:

    • InvoiceModel

    • InvoiceItemModel

    • BuyerModel

    • SellerModel

    • BankAccountModel

  5. 然后,我们将定义模型集合。

    我们的应用程序允许用户操作多个模型,因此它们需要组织成一个名为 Collection 的特殊可迭代对象。我们需要以下集合:

    • InvoiceCollection

    • InvoiceItemCollection

    • BuyerCollection

    • BankAccountCollection

  6. 接下来,我们定义视图。

    视图向应用程序用户展示模型或集合。单个模型或集合可以被多个视图渲染。我们应用程序中需要的视图如下:

    • EditInvoiceFormView

    • InvoicePageView

    • InvoiceListView

    • PrintInvoicePageView

    • EmailInvoiceFormView

    • YearlyIncomeGraphView

    • EditBuyerFormView

    • BuyerPageView

    • BuyerListView

    • EditBankAccountFormView

    • BankAccountPageView

    • BankAccountListView

    • EditSellerInfoFormView

    • ViewSellectInfoPageView

    • ConfirmationDialogView

  7. 最后,我们定义一个控制器。

    控制器允许用户与应用程序交互。在 MVC 中,每个视图都可以有不同的控制器,用于执行以下操作:

    • 将 URL 映射到特定视图

    • 从服务器获取模型

    • 显示和隐藏视图

    • 处理用户输入

使用模型和集合定义业务逻辑

现在,是时候使用 MVC 和 OOP 方法为计费应用程序设计业务逻辑了。

在这个菜谱中,我们将定义应用程序的内部结构,包括模型和集合对象。虽然模型代表单个对象,但集合是一组可以迭代、过滤和排序的模型。

计费应用程序中模型和集合之间的关系符合以下方案:

使用模型和集合定义业务逻辑

如何操作...

对于每个模型,我们将创建两个表格:一个用于属性,另一个用于方法:

  1. 我们定义 BuyerModel 属性。

    名称 类型 必需 唯一
    id Integer
    name 文本
    address 文本
    phoneNumber 文本
  2. 然后,我们定义 SellerModel 属性。

    名称 类型 必需 唯一
    id Integer
    name 文本
    address 文本
    phoneNumber 文本
    taxDetails 文本
  3. 在此之后,我们定义 BankAccountModel 属性。

    名称 类型 必需 唯一
    id Integer
    beneficiary 文本
    beneficiaryAccount 文本
    bank 文本
    SWIFT 文本
    specialInstructions 文本
  4. 我们定义 InvoiceItemModel 属性。

    名称 参数 返回类型 唯一
    calculateAmount - Decimal
  5. 接下来,我们定义 InvoiceItemModel 方法。

    我们不需要在模型中存储项目金额,因为它始终取决于价格和数量,因此可以计算得出。

    名称 类型 必需 唯一
    id Integer
    deliveryDate 日期
    description 文本
    price Decimal
    quantity Decimal
  6. 现在,我们定义 InvoiceModel 属性。

    名称 类型 必需 唯一
    id Integer
    referenceNumber 文本
    date 日期
    bankAccount 引用
    items 集合
    comments 文本
    status Integer
  7. 我们定义 InvoiceModel 方法。

    发票金额可以轻松计算为发票项目金额的总和。

    名称 参数 返回类型 唯一
    calculateAmount Decimal
  8. 最后,我们定义集合。

    在我们的案例中,它们是 InvoiceCollection、InvoiceItemCollection、BuyerCollection 和 BankAccountCollection。它们用于存储适当类型的模型,并提供一些方法来向集合中添加/删除模型。

它是如何工作的...

Backbone.js 中的模型通过扩展 Backbone.Model 实现,集合通过扩展 Backbone.Collection 创建。要实现模型和集合之间的关系,我们可以使用特殊的 Backbone 扩展,这些扩展在本书的后续章节中有所描述。

参见

  • 在第二章中,操作模型属性的配方,模型

  • 在第三章中,创建模型集合的配方,集合

要了解更多关于对象属性、方法和 JavaScript 中的 OOP 编程的信息,您可以参考以下资源:

developer.mozilla.org/en-US/docs/JavaScript/Introduction_to_Object-Oriented_JavaScript

使用视图和路由器建模应用程序的行为

与传统的 MVC 框架不同,Backbone 不提供任何实现控制器功能的独立对象。相反,控制器在 Backbone.Router 和 Backbone.View 之间分散,以下是如何做的:

  • 路由器处理 URL 变化并将应用程序流程委托给视图。通常,路由器异步从存储中获取模型。当模型被获取时,它触发视图更新。

  • 视图监听 DOM 事件,要么更新模型,要么通过路由器导航应用程序。

以下图显示了 Backbone 应用程序中的典型工作流程:

使用视图和路由器建模应用程序的行为

如何做到这一点...

让我们按照以下步骤来了解如何在我们的应用程序中定义基本视图和路由:

  1. 首先,我们需要为应用程序创建线框。

    让我们在本食谱中绘制几个线框:

    • 编辑发票页面允许用户选择买方,从列表中选择卖方的银行账户,输入发票的日期和参考编号,以及构建已发货的产品和服务表格。如何做到这一点...

    • 预览发票页面显示了买方将看到的最终发票。这种显示应该渲染我们在编辑发票表单中输入的所有信息。买方和卖方信息可以在应用程序存储中查找。用户可以选择返回到编辑显示或保存此发票。如何做到这一点...

  2. 然后,我们将定义视图对象。

    根据之前的线框,我们需要有两个主要视图:EditInvoiceFormView 和 PreviewInvoicePageView。这些视图将与 InvoiceModel 一起操作;它引用其他对象,如 BankAccountModel 和 InvoiceItemCollection。

  3. 现在,我们将视图拆分成子视图。

    对于产品或服务表中的每一项,我们可能希望根据用户在价格和数量字段中输入的内容重新计算金额字段。做到这一点的一种方法是在用户更改表格中的值时重新渲染整个视图;然而,这不是一种高效的方法,并且需要大量的计算机功率来完成。

    如果我们只想更新视图的一小部分,我们不需要重新渲染整个视图。最好是把大视图拆分成不同的、独立的片段,例如子视图,它们只能渲染大视图的特定部分。在我们的例子中,我们可以有以下视图:

    如何做到这一点...

    如我们所见,EditInvoiceItemTableView 和 PreviewInvoiceItemTableView 通过辅助视图 EditInvoiceItemView 和 PreviewInvoiceItemView 来渲染 InvoiceItemCollection,这些辅助视图负责渲染 InvoiceItemModel。这种分离使我们能够在项目发生变化时重新渲染集合中的项目。

  4. 最后,我们将定义与相应视图关联的 URL 路径。在我们的例子中,我们可以有多个 URL 来显示不同的视图,例如:

    • /invoice/add

    • /invoice/:id/edit

    • /invoice/:id/preview

    在这里,我们假设 Edit Invoice 视图可以用于创建新的发票或编辑现有的发票。在路由实现中,我们可以加载此视图并在特定的 URL 上显示它。

它是如何工作的...

Backbone.View 对象可以被扩展以创建我们自己的视图,该视图将渲染模型数据。在视图中,我们可以定义处理用户操作的手柄,例如数据输入和键盘或鼠标事件。

在应用程序中,我们可以有一个单独的 Backbone.Router 对象,它允许用户通过更改浏览器地址栏中的 URL 来导航应用程序。该路由对象包含一系列可用的 URL 和回调函数。在回调函数中,我们可以触发与 URL 关联的特定视图的渲染。

如果我们希望用户能够从一个视图跳转到另一个视图,我们可能希望他们点击与视图关联的常规 HTML 链接,或者通过编程方式导航到应用程序。

参见

  • 第二章, 视图

从头开始创建应用程序结构

在本食谱中,我们将讨论如何从头开始创建 Backbone 项目。在处理本书的后续章节时,有一些重要信息是我们应该注意的。

如何实现...

我们将讨论 Backbone 依赖关系以及我们项目的目录结构。请遵循以下指南:

  1. 下载 Backbone.js。

    访问 backbone.js 并下载 Backbone.js 库。有多个版本可供选择:生产版、开发版和边缘版。

    您可以使用生产版本以获得最佳性能,因为它已经过优化和最小化。在开发应用程序时,可以使用开发版本,这样您就可以使用您 IDE 的代码补全和调试功能。最后,您还可以使用 Backbone 的边缘版本,但请自行承担风险,因为它可能尚未完全测试。

  2. 下载 Backbone 依赖项。

    Backbone.js 依赖于 Underscore.js 库,可以从 underscorejs.org 下载。Underscore 还提供了三个不同版本。

    此外,Backbone.js 依赖于 jQuery 或 Zepto 库。这些库具有相同的语法,并且都为开发者提供了有用的功能。它们简化了与文档树、事件处理、AJAX 和 JavaScript 动画的工作。

    在本书的许多示例中,我们将使用 jQuery 库,可以从 jquery.com 下载。它提供了开发和生产版本。

  3. 创建项目目录结构。

    如果你遵循特定的目录结构,将更容易找到任何文件并与之工作,因为这种应用程序结构为你的项目带来了更多的秩序。以下是一个可以用于简单 Backbone 应用的目录结构示例:

    • lib/: 这是一个用于第三方库的目录,例如以下内容:

      backbone.js: 这是 Backbone.js 的源代码

      underscore.js: 这是 Underscore.js 的源代码

      jquery.js: 这是 jQuery 的源代码

    • js/: 这是项目 JavaScript 文件的目录。

      main.js: 这是项目中使用的主体 JavaScript 文件

      index.html: 这是我们的应用程序的主要文件。

    创建应用程序的主要文件,即 index.html。它应该包含第三方库和你的应用程序文件,如下面的代码所示:

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Backbone.js Cookbook – Application Template</title>
    
        <script src="img/jquery.js"></script>
        <script src="img/underscore.js"></script>
        <script src="img/backbone.js"></script>
    
        <script src="img/main.js"></script>
      </head>
      <body></body>
    
    </html>
    
  4. 创建名为 main.js 的主要 JavaScript 文件,它将包含你的应用程序代码。

    (function($){
    
      // Your code is here
    
    })(jQuery);
    

    当我们将脚本包含到 head 标签中时,它们会在浏览器处理 body 内容之前以及整个 HTML 文档加载之前执行。

    在 Backbone 应用程序中,就像在许多其他 JavaScript 应用程序中一样,我们想要确保我们的程序在文档加载后立即开始运行,因此 main.js 应该看起来像以下代码片段:

    (function($){
    
      // Object declarations goes here
    
      $(document).ready(function () {
    
       // Start application code goes here
    
      });
    })(jQuery);
    

    小贴士

    你可以使用这个应用程序模板来创建你自己的 Backbone 应用。我们也将使用这个模板作为本书中的示例。

编写你的第一个 Backbone 应用程序

在本食谱中,我们将编写我们的第一个 Backbone 应用程序。让我们将其作为账单系统的一个简单部分。

例如,我们可以为发票项目实现一个模型和一个视图。让我们创建包含数量和价格字段并计算项目金额的 InvoiceItemModel。我们还需要有一个用于渲染模型的 PreviewInvoiceItemView。

我们演示应用程序的输出可能非常简单,如下面的截图所示:

编写你的第一个 Backbone 应用程序

如何做到...

本食谱中的新代码应该放入我们在上一个食谱中创建的 main.js 文件中;我们将这样做:

  1. 通过扩展 Backbone.Model 对象来定义模型。

      var InvoiceItemModel = Backbone.Model.extend({
    
        // Set default values.
        defaults: {
          price: 0,
          quantity: 0
        },
    
        // Calculate amount.
        calculateAmount: function() {
          return this.get('price') * this.get('quantity');
        }
      });
    

    在 InvoiceItemModel 对象中,我们初始化了默认值并执行了业务逻辑,一个计算总金额的函数。

  2. 创建一个模型实例。

        var invoiceItemModel = new InvoiceItemModel({
          price: 2,
          quantity: 3
        });
    
  3. 定义将渲染模型的视图。

      var PreviewInvoiceItemView = Backbone.View.extend({
    
        // Define template using templating engine from
        // Underscore.js.
        template: _.template('\
          Price: <%= price %>.\
          Quantity: <%= quantity %>.\
          Amount: <%= amount %>.\
        '),
    
        // Render view.
        render: function () {
    
          // Generate HTML by rendering the template.
          var html = this.template({
    
            // Pass model properties to the template.
            price: this.model.get('price'),
            quantity: this.model.get('quantity'),
    
            // Calculate amount and pass it to the template.
            amount: this.model.calculateAmount()
          });
    
          // Set html for the view element using jQuery.
          $(this.el).html(html);
        }
      });
    

    如我们所见,我们的视图使用了在创建时传递给视图的 this.model 和 this.el 属性。

        var previewInvoiceItemView = new PreviewInvoiceItemView({
          model: invoiceItemModel,
          el: 'body'
        });
    

    在视图中,我们使用了 jQuery 库来设置与视图关联的元素的内容 $(this.el).html(html)。在我们的例子中,this.el 包含 'body',它也是一个 jQuery 选择器。

    这样的选择器类似于 CSS 选择器,并允许 jQuery 使用 $() 函数找到任意 HTML 元素。

  4. 要渲染一个视图,我们只需执行 render() 方法。

      previewInvoiceItemView.render();
    

    在渲染视图时,我们还使用了 Underscore.js 提供的模板引擎。这个模板引擎用数据替换模板并输出静态 HTML。有关模板的更多信息,请参阅 第六章 中的“在视图中使用模板”配方,模板、表单和 UX 糖。

  5. 启动应用程序。

    启动应用程序有多种方式。如果你的应用程序只有一个视图,你可以创建其新实例并手动渲染它。

    应用程序应该在 HTML 页面加载后立即启动。让我们编写一些代码来启动一个简单的 Backbone 应用程序:

      // When document is ready create the Model and show
      // the View.
      $(document).ready(function () {
    
        // Create InvoiceItemModel instance and set
        // model attributes.
        var invoiceItemModel = new InvoiceItemModel({
          price: 2,
          quantity: 3
        });
    
        // Create PreviewInvoiceItemView instance.
        var previewInvoiceItemView = new PreviewInvoiceItemView({
    
          // Pass our model.
          model: invoiceItemModel,
    
          // Set element where to render HTML.
          el: 'body'
        });
    
        // Render view manually.
        previewInvoiceItemView.render();
      });
    

参见

  • 第二章, 模型

  • 第三章, 集合

  • 第四章, 视图

  • 第五章, 事件和绑定

在你的应用程序中实现 URL 路由

Backbone.Router 对象用于应用程序内的导航。如果你想通过点击适当的 URL 访问不同的视图页面,你应该使用它。用户还可以通过浏览器的历史记录栏在应用程序中进行导航。

默认情况下,路由器与哈希路径配合良好,例如 index.html#path/to/page。任何放置在哈希字符之后的字符串都应被视为路由,并由 Backbone.Router 处理。

如何操作...

在这里,我们将解释如何在我们的应用程序中创建自己的路由器:

  1. 通过将 Backbone.Router 扩展到 Workspace 对象中并设置路由和回调函数的成对关系,在传递给 extend() 方法的 routes 属性中,定义一个路由器。这为路由器提供了信息,在访问适当的路由时应该执行哪个回调。

      var Workspace = Backbone.Router.extend({
        routes: {
          // Default path.
          '': 'invoiceList',
    
          // Usage of static path.
          'invoice': 'invoiceList',
        },
      });
    
  2. 向路由器对象添加回调方法。

        invoiceList: function() {
          var invoiceListView = new InvoiceListView({
            el: 'body'
          });
          invoiceListView.render();
        }
    

    如果用户访问 index.html 或 index.html#invoice,则会执行 invoiceList() 回调,该回调渲染 InvoiceListView。在这里,InvoiceListView 是一个简单的占位符。

    如何操作...

  3. 告诉 Backbone 使用此路由并启动应用程序。

      $(document).ready(function () {
        new Workspace();
        Backbone.history.start();
      });
    

    在这里,我们创建一个新的 Workspace 对象并执行 Backbone.history 对象的 start() 方法,该对象用于全局应用程序路由。一如既往,我们应该在 HTML 页面完全加载后启动我们的应用程序。

它是如何工作的...

Backbone.Router 仅用于定义路由和回调。所有重要工作都由 Backbone.history 完成,它作为全局路由器(每帧)来处理 hashchange 或 pushState 事件,匹配适当的路由并触发回调。您永远不需要创建全局路由器的实例——您应该使用 Backbone.history 的引用,该引用将在您使用带有路由的路由器时自动为您创建。

更多内容...

Backbone 路由器允许定义带有参数的路由,我们将在本节中解释。

在 URL 中解析参数

如果我们想让路由解析 URL 中的参数,需要在参数名称之前使用冒号字符(:)。以下是一个示例,展示了 Backbone.Router 如何解析带有参数的 URL。

  var Workspace = Backbone.Router.extend({
    routes: {
      // Usage of fragment parameter.
      'invoice/:id': 'invoicePage',
    },

    // Shows invoice page.
    invoicePage: function(id) {
      var invoicePageView = new InvoicePageView({
        el: 'body',

        // Pass parameter to the view.
        id: id
      });
      invoicePageView.render();
    },
  });

例如 index.html#invoice/1 和 index.html#invoice/2 这样的路径将被路由器解析。在这两种情况下,invoicePage() 回调都会执行;它将 ID 参数传递给 InvoiceLPageView 并渲染它。

在 URL 中解析参数

小贴士

在您的路由器中验证参数

在路由定义中设置参数的数据类型或格式的限制没有默认方式。所有传递给路由器回调的参数都是字符串,验证它们是开发者的责任。

相关内容

  • 在 第五章 中的处理路由事件配方,事件和绑定

  • 在 第四章 中使用 Backbone.Router 的切换视图配方,视图

使用插件扩展应用程序

Backbone 的核心小巧、经过良好测试且维护良好。然而,开发者可能需要额外的功能来用于复杂的 Web 应用程序。Backbone 框架的力量取决于模块化和灵活性。现有组件可以轻松地扩展或替换;因此,许多开发者创建了他们自己的插件。

您可以从 github.com/documentcloud/backbone/wiki/Extensions,-Plugins,-Resources 下载并使用超过 100 个插件,在您的应用程序中。在这本书中,我们将使用其中的一些,因此我们需要了解如何使用插件扩展我们的应用程序。

如何操作...

如果插件是一个单独的 JavaScript 文件,只需将其复制到项目的 lib 文件夹中,并在 index.html 中包含它。

  <script src="img/backbone.plugin.js"></script>

或者,如果插件附带额外的文件,例如 CSS 和图片,请将所有插件文件放置在 lib 文件夹下的 plugin-name 目录中,然后在 index.html 中包含 JS 和 CSS 文件。

小贴士

使用 Git 子模块

如果您的项目托管在 Git 仓库中,您可以使用 Git 子模块命令在您的项目仓库中插入插件仓库。如果您想通过编写单个 git 命令轻松更新项目插件,这将非常有用。

相关内容

  • 在 第八章 的 Grunt 菜谱中创建 Backbone.js 扩展,特殊技巧

贡献到 Backbone 项目

Backbone.js 是一个由强大社区开发的开源项目。在这个菜谱中,我们将讨论一些帮助你成为这个社区一部分并改进 Backbone.js 的内容。

如何操作...

让我们遵循以下步骤来使 Backbone.js 更好:

  1. 处理问题队列。

    如果你发现了 Backbone.js 中的错误或希望实现新功能,你可以将你的问题提交到 github.com/documentcloud/backbone/issues 的问题队列中。在这样做之前,请确保没有类似的问题;否则,你可以更新现有的问题队列。

  2. 贡献代码。

    你可以向 Backbone 项目提交自己的代码。这样的贡献对社区和项目本身都非常有帮助。

    通过使用 Backbone,你可以节省自己的时间。通过为项目做出贡献,你不仅节省了使用它的其他开发者的时间,也节省了你未来再次处理相同问题的自己的时间。

    有关代码贡献过程的详细指南可以在 github.com/documentcloud/backbone/wiki/Contributing-to-Backbone 的维基页面上找到。

  3. 从事 Backbone.js 的文档工作。

    官方文档位于 backbonejs.org,基于 GitHub 仓库中的最新版本的 Backbone.js。你可以通过更新 index.html 文件或 docs 目录来改进文档。如果你想添加一个新示例,请使用 examples 文件夹。

第二章. 模型

本章将涵盖以下内容:

  • 创建模型

  • 操作模型属性

  • 操作模型标识符

  • 验证模型属性

  • 覆盖获取器和设置器

  • 创建撤销点以存储/恢复模型的状态

  • 实现模型的工作流程

  • 在模型中使用高级验证

  • 验证 HTML 表单

  • 在模型中处理嵌套属性

  • 实现一对一关系

简介

在本章中,我们将学习 Backbone 模型的概念以及如何使用它。我们还将考虑各种 Backbone 扩展,它们提供了许多改进,并为我们的模型带来了惊人的功能。

本章的前三个菜谱包含了对 Backbone 不熟悉的初学者的信息;其他菜谱提供了额外的价值,并涵盖了更多高级主题。

创建模型

模型是任何 MVC 应用程序的基本构建块,它包含数据、提供验证、执行访问控制和实现应用程序所需的具体业务逻辑。在 Backbone.js 中,模型通过从 Backbone.Model 对象扩展其实例来定义。在本菜谱中,我们将学习如何在 Backbone.js 中处理模型。

如何操作...

执行以下步骤以定义新的模型对象并创建其实例:

  1. 通过扩展 Backbone.Model 定义模型。

      var InvoiceItemModel = Backbone.Model.extend({
    
      });
    

    在模型对象内部不需要定义数据结构,因为 Backbone 允许在初始化模型时动态定义。

  2. 创建一个 Backbone.Model 实例,并用属性值初始化它。

      var invoiceItemModel = new InvoiceItemModel({
        date: '2013-04-24',
        description: 'Wooden Toy House',
        price: 22,
        quantity: 3
      });
    

更多...

在本节中,我们将学习如何克隆模型以及如何使用默认值初始化模型。

模型的克隆

当你将一个模型分配给另一个变量时,它使得一个模型反映另一个模型的变化。如果你需要一个模型的独立副本,请使用 clone() 方法。

newModel = invoiceItemModel.clone();

设置默认属性值

有时候,你可能希望模型在创建新的模型实例时具有默认值初始化的属性,这样你就不需要手动设置它们。以下是定义默认属性的方法:

  var InvoiceItemModel = Backbone.Model.extend({

    // Define default attributes.
    defaults: {
      date: '',
      description: '',
      price: 0,
      quantity: 1
    },
  });

以下示例显示 quantitydate 属性默认被初始化:

  var invoiceItemModel2 = new InvoiceItemModel({
    description: 'Farm Animal Set',
    price: 17
  });

  invoiceItemModel2.get('date') != undefined; // true
  invoiceItemModel2.get('quantity'); // 1

使用多行表达式设置默认属性值

如果你想要使用多行表达式设置默认值,你可以将其包装在一个函数中,并在设置 defaults 中的默认属性时调用它。

  // Create new model object
  var InvoiceItemModel = Backbone.Model.extend({

    // Set default attributes.
      defaults: {
        description: '',
        price: 0,
        quantity: 1,

    // Use function for multiline expression.
        date: function() {
          var date = new Date();

        // Return attribute value.
          return date.toISOString();
        }
      }
    });

同样,也可以在 initialize() 方法中实现相同的功能,该方法在模型对象创建并初始化后立即调用。

  // Crate new model
  var InvoiceItemModel = Backbone.Model.extend({

    // Set default values.
    defaults: {
      description: '',
      price: 0,
      quantity: 1,
    },

    // Set default values in initialize method.
    // Following method is run after the object is created.
    initialize: function() {

      // Check that attribute is not initialized yet.
        if (!this.has('date')) {
          var date = new Date();

        // Set attribute value.
          this.set('date', date.toISOString());
        }
      }
    }

initialize() 方法中,我们使用 JavaScript 的 Date 对象将 date 属性设置为今天的日期。在这样做之前,我们需要检查 date 属性尚未初始化,这样我们就不覆盖它了。

小贴士

如果默认属性被定义,那么它们可以覆盖在 initialize() 方法中定义的属性,因此我们需要从 default 值中删除这些属性,否则它们将作为默认值初始化。

参见

  • 在当前的食谱示例中,我们使用了 has()set() 方法,这些方法在以下食谱中有描述:操作模型属性

操作模型属性

属性是模型存储所有数据的地方。与用于存储内部对象信息的模型属性不同,属性不能通过 . 操作符访问。有一些特殊的方法可以用来操作它们,我们将在本食谱中学习。

如何做...

与模型属性一起工作的主要方法有 get()set()unset()clear()

  1. 使用 get() 方法获取属性值。

    var quantity = invoiceItemModel.get('quantity');
    

    如果找不到属性,则返回 undefined

  2. 使用 set() 方法来更新/创建单个属性值。

    invoiceItemModel.set('quantity', 5);
    
    • 要更新多个属性,请使用键值格式。

          invoiceItemModel.set({
            quantity: 5,
            price: 10
          });
      

    当设置一个属性如果它不存在时,会创建一个。如果验证没有失败,set() 方法返回一个值 true 的引用到模型;否则返回值 false。我们将在食谱中了解更多关于验证的信息,验证模型属性

  3. 使用 unset() 方法从模型中删除一个属性。

    invoiceItemModel.unset('quantity');
    
  4. 使用 clear() 方法从模型中删除所有属性。

    invoiceItemModel.clear();
    

它是如何工作的...

属性存储在 attributes 属性中。最好不要直接访问属性,而应使用我们之前学过的方法;否则,可能会破坏事件触发机制或与其他 Backbone 扩展的集成。

当一个新的模块初始化时,defaults 属性的值被分配给 attributes

还有更多...

在本节中,我们将学习一些与模型属性一起使用的有用方法。

检查模型是否有属性

要检查模型是否有属性,请使用 has() 方法。如果属性存在,则返回 true,否则返回 false

if (!invoiceItemModel.has('quantity')) {
  console.log('Quantity attribute does not exists!')
}

获取 HTML 转义属性值

如果你打算显示用户输入的文本,你假设它是纯文本格式,你应该担心安全问题。防止可能导致可能的 XSS 攻击的最佳方式是在输出任何用户输入的文本之前使用 escape() 方法。这阻止浏览器通过转义 HTML 字符来解析任何 HTML 代码。让我们看看它是如何工作的:

  var hacker = new Backbone.Model({
    name: "<script>alert('xss')</script>"
  });
  var escaped_name = hacker.escape('name');
  // &lt;script&gt;alert('xss')&lt;/script&gt;

操作模型标识符

每个模型都有一个唯一的标识符属性 ID,这允许区分一个模型和另一个模型。当开发 Backbone 应用程序时,通常需要操作标识符。

如何做...

以下步骤解释了如何设置和获取 id 属性:

  1. 设置和获取 id 属性非常简单。

      invoiceItemModel.id = Math.random().toString(36).substr(2);
    

    获取 id 属性如下所示:

      var id = invoiceItemModel.id;
    

它是如何工作的...

id 属性提供了直接访问存储标识符的属性。默认情况下它是 id;然而,在扩展模型时,你可以通过设置 idAttribute 来覆盖它。

  var Meal = Backbone.Model.extend({
    idAttribute: "_id"
  });

当创建新的模型时,除非手动分配,否则标识符为空。

还有更多...

如果你的模型中 id 尚未初始化,则可以使用客户端标识符,该标识符可以通过 cid 属性访问。cid 的值是唯一的,并在创建新的模型实例时自动分配。客户端标识符可以采用如 c0c1c2 等形式。

验证模型属性

为了防止意外行为,我们通常需要验证模型属性。

如何做到...

执行以下步骤以设置属性验证:

  1. 可以通过定义 validate() 方法来进行验证。

      var InvoiceItemModel = Backbone.Model.extend({
    
        // Define validation criteria.
        validate: function(attrs) {
          if (attrs.quantity <= 0) {
            return "quantity can't be negative or equal to zero";
          }
        }
      });
    

    attrs 参数包含已更改的属性值。如果它们未通过验证,validate() 方法将返回错误信息。

  2. 属性验证在 save() 方法上触发。如果传递 {validate: true} 作为最后一个参数,它也可以在 set() 方法上触发。

      var invoiceItemModel = new InvoiceItemModel({
        description: 'Wooden Toy House',
        price: 10
      });
    
        // Set value that is not valid.
        invoiceItemModel.set('quantity', -1, {validate: true});
    

    小贴士

    当验证模型时,你可以通过 this.get()this.attributes 的帮助访问旧属性值。

它是如何工作的...

validatesave() 之前被调用,并接受从 save() 传递的更新后的模型属性。如果 validate() 返回错误字符串,save() 将不会继续,并且模型属性将不会被修改。失败的验证会触发 invalid 事件。如果你想在 set() 方法中触发验证,请将 {validate: true} 作为最后一个参数传递。

还有更多...

在本节中,我们将更深入地研究验证的细节。

处理验证错误

如果模型没有通过验证,我们通常需要继续运行应用程序并提供自定义代码来处理事件。让我们看看它是如何完成的。

  invoiceItemModel.on("invalid", function(model, error) {
    console.log(error);
  });

这样的错误处理程序应该在触发验证事件之前绑定。

另一种处理事件的方法允许我们将错误处理函数作为选项传递给 set()fetch()save()destroy() 方法。

  var invoiceItemModel2 = new InvoiceItemModel({
    description: 'Animal Farm',
    price: 17
  });
    invoiceItemModel2.set({quantity: 0}, {
      invalid: function(model, error) {
        console.log(error);
      },
      validate: true
    });

手动触发验证

虽然验证在每次模型更新或保存到存储时都会执行,但有时你可能想手动检查模型是否通过验证。让我们弄清楚如何做到这一点。

  var invoiceItemModel3 = new InvoiceItemModel({
    description: 'Wooden Toy House',
    price: 10,
    quantity: -5
  });
    invoiceItemModel3.isValid(); // false

isValid() 返回 true 或 false,但不会触发 invalid 事件。

参考内容

  • 在第五章中处理 Backbone 对象的事件事件和绑定

覆盖获取器和设置器

有时需要覆盖应用程序中的获取器或设置器。这样做可能有不同的原因:

  • 一个属性以不同于输入或输出的格式存储

  • 你有一个虚拟属性,它不在模型中存储,但依赖于其他属性

  • 防止将非法值分配给属性

默认情况下,Backbone 不允许用户覆盖获取器或设置器,但有一个名为 Backbone.Mutators 的扩展,允许这样做。

准备工作

GitHub 页面 github.com/asciidisco/Backbone.Mutators 可以下载 Backbone.Mutators

要将此扩展包含到项目中,将 backbone.mutators.js 文件保存到 lib 文件夹,并在 index.html 中包含对其的引用。

注意

在 第一章 的 使用插件扩展应用程序 菜单中,详细描述了如何在项目中包含 Backbone 扩展。

如何做到这一点...

我们可以为不存在的虚拟属性指定获取器或设置器。在某些情况下这可能很有用,例如,如果虚拟属性依赖于其他属性。

  1. 通过覆盖获取器来引入一个新的虚拟属性。

      var BuyerModel = Backbone.Model.extend({
    
        // Use mutators
        mutators: {
    
        // Introduce virtual attribute.
          fullName: {
            get: function () {
              return this.firstName + ' ' + this.lastName;
            }
          }
        }
      });
    

    在模型对象中,我们定义了一个新的 mutators 属性,为名为 fullName 的新虚拟属性提供了获取器。这个属性不假定存储在模型中,因为它包含现有属性 firstNamelastName 的值。现在,让我们看看我们如何使用覆盖的获取器。

      var buyerModel = new BuyerModel();
      buyerModel.set({
        firstName: 'John',
        lastName: 'Smith'
      });
    
        buyerModel.get('fullName'); // John Smith
        buyerModel.get('firstName'); // John
        buyerModel.get('lastName'); // Smith
    
  2. 覆盖设置器,这样虚拟属性实际上不会在模型中保存,而是更新其他属性。

      var BuyerModel = Backbone.Model.extend({
    
        // Use mutators
        mutators: {
    
          // Introduce virtual attribute.
          fullName: {
            set: function (key, value, options, set) {
              var names = value.split(' ');
              this.set('firstName', names[0], options);
              this.set('lastName', names[1], options);
            }
          }
        }
      });
    

    fullName 属性的设置器中,我们将值分割成一个数组,然后将 firstNamelastName 属性分配给它的部分。以下是一个示例,演示了如何使用它:

        var buyerModel2 = new BuyerModel()
        buyerModel2.set('fullName', 'Joe Bloggs');
    
        buyerModel2.get('fullName'); // Joe Bloggs
        buyerModel2.get('firstName'); // Joe
        buyerModel2.get('lastName'); // Bloggs
    

小贴士

使用 set() 方法初始化属性

如果你为属性使用设置器突变器,触发它的唯一方法是调用 set() 方法。如果你在创建新模型时分配属性,设置器突变器将不会工作,因为在这种情况下不会触发 change 事件。否则,你需要为特定属性触发 change 事件。

它是如何工作的...

Backbone.Mutators 扩展覆盖了 Bakcbone.Modelget()set() 方法。新方法尝试调用被覆盖的获取器和设置器。如果没有,则运行原始的 get()set() 方法。

它还覆盖了 toJSON() 方法,并替换了被覆盖获取器的属性。

更多内容...

在本节中,我们将学习 Backbone.Mutators 扩展的高级用法。

覆盖现有属性的获取器和设置器

如果需要将属性以不同于输出或输入的格式存储,可以覆盖现有属性的设置器。可以覆盖此属性的获取器和设置器以解决这个问题。让我们看看如何使用 Backbone.Mutators 为现有属性:

  var BuyerModel = Backbone.Model.extend({

    // Use mutators.
    mutators: {

      // Override existing attribute.
      vip: {
        get: function() {
          return this.vip === true ? 'VIP' : 'Regular';
        },
        set: function (key, value, options, set) {
          set(key, value === 'VIP', options);
        }
      }
    }
  });

在此模型中,存在一个 vip 属性,它是 boolean 类型的。我们希望这个属性以字符串形式呈现给用户,因此我们将覆盖它的获取器和设置器。

使用语法与常规属性相同。

    var buyerModel3 = new BuyerModel();
    buyerModel2.set({
      fullName: 'Mister X',
      vip: 'VIP'
    });

    buyerModel2.get('vip'); // VIP
    buyerModel2.attributes.vip; // true

小贴士

Mutators 旨在覆盖设置器或获取器,但它们本身不修改属性值。您可以通过访问模型的attributes属性来始终获取原始属性。

处理 mutators 事件

您可以将回调绑定到mutators:set:*事件。以下是实现方式:

    buyerModel3.on('mutators:set:fullName',
      function (a, b, c, d) {
        console.log('mutators:set:fullName is triggered');
    });

    buyerModel3.set({
      fullName: 'Mister Y'
    });

参见

  • 在第五章中处理 Backbone 对象的事件事件和绑定

创建撤销点以存储/恢复模型的状态

有时,您可能需要在应用程序中管理模型的状态。在以下情况下这可能很有用:

  • 您的应用需要撤销/重做功能

  • 您想实现事务

  • 您的应用模拟某些过程

  • 您想临时更改模型然后恢复它

通常,对于所有上述情况,开发者经常使用Memento模式。在 Backbone 中有一个此模式的实现,可在Backbone.Memento扩展中找到。此扩展允许开发者存储或恢复模型的状态,并提供一个用于操作多个状态的栈。

准备工作

您可以从GitHub页面github.com/derickbailey/backbone.memento下载Backbone.Memento扩展。要将此扩展包含到您的项目中,请将backbone.memento.js文件保存到lib文件夹中,并在index.html中包含对其的引用。

注意

在第一章中详细描述了将 Backbone 扩展包含到您的项目中,理解 Backbone扩展应用程序与插件配方。

如何实现...

执行以下步骤以操作模型状态:

  1. initialize()方法中扩展模型以使用Backbone.Memento扩展。

      var InvoiceItemModel = Backbone.Model.extend({
    
        // Extend model instance with memento instance.
        initialize: function() {
          _.extend(this, new Backbone.Memento(this));
        }
      });
    
  2. 创建模型实例并用值初始化它。

      var invoiceItemModel = new InvoiceItemModel();
      invoiceItemModel.set('price', 10);
    
  3. 使用store()方法保存状态。

      invoiceItemModel.store();
    
  4. 更新模型以使用临时值。

      invoiceItemModel.set('price', 20);
    
  5. 使用restore()方法检索之前保存的状态。

      invoiceItemModel.restore();
    
  6. 获取保存状态中的模型值。

      invoiceItemModel.get('price'); // 10
    

它是如何工作的...

Memento 使用LIFO后进先出)数据结构,也称为栈,用于存储模型状态。因此,可以多次保存模型状态,然后以反向顺序恢复它们。以下图表显示了它是如何工作的:

它是如何工作的...

每次调用store()方法时,状态都会被保存在栈顶。每次调用restore()方法时,最后保存的状态将被恢复并从栈顶删除。

更多...

在本节中,我们将了解 Memento 的高级功能。

使用 Memento 栈工作

下面是一个示例,演示如何处理这样的状态栈:

    // States stack demo.
    var invoiceItemModel2 = new InvoiceItemModel();
    invoiceItemModel2.set('price', 10);

    // Save state and update value.
    invoiceItemModel2.store();
    invoiceItemModel2.set('price', 20);

    // Save state and update value.
    invoiceItemModel2.store();
    invoiceItemModel2.set('price', 30);

    // Restore last state and get value.
    invoiceItemModel2.restore();
    invoiceItemModel2.get('price'); // 20

    // Restore last state and get value.
    invoiceItemModel2.restore();
    invoiceItemModel2.get('price'); // 10

如前述代码所示,处理栈相当简单。

从栈中的第一个状态恢复

有时,需要将模型重置为最初在堆栈中保存的状态,无论之后保存了多少状态。这可以通过使用restart()方法来完成。

    invoiceItemModel3.restart();

忽略被恢复的属性

Backbone.Memento中有一个有趣的功能,允许你忽略一些模型属性不被保存或恢复。如果一个模型包含一些技术属性,这些属性不是作为状态的一部分使用,这个功能就非常有用。当在initialize()方法中扩展模型时,将需要忽略的属性传递给ignore选项。

  var AnotherInvoiceItemModel = Backbone.Model.extend({

    // Extend model instance with memento instance.
    // Ignore restoring of description attribute.
    initialize: function() {
      _.extend(this, new Backbone.Memento(
        this, {ignore: ["description"]}
      ));
    }
  });

与集合一起工作

Memento 扩展还允许扩展集合以具有 Memento 功能。它在处理集合时提供相同的方法:store()restore()restart()

参见

  • 还有一个名为Backbone.actAs.Mementoable的扩展,它以更准确的方式实现了 Memento 模式,因为它使用单独的对象来存储状态。它更灵活,但默认不提供堆栈,并且不能忽略特定属性被保存/恢复。

  • Backbone.actAs.Mementoable可以从GitHub页面github.com/iVariable/Backbone.actAs.Mementoable下载。

  • 你可以在第三章 集合 中了解更多关于与集合一起工作的内容。

实现模型的流程

如果你正在实现一个业务逻辑,该逻辑假设模型可以处于不同的状态,并且对状态变化有特殊的规则,你应该使用workflow.js扩展,这对于构建此类功能非常有帮助。

准备工作

你可以从 GitHub 页面github.com/kendagriff/workflow.js下载workflow.js扩展。要将此扩展包含到你的项目中,将workflow.js文件保存到lib文件夹中,并在index.html中包含对其的引用。

注意

在第一章 理解 Backbone 的“通过插件扩展应用程序”配方中详细描述了如何将 Backbone 扩展包含到你的项目中。

如何做...

让我们为InvoiceModel创建一个流程,因为它有一个status属性,它表示模型状态,非常适合作为流程示例。

  1. 绘制状态和可能转换的图。如何做...

    可用的状态有草稿、已发行、已支付和已取消。还有一些允许一个状态变为另一个状态的转换。如果没有合适的转换,则这种变化是不可能的。

  2. 在代码中定义workflow

      var InvoiceModel = Backbone.Model.extend({
    
        // Define workflow states.
        workflow: {
    
          // Define initial state.
          initial: 'draft',
    
          // Define state transitions.
          events: [
            { name: 'issue', from: 'draft', to: 'issued' },
            { name: 'payout', from: 'issued', to: 'paid' },
            { name: 'cancel', from: 'draft', to: 'canceled' },
            { name: 'cancel', from: 'issued', to: 'canceled' },
          ]
        },
    
        initialize: function() {
          // Extend model instance with workflow instance.
          // Set attribute name which contains status.
          _.extend(this,
            new Backbone.Workflow(this, {attrName: 'status'})
          );
        }
      });
    

    如我们所见,有一个新的workflow属性,它描述了我们的流程。转换定义在一个数组中,该数组被分配给events属性。

    转换数组中的每个元素都应该包含转换名称、起始状态和目标状态。模型初始状态应在 initial 属性中定义。

    在前面的示例中,在 initialize() 方法中,我们通过 Backbone.Workflow 对象的实例扩展了我们的模型对象,并将状态属性名称 (attrName) 作为选项传递,其中包含 'status' 而不是默认值 'workflow_state'

  3. 通过调用 triggerEvent() 方法触发工作流转换。

        var invoiceModel = new InvoiceModel();
        invoiceModel.get('status'); // draft
    
        invoiceModel.triggerEvent('issue');
        invoiceModel.get('status'); // issued
    
        invoiceModel.triggerEvent('payout');
        invoiceModel.get('status') // paid
    

    如前述代码所示,triggerEvent() 接受一个参数,即转换名称。如果触发了不适当的转换,则会抛出异常。

它是如何工作的...

Workflow.js 扩展是用 CoffeeScript 编写的,并且很容易理解。它只提供了 triggerEvent() 方法,该方法切换 workflow 属性并触发事件。

更多内容...

在本节中,我们将学习如何处理转换事件。

绑定回调到转换事件

有时,你可能想在特定转换被触发时执行代码。在这种情况下,你需要将回调函数绑定到转换事件。如果正在触发事件,则执行此回调。

Workflow.js 提供了两种类型的事件 transition:from:*transition:to:*。第一种事件在工作流失去特定状态时触发,第二种事件在工作流达到特定状态时触发。让我们为我们的模型定义事件绑定。

  var InvoiceModel = Backbone.Model.extend({

    // Define workflow states.
    // [workflow definition goes here]

    initialize: function() {
      // Extend model instance with workflow instance.
      // Set attribute name which contains status.
      _.extend(this,
        new Backbone.Workflow(this, {attrName: 'status'})
      );

      // Bind reaction on event when status changes from
      // draft to any.
      this.bind('transition:from:draft', function() {
        this.set('createdDate', new Date().toISOString());
      });

      // Bind reaction on event when status changes
      // from any to paid.
      this.bind('transition:to:paid, function() {
        this.set('payoutDate', new Date().toISOString());
      });
    }
  });

在前面的示例中,我们绑定了一些回调函数,当适当的事件被触发时,这些回调函数会更新日期属性。

下面的代码片段是一个示例,演示了当工作流事件被触发时会发生什么。

    var invoiceModel = new InvoiceModel();
    invoiceModel.get('status'); // draft

    invoiceModel.triggerEvent('issue');
    invoiceModel.get('status'); // issued
    invoiceModel.get('createdDate');
    // 2012-05-01T12:00:10.234Z

    invoiceModel.triggerEvent('payout');
    invoiceModel.get('status') // paid
    invoiceModel.get('payoutDate');
    // 2012-05-01T12:00:10.238Z

小贴士

始终在改变状态时使用 triggerEvent() 方法

只有当事件由 triggerEvent() 方法触发时,事件回调才会执行。这就是为什么当对象被初始化或者使用 set() 方法更新工作流状态时,事件回调不会执行。

参见

  • 在 第五章 事件和绑定处理 Backbone 对象的事件

在模型中使用高级验证

默认情况下,Backbone 提供了一种简单的方法来使用 validate() 方法验证模型属性,这允许你创建自己的验证函数,但与使用现有解决方案相比,这可能会花费开发者更多的时间。

准备工作

你为什么不使用另一个名为 Backbone.Validation 的 Backbone 扩展来节省时间,它提供了许多功能,并允许重用现有的验证器。它可以从 GitHub 页面 github.com/thedersen/backbone.validation 下载。

要将此扩展包含到你的项目中,将 backbone-validation.js 文件保存到 lib 文件夹中,并在 index.html 中包含对其的引用。

注意

在第一章的通过插件扩展应用食谱中详细描述了将 Backbone 扩展包含到你的项目中。

如何做到这一点...

执行以下步骤以设置模型的验证标准:

  1. 使用Backbone.Validation.mixin扩展Backbone.object()

      _.extend(Backbone.Model.prototype, Backbone.Validation.mixin);
    

    注意

    在第八章的使用 mixins 与 Backbone 对象食谱中可以找到更多关于 mixins 的信息,特殊技术

  2. validation属性中定义验证标准。

      var BuyerModel = Backbone.Model.extend({
    
        // Defining a validation criteria.
        validation: {
          name: {
            required: true
          },
          email: {
            pattern: 'email'
          }
        }
      });
    

它是如何工作的...

Backbone.Validation扩展覆盖了Backbone.Modelvalidate()方法,因此我们仍然可以像往常一样调用validate()isValid()方法,并且当模型更新时,验证会自动执行。让我们看看这个例子。

    var buyerModel = new BuyerModel();

    // Set attribute values which do not validate.
    buyerModel.set({
      email: 'http://example.com'
    }, {validate: true});

    // Check if model is valid.
    buyerModel.isValid(); // false
    buyerModel.get('email'); // undefined

还有更多...

在本节中,我们将学习更多关于内置验证器的知识。

使用内置验证器

在上一个示例中,我们重用了现有的验证器,例如requiredpattern。它们被称为内置验证器。在本食谱中,我们将学习如何使用它们的所有功能。

  • required: 它验证属性是否必需。它可以等于 true 或 false。

      var BuyerModel = Backbone.Model.extend({
        validation: {
          name: {
            required: true
          },
        }
      });
    
  • acceptance: 它验证是否必须接受某些内容,例如使用条款。它检查属性值是否为 true 或 false。它与boolean属性一起工作。

       var UserRegistrationModel = Backbone.Model.extend({
        validation: {
          terms: {
            acceptance: true
          },
        }
      });
    
  • min: 它验证属性值必须是一个数字,并且等于或大于指定的最小值。

      var BuyerModel = Backbone.Model.extend({
        validation: {
          age: {
            min: 18
          },
        }
      });
    
  • max: 它验证属性值必须是一个数字,并且等于或小于指定的最大值。

      var EventRegistrationModel = Backbone.Model.extend({
        validation: {
          guests: {
            max: 2
          },
        }
      });
    
  • range: 它验证属性值必须是一个数字,并且等于或介于指定的两个数字之间。

      var ChildTicketModel = Backbone.Model.extend({
        validation: {
          age: {
            range: [2, 12]
          },
        }
      });
    
  • 长度: 它验证属性值必须是一个字符串,其长度等于指定的长度值。

      var AddressModel = Backbone.Model.extend({
        validation: {
          zip: {
            length: 5
          },
        }
      });
    
  • minLength: 它验证属性值必须是一个字符串,其长度等于或大于指定的最小长度值。

      var UserModel = Backbone.Model.extend({
        validation: {
          password: {
            minLength: 8
          },
        }
      });
    
  • maxLength: 它验证属性值必须是一个字符串,其长度等于或小于指定的最大长度值。

      var UserModel = Backbone.Model.extend({
        validation: {
          password: {
            maxLength: 8
          },
        }
      });
    
  • rangeLength: 它验证属性值必须是一个字符串,并且等于或介于指定的两个数字之间。

      var BuyerModel = Backbone.Model.extend({
        validation: {
          phoneNumber: {
            rangeLength: [10, 12]
          },
        }
      });
    
  • oneOf: 它验证属性值必须等于指定数组中的某个元素。它使用区分大小写的匹配。

      var BuyerModel = Backbone.Model.extend({
        validation: {
          type: {
            oneOf: [''person'', ''organization'']
          },
        }
      });
    
  • equalTo: 它验证属性值必须等于指定名称的属性的值。

      var UserModel = Backbone.Model.extend({
        validation: {
          password: {
            required: true
          },
          passwordRepeat: {
            equalTo: 'password'
          }
        }
      });
    
  • pattern: 它验证属性值必须匹配指定的模式。它可以是一个正则表达式,或者内置模式之一的名字。

    var BuyerModel = Backbone.Model.extend({
      validation: {
        email: {
          pattern: 'email'
        }
      }
    });
    

    模式可以接受以下属性值之一:

    • number: 匹配任何十进制数字

    • digits: 匹配任何数字序列

    • email: 匹配有效的电子邮件地址

    • url: 匹配任何有效的 URL

    您也可以指定任何正则表达式。

    var BuyerModel = Backbone.Model.extend({
      validation: {
        phoneNumber: {
          pattern: /^(\+\d)*\s*(\(\d{3}\)\s*)*\d{3}(-{0,1}|\s{0,1})\d{2}(-{0,1}|\s{0,1})\d{2}$/
        }
      }
    });
    

参见

  • 在这个菜谱中,我们学习了Backbone.Validation扩展的基础知识,尽管还有更多技术可以在GitHub文档页面github.com/thedersen/backbone.validation上找到。

  • Backbone.Validation还有一些替代方案。它们是Backbone.validationsBackbone.Validator扩展。它们都非常相似,但Backbone.Validation有更好的文档,并提供更多方法和事件。

验证 HTML 表单

大多数网络应用程序使用 HTML 表单进行数据输入,Backbone 也不例外。应用程序应该让用户了解任何验证错误。此类功能的实现可能落在开发者的肩上,但不是在 Backbone 中!

幸运的是,Backbone.Validation提供了与视图的集成,并且与 HTML 表单配合良好。

准备工作

确保您已安装Backbone.Validation扩展。安装方法在之前的菜谱在模型中使用高级验证中描述。

如何操作...

执行以下步骤以验证表单:

要允许表单验证,我们需要在视图的initialize()方法中将视图绑定到Backbone.Validation对象。

Backbone.Validation.bind(this);

Backbone.Validation假设您的模型存储在this.model中,并且您已经实现了从表单元素获取数据并使用它更新模型值。

它是如何工作的...

如果用户输入的信息无法验证,那么Backbone.Validation会将invalid类添加到适当的表单元素中,并使用错误消息设置data-error属性。

注意

data-*属性是 HTML5 的一个特性。它们可以通过 CSS3 或自定义 JavaScript 轻松显示。它们也受到主要前端框架的支持,如 jQueryMobile 或 Twitter Bootstrap。

以下截图说明了Backbone.Validation如何验证 HTML 表单中输入的错误数据:

它是如何工作的...

还有更多...

以下代码片段是此菜谱示例的完整列表:

(function($){

  // Define new model.
  var BuyerModel = Backbone.Model.extend({
    defaults: {
      name: '',
      age: ''
    },

    // Define validation criteria.
    validation: {
      name: {
        required: true
      },
      age: {
        min: 18
      }
    }
  });

  var BuyerModelFormView = Backbone.View.extend({

    // Bind Backbone.Validation to a view.
    initialize: function(){
      Backbone.Validation.bind(this);
    },

    // Define a template.
    template: _.template('\
      <form>\
        Enter name:\
        <input name="name" type="text" value="<%= name %>"><br>\
        Enter age:\
        <input name="age" type="text" value="<%= age %>"><br>\
        <input type="button" name="save" value="Save">\
      </form>\
    '),

    // Render view.
    render: function(){
      // Render template with model values.
      var html = this.template(this.model.toJSON());

      // Update html.
      $(this.el).html(html);
    },

    // Bind save callback click event.
    events: {
      'click [name~="save"]': 'save'
    },

    // Save callback.
    save: function(){

      // Update model attributes.
      this.model.set({
        name: $('[name~="name"]').val(),
        age: $('[name~="age"]').val()
      });
    }
  });

  $(document).ready(function () {
     // Create new model instance.
     var buyerModel = new BuyerModel();

     // Create new view instance.
     var buyerModelFormView = new BuyerModelFormView({
       model: buyerModel,
       el: 'body'
     });

     // Render view.
     buyerModelFormView.render();
  });
})(jQuery);

在模型中处理嵌套属性

有时需要嵌套属性来操作存储在模型中的复杂层次结构。这通常是通过使用 JavaScript 对象作为嵌套属性来完成的;然而,这不是 Backbone 的方式。幸运的是,有Backbone-Nested扩展,它在使用嵌套属性时提供了各种改进。

准备工作

您可以从GitHub页面github.com/afeld/backbone-nested下载Backbone-Nested扩展。要将此扩展包含到您的项目中,将backbone-nested.js文件保存到lib文件夹中,并在index.html中包含对其的引用。

注意

在 第一章 的 使用插件扩展应用程序 菜谱中描述了将 Backbone 扩展添加到您的项目中,详细理解 Backbone

如何做到...

要在模型中使用嵌套属性,请执行以下步骤:

  1. 在扩展新模型时,使用 Backnone.NestedModel 作为基础对象。

      var BuyerModel = Backbone.NestedModel.extend({
    
      });
    
  2. 使用 Backbone-Nested 扩展提供的点语法设置嵌套属性值。

        buyerModel.set({
          'name.title': 'Mr',
          'name.generation': 'II'
        });
    

    您仍然可以使用典型的 JavaScript 对象语法来设置多个值。

        buyerModel.set({
          name: {
            first: 'John',
            last: 'Smith',
            middle: {
              initial: 'P',
              full: 'Peter'
            }
          }
        });
    
  3. 使用点语法获取属性值。

        buyerModel.get('name.middle.full'); // Peter
        buyerModel.get('name.middle');
        // { full: 'Peter', initial: 'P }
        buyerModel.get('name.title'); // Mr
    

它是如何工作的...

Backbone-Nested 扩展基于 Backbone.Model 提供了一个新的模型对象 Backbone.NestedModel。它覆盖了现有方法,如 get()set()has()toJSON 等。它还提供了新的 add()remove() 方法。

还有更多...

本节描述了 Backbone-Nested 扩展的高级用法。

使用嵌套属性数组进行操作

当然,还有处理更复杂结构(如嵌套属性数组)的方法。您也可以使用对象语法来设置它。

    buyerModel.set({
      'addresses': [
        {city: 'Brooklyn', state: 'NY'},
        {city: 'Oak Park', state: 'IL'}
      ]
    });

或者,您可以使用点和中括号语法在嵌套数组中设置属性,如下面的示例所示:

    buyerModel.set({
      'addresses[1].state': 'MI'
    });

并且相同的语法也用于从嵌套数组中获取属性。

    buyerModel.get('addresses[0].state'); // NY
    buyerModel.get('addresses[1].state'); // MI

向/从嵌套数组中添加/删除元素

Backbone-Nested 提供了处理嵌套数组的一些额外方法。add 方法可以向嵌套数组中添加新元素。以下是它的工作方式。

    buyerModel.add('addresses', {
      city: 'Seattle',
      state: 'WA'
    });

    buyerModel.get('addresses[2]');
    // { city: 'Seattle', state: 'WA' }

remove() 方法从嵌套数组中删除所需的元素。让我们看看它是如何完成的。

    buyerModel.remove('addresses[1]');

    buyerModel.get('addresses').length; // 2

将回调绑定到事件

当将回调绑定到事件时,您可以使用之前描述的相同的点和中括号语法。让我们看看以下将回调绑定到事件的示例:

    buyerModel.bind('change:addresses[0].city', function(model, value){
      console.log(value);
    });

    buyerModel.set('addresses[0].city', 'Chicago');

此外,Backbone-Nested 还提供了额外的 add:*remove:* 事件来处理数组更新事件。

参见

  • 在 第五章 的 处理 Backbone 对象的事件 菜谱中可以找到更多关于事件处理的信息,事件和绑定

  • Backbone-Nested 扩展有一些替代方案,例如 Backbone-deep-modelBackbone-dotattr。它们都非常相似,但 Backbone-Nested 提供了更多功能,并且维护得更好。

实现一对一关系

在任何应用程序中,我们可能都需要具有相互关联的模型。例如,博客文章模型可以与其作者的模型或评论模型相关联。

在处理博客文章时,我们可能还需要快速访问评论,或者列出特定作者的博客文章列表。此外,我们可能希望以单个 JSON 格式导出包含作者信息和评论的博客文章。

在 Backbone 应用程序中,这可以通过 Backbone-relational 扩展来实现。

准备工作

您可以从 GitHub 页面 github.com/PaulUithol/Backbone-relational 下载 Backbone-relational 扩展。要将 Backbone-relational 包含到您的项目中,将 backbone-relational.js 文件保存到 lib 文件夹,并在 index.html 中包含对其的引用。

注意

在 第一章 的 使用插件扩展应用程序 菜单中详细描述了将 Backbone 扩展包含到您的项目中。

如何实现...

让我们回顾我们的发票应用程序,并尝试找出我们如何在其中应用一对一关系。假设我们希望买家登录应用程序并查看分配给他们的所有发票。

在这种情况下,我们需要将买家凭证存储在某个地方。这可以是一个与现有 BuyerModel 关联的新 UserModel。我们知道每个用户对应一个买家,反之亦然,所以我们处理的是一对一关系。让我们实现这样一个一对一关系。

  1. Backbone.RelationalModel 扩展模型,并传递带有关系定义的 relations 属性。

      // Define new model object.
      var UserModel = Backbone.RelationalModel.extend({
    
      });
    
      // Define new model object.
      var BuyerModel = Backbone.RelationalModel.extend({
    
        // Define one-to-one relationship.
        relations: [
          {
             // Relationship type
             type: Backbone.HasOne,
    
             // Relationship key in BuyerModel.
             key: 'user',
    
             // Related model.
             relatedModel: UserModel,
    
             // Define reverse relationship.
             reverseRelation: {
               type: Backbone.HasOne,
               key: 'buyer'
             }
          }
        ]
      });
    

    如前例所示,relations 属性接受一个数组,因此可能存在多个关系。

    小贴士

    注意,UserModel 应该在 BuyerModel 之前定义,因为它在代码中之后被引用(在 BuyerModelrelations 属性中)。

  2. 通过在 BuyerModel 实例中引用 UserModel 实例或反之亦然来初始化一对一关系。

        var userModel1 = new UserModel({
          login: 'jsmith',
          email: 'jsmith@example.com'
        });
    
        var buyerModel1 = new BuyerModel({
          firstName: 'John',
          lastName: 'Smith',
          user: userModel1
        });
    

    在创建 BuyerModelUserModel 时,也可以通过传递单个输入 JSON 来实现相同的效果。

        var buyerModel = new BuyerModel({
          firstName: 'John',
          lastName: 'Smith',
          user: {
            login: 'jsmith',
            email: 'jsmith@example.com'
          }
        });
    
  3. 如果定义了反向关系,则在初始化 UserModel 时传递一个 BuyerModel 数组。

        var userModel = new UserModel({
          login: 'jsmith',
          email: 'jsmith@example.com',
          buyer: {
            firstName: 'John',
            lastName: 'Smith'
          }
        });
    
  4. 可选地,使用 get() 方法帮助访问相关模型。

        buyerModel1.get('user').get('email');
        // jsmith@example.com
        userModel1.get('buyer').get('lastName'); // Smith
    

它是如何工作的...

每个 Backbone.RelationalModel 在创建时会将自己注册到 Backbone.Store(在销毁时从 Store 中移除)。当创建或更新一个关系中的键属性时,被移除的相关对象会收到移除通知,而新的相关对象会在 Store 中查找。

参考以下内容

  • 一对多关系和多对多关系在 第三章 的 实现一对一关系 菜单中描述,集合

  • Backbone-relational 扩展的完整文档可以在 GitHub 页面 github.com/PaulUithol/Backbone-relational 上找到。

  • 此外,还有一些与 Backbone-relational 非常相似但称为 Backbone-associationsligament.js 的替代方案。然而,它们不提供一对一和多对多关系。

第三章. 集合

在本章中,我们将介绍:

  • 创建模型集合

  • 通过索引从集合中获取模型

  • 通过 ID 从集合中获取模型

  • 将模型添加到集合中

  • 从集合中删除模型

  • 将集合作为栈或队列进行操作

  • 对集合进行排序

  • 在集合中过滤模型

  • 遍历集合

  • 链接集合

  • 在集合上运行 No SQL 查询

  • 在同一集合中存储不同类型的模型

  • 实现一对一关系

简介

当使用 Backbone 开发应用程序时,你通常需要与多个模型一起工作,这些模型可以组织到集合中。集合不仅仅是 JavaScript 数组。Backbone 提供了各种有用的方法来处理它。此外,Backbone 集合可以轻松与 REST 服务器通信以获取或发布多个模型。

在本章中,我们将学习与集合一起工作的常见操作,并将发现提供惊人功能的新扩展。

创建模型集合

在本食谱中,我们将学习如何创建模型集合。集合是一个用于将模型组织成有序集的对象。有特定的方法可以对集合进行排序、过滤和迭代。

如何做到...

按照以下步骤创建集合:

  1. 扩展Backbone.Collection对象并传递模型的对象名称作为选项。

    var InvoiceItemCollection = Backbone.Collection.extend
    ({
      model: InvoiceItemModel
    });
    
  2. 初始化一个新的集合实例并传递初始的模型数组。

    var invoiceItemCollection = new InvoiceItemCollection
    ([
      {description: 'Wooden Toy House', price: 22, quantity: 3},
      {description: 'Farm Animal Set', price: 17, quantity: 1},
      {description: 'Farmer Figure', price: 8, quantity: 1},
      {description: 'Toy Tractor', price: 15, quantity: 1}
    ]);
    

它是如何工作的...

Backbone.Collection知道在创建新实例时使用哪个模型对象,因为我们已在model属性中指定了它。内部,模型存储在models数组中。

更多...

我们还可以使用现有的模型初始化集合。以下是操作方法。

invoiceItemModel1 = new InvoiceItemModel
  ({
    description: 'Wooden Toy House',
    price: 22,
    quantity: 3
  });
invoiceItemModel2 = new InvoiceItemModel
  ({
    description: 'Farm Animal Set',
    price: 17,
    quantity: 1
  });
var invoiceItemCollection2 = new InvoiceItemCollection
  ([
    invoiceItemModel1,
    invoiceItemModel2
  ]);

通过索引从集合中获取模型

当与集合一起工作时,我们可能需要获取特定索引处的模型,因为它存储在集合内部。

如何做到...

使用at()方法从集合的特定索引处获取模型。

var model = invoiceItemCollection.at(2);
model.get('description'); // Farmer Figure

它是如何工作的...

内部,模型存储在models数组中,因此第一个元素从零索引开始。Backbone.Collection在向集合添加新模型、从集合中删除一个模型或执行排序时保持此数组的状态准确。

小贴士

排序集合时要小心

在执行集合操作时,排序可能会更新模型索引,因此具有相同参数的at()方法在排序前后可能获取不同的模型。

更多...

在本节中,我们将了解有关集合中模型的一些有趣细节。

获取集合模型的索引

要获取存储在集合中的模型的索引,请使用从Underscore.js继承的indexOf()方法。

invoiceItemCollection.indexOf(model); // 2

获取模型的独立副本

从集合中检索的模型对象与存储在该处的对象相同,因此如果我们修改此对象,集合中的一个对象会得到更新。

model.set('description', 'Superman Figure');
invoiceItemCollection.at(2).get('description');
// Superman Figure

如果我们需要获取模型对象的独立副本,我们可以使用返回的模型对象的 clone() 方法。修改克隆模型的属性不会影响原始模型的属性。

var anotherModel = invoiceItemCollection.at(2).clone();
anotherModel.set('description', 'Another Figure');
invoiceItemCollection.at(2).get('description');
// Superman Figure

获取集合的长度

有一种方法可以获取集合的长度。这是通过 length() 方法完成的。以下示例获取集合长度,然后从集合中获取最后一个模型:

var length = invoiceItemCollection.length; //4
model = invoiceItemCollection.at(length-1);
model.get('description'); // Toy Tractor

通过 ID 从集合中获取模型

在我们的应用程序中,我们可能需要通过其 ID 请求集合中的模型。

如何做到...

按以下步骤通过 ID 从集合中获取模型:

  1. 要通过其标识符从集合中获取模型,请使用 get() 方法。

    model = invoiceItemCollection2.get('4ryurtz3m5gn9udi');
    
  2. 要通过其客户端标识符从集合中获取模型,您还可以再次使用 get() 方法。

    model = invoiceItemCollection.get('c4');
    model.get('description'); // Toy Tractor
    

它是如何工作的...

当通过其 ID 获取模型时,Backbone.Collection 会搜索 _byId 数组中的模型,该数组存储了映射到其 ID 的模型。这种实现保证了最佳性能,因为无需遍历集合中的所有模型。

参考以下内容

  • 在 第二章 模型使用插件扩展应用程序

向集合中添加模型

在本食谱中,我们将学习向集合添加新模型的不同方法。

如何做到...

调用 add() 方法将新模型添加到集合的末尾。

invoiceItemCollection.add
  ({
    description: 'Toy Track',
    price: 10,
    quantity: 1
  });

它是如何工作的...

add() 方法中的代码防止重复添加到集合中。唯一模型被插入到 models 数组中,并在 _byId 数组中映射到其 ID。此外,在 collection 属性中创建了对集合的引用。

默认情况下,新模型被添加到集合的末尾。但如果启用了排序或指定了插入索引,模型可以插入到不同的位置。

当向集合中添加新模型时,会触发 add 事件。

更多内容...

在本节中,我们将学习将模型(s)添加到集合的不同方法。

在特定位置添加模型

要在特定位置添加模型,我们需要传递 {at: index} 作为选项。

invoiceItemCollection.add
  (
    {description: 'Fisherman Hut', price: 15, quantity: 1},
    {at: 0}
  );
invoiceItemCollection.at(0).get('description');
// Fisherman Hut

添加多个模型

我们还可以同时添加多个模型。

invoiceItemCollection.add
  ([
    {description: 'Powerboat', price: 12, quantity: 1},
    {description: 'Jet Ski', price: 12, quantity: 1}
  ]);

添加现有模型

我们还可以使用现有的模型对象作为 add() 方法的参数。我们可以传递单个对象以及现有对象的数组。

参考以下内容

  • 在 第五章 事件和绑定处理 Backbone 对象的事件

从集合中移除模型

在本食谱中,我们将学习如何从集合中移除模型。

如何做到...

调用 remove() 方法从集合中移除模型。

invoiceItemCollection.remove(['c0', 'c1', 'c2', 'c3']);

在这里,我们可以传递模型的 idcid,甚至模型对象作为参数。我们可以传递单个值或值的数组。

它是如何工作的...

当调用 remove() 方法时,模型将从 models 数组中删除,并且它们之间的任何引用也将被删除。因此,模型对象本身不会被销毁,如果需要,我们仍然可以与之交互。

还有更多...

有时,我们可能需要从集合中删除所有现有的模型并添加一些其他模型。有一个有用的 reset() 方法,它可以同时完成这两项工作。以下是它是如何工作的。

invoiceItemCollection.reset
  ([
    {description: 'Wooden Toy House', price: 22, quantity: 3},
    {description: 'Farm Animal Set', price: 17, quantity: 1}
  ]);

将集合作为栈或队列使用

Backbone 中有一些特殊的方法允许将集合作为栈或队列使用。

如何操作...

按照以下步骤将集合作为栈或队列使用:

  1. 调用 push() 方法将模型添加到集合的末尾。

    invoiceItemCollection.push(model);
    
  2. 调用 pop() 方法从集合中移除并返回最后一个模型。

    model = invoiceItemCollection.pop();
    
  3. 调用 unshift() 方法在集合的开始处添加一个模型。

    invoiceItemCollection.unshift(model);
    
  4. 调用 shift() 方法从集合中移除并返回第一个模型。

    model = invoiceItemCollection.shift();
    

它是如何工作的...

要组织一个栈,也称为 LIFO(后进先出),我们需要使用 pushpopunshiftshift)方法。要组织一个队列,也称为 FIFO(先进先出),我们需要使用 unshiftpoppushshift)方法。

以下图像展示了栈和队列之间的区别:

如何工作...

排序集合

Backbone.js 提供了一个开箱即用的排序机制,我们将在本菜谱中学习。

如何操作...

按照以下步骤对集合进行排序:

  1. comparator 回调函数赋值给集合的 comparator 属性,以保持正确的顺序。

    invoiceItemCollection.comparator = function(model)
      {
        return model.get("price");
      };
    
  2. comparator 回调函数接受一个参数,即模型对象。它应该返回一个值,根据该值对集合进行排序。

  3. 可选地,调用 sort() 方法强制排序。

    invoiceItemCollection.sort();
    
  4. 检查结果。

    invoiceItemCollection.pluck("price"); // [8, 15, 17, 22]
    

它是如何工作的...

comparator 回调函数被定义时,Backbone 使用它将新模型插入到 models 数组中,以便按正确顺序插入。

如果你将新的 comparator 回调函数分配给具有现有模型的集合,你需要通过调用 sort() 方法手动触发排序。

如果集合中的模型被更新,你也需要调用 sort() 方法。这可以通过在模型的 change 事件上绑定排序来自动完成。

还有更多...

在本节中,我们将以不同的方式定义比较器。

在比较器中比较一对模型

实现比较器的另一种方式是评估作为参数传递的模型对,并返回以下值之一:

  • -1(或任何负值),如果第一个模型应该在第二个模型之前

  • 0,如果它们处于相同的等级

  • 1(或任何正值),如果第一个模型应该在第二个模型之后

以下示例演示了按 description 属性的长度进行排序:

invoiceItemCollection.comparator = function(m1, m1)
  {
    return m1.get("description").length -
    m2.get("description").length;
  };
invoiceItemCollection.sort();
invoiceItemCollection.pluck("description");
// "Toy Tractor", "Farmer Figure", "Farm Animal Set",
// "Wooden Toy 

参见

  • 在 [第五章 事件和绑定 中处理 Backbone 对象的事件...

过滤集合中的模型

Backbone 提供了一种简单的过滤机制,我们可以使用它。

如何操作...

要过滤集合中的模型,请使用 where() 方法。它接受一个搜索标准并返回找到的模型数组。

var result = invoiceItemCollection.where({quantity: 1});
// Result is just an array of models, so let's create
// new collection.
var resultCollection = new InvoiceItemCollection(result);
resultCollection.pluck('quantity'); // [1, 1, 1]

也可以一起传递多个标准。

invoiceItemCollection.where({quantity: 1, price: 10});

参见

  • 参考关于在集合上运行 No SQL 查询的配方,以了解更多关于高级过滤的信息

遍历集合

在本配方中,我们将讨论通过遍历集合以实现所需功能的各种方法。

如何操作...

遍历集合的最简单方法是使用 Underscore.js 提供的 each() 方法。

var  descriptions_txt = '';
invoiceItemCollection.each(function(model, index, list)
  {
    descriptions_txt += descriptions_txt ? ', ' : ''; 
    descriptions_txt += model.get('description');
  });
descriptions_txt; // Wooden Toy House, Farm Animal Set

each() 方法中,我们传递一个迭代函数,该函数对每个模型执行。它接受以下参数:

  • 模型:正在迭代的模型

  • 索引:这是模型索引

  • 列表:这是整个模型数组

工作原理...

Backbone.js 基于 Underscore.js,它提供了各种有用的工具,包括用于处理集合和数组的各种方法。Backbone 集合支持其中的一些功能。

还有更多...

在本节中,我们将学习一些依赖于迭代方法但更为具体的方法。

检查每个模型以匹配特定条件

要检查满足特定条件的集合中的每个模型,请使用 every() 方法。它接受一个回调参数,如果条件满足,则应返回一个 Boolean 值。

var multiple = invoiceItemCollection.every(function(model)
  {
    return model.get('quantity') > 1;
  });
multiple; // false

检查任何模型以匹配特定条件

要检查集合中满足特定条件的任何模型,请使用 some() 方法。它接受一个回调参数,如果条件满足,则应返回一个 Boolean 值。

var multiple = invoiceItemCollection.some(function(model)
  {
    return model.get('quantity') > 1;
});
multiple; // true

从集合中的每个模型获取属性

在前面的示例中,我们使用了 pluck() 方法,该方法从集合中的每个模型返回指定属性的值数组。让我们看看它是如何工作的。

var descriptions = invoiceItemCollection.pluck("description");
descriptions; // ["Wooden Toy House", "Farm Animal Set"]

对集合中的每个模型执行特定计算

要对集合中的每个模型执行特定计算,请使用 map() 方法。它接受回调作为参数,对集合中的每个模型执行它,并返回结果数组。

var amounts = invoiceItemCollection.map(function(model)
  {
    return model.get('quantity') * model.get('price');
  });
amounts; // [66, 77]

将集合中的模型简化为一个单一值

使用 reduce() 方法可以将集合中的模型简化为一个单一值。以下是其工作原理。

var count = invoiceItemCollection.reduce(function(memo,model)
  {
    return memo + model.get('quantity');
  }, 0);
count; // 4

参见

连接集合

如果你想连续执行多个 Underscore 方法,一种好的做法是将一个方法链接到另一个方法上。

让我们考虑一个简单的 MapReduce 示例,它计算总金额。

var amounts = invoiceItemCollection.map(function(model)
  {
    return model.get('quantity') * model.get('price');
  });
// [66, 77]
var total_amount = _.reduce(amounts, function(memo, val)
  {
    return memo + val;
  }, 0);
total_amount; // 83

在这里,amounts 是一个 JavaScript 数组,它不提供我们可以调用的 reduce() 方法。为了解决这个问题,我们调用由 Underscore.js 提供的 reduce() 方法,它将数组作为第一个参数。

如何操作…

使用链接,可以使用点符号直接调用一个方法然后是另一个方法。以下是一个示例。

var amount = invoiceItemCollection.chain()
.map(function(model)
  {
    return model.get('quantity') * model.get('price');
  })
.reduce(function(memo, val)
  {
    return memo + val;
  })
.value(); // 83

它是如何工作的...

chain() 方法将一个值包装成一个对象,该对象提供了可以执行的不同方法,这些方法返回一个包装后的值。要解包一个结果,请使用 value() 方法。

参见

在集合上运行 No SQL 查询

在前面的配方中,我们描述了多种技术,包括使用 where() 方法在集合中搜索模型的技术。

有更多高级的方法可以在集合中搜索模型,这可以通过名为 Backbone Query 的 Backbone 扩展来实现。它允许运行 No SQL(如 MongoDB)查询以搜索、排序和分页集合中的模型。

准备工作

你可以通过访问 github.com/davidgtonge/backbone_query 下载 Backbone Query 扩展。要将此扩展包含到你的项目中,将 backbone-query.js 文件保存到 lib 文件夹中,并在 index.html 中包含对其的引用。

注意

在 第一章 的 使用插件扩展应用程序 配方中详细描述了将 Backbone 扩展包含到你的项目中,理解 Backbone

如何操作...

按照以下步骤执行对集合的 No SQL 查询:

  1. 要允许执行 No SQL 查询,请从 Backbone.QueryCollection 对象扩展集合而不是从 Backbone.Collection 对象扩展。

    var BuyerCollection = Backbone.QueryCollection.extend
      ({
        model: BuyerModel
      });
    
  2. 使用 query() 方法运行查询。

    var result = buyerCollection.query({ firstName: 'John' });
    
  3. 可选地运行结果数组的 pluck 属性。

    var resultCollection = new BuyerCollection(result);
    resultCollection.pluck('firstName'); // ["John", "John"]
    

它是如何工作的...

Backbone.QueryCollection 扩展 Backbone.Collection 并提供了新的 query() 方法,该方法递归地将基本查询解析为子查询,并使用 Underscore.jsreduce() 方法依次运行相同组的查询。

Backbone Query 最初是用 CoffeeScript 编写的,后来编译成 JavaScript。所以,如果你对理解其源代码感兴趣,请查看 backbone-query.coffee。尽管如此,它看起来相当相似。

更多内容...

本节描述了 No SQL 操作符,并涵盖了一些高级主题,如分组、排序、分页和缓存。

使用标准操作符

以下运算符是常见的,并应用于存储在集合中的模型的属性。

$equal

它使用===执行严格的相等性测试。

buyerCollection.query({ firstName: {$equal: 'John'} });

如果没有提供运算符,并且查询值既不是正则表达式也不是数组,则默认假设为$equal

buyerCollection.query({ firstName: 'John' });

$ne

这意味着不等于,与$equal相反,并返回所有不等于查询值的模型。

buyerCollection.query({ firstName: {$ne: 'John'} });

$in

可以使用$in提供一组可能的值;如果提供的任何值匹配,则返回模型。

buyerCollection.query({ firstName: {$in: ['John', 'Joe','Patrick']} });

$nin

这意味着不在,与$in相反,如果提供的所有值都不匹配,则返回模型。

buyerCollection.query
({ firstName: {$nin: ['Samuel', 'Victor']} });

$exists 或 $has

这检查属性的存在,可以提供truefalse

buyerCollection.query({ middleName: {$exists: true} });

buyerCollection.query({ middleName: {$has: false} });

组合查询

可以将多个查询组合在一起。有$and$or$nor$not运算符,我们将在稍后学习。

$and

这是一个逻辑与运算符。以下查询选择所有名为 John 且住在 Alexandria 的买家:

buyerCollection.query({ $and: {firstName: 'John', city: 'Alexandria'}});

$and运算符在没有提供组合运算符时用作粘合剂。

buyerCollection.query
({ firstName: 'John', city: 'Alexandria' });

$or

这是一个逻辑或运算符。以下查询选择所有名为 John 或住在 Alexandria 的买家:

buyerCollection.query
({ $or: {firstName: 'John', city: 'Alexandria'}});

$nor

这与$or相反。以下查询选择所有名字不是 John 或不住在 Alexandria 的买家:

buyerCollection.query
({ $nor: {firstName: 'John', city: 'Alexandria'}});

$not

这是$and的反面。以下查询选择所有除了名字是 John 且住在 Alexandria 的买家以外的买家:

buyerCollection.query
({ $not: {firstName: 'John', city: 'Alexandria'}});

对同一键的多个查询

如果我们需要对同一键执行多个查询,则可以将查询作为数组提供。以下查询返回所有名为 John 或 Joe 的客户:

buyerCollection.query
  ({
    $or:[
      { firstName: 'John' },
      { firstName: 'Joe' }
    ]
  });

排序查询结果

要按属性排序结果,我们需要在第二个参数中传递它,并使用sortBy键。我们还可以通过传递带有sort键的ascdesc值来指定顺序。默认情况下,假设值为asc。以下代码显示了如何进行排序:

result = buyerCollection.query
  (
    { firstName: {$like: 'John'} },
    { sortBy: 'lastName', order: 'desc' }
  );
resultCollection = new BuyerCollection(result);
resultCollection.pluck('lastName'); // ["Smith", "Doe"]

分页查询结果

有一种方法可以将大结果数组分成几个页面,并返回指定的一个。让我们看看它是如何完成的。

buyerCollection.query
({firstName: 'John'}, {limit:10, offset:1, page:2});

我们可以在第二个参数中指定以下属性:

  • limit: 它将结果数组的大小限制为给定的数字。返回前 N 个元素。这是一个必需属性。

  • page: 它返回指定结果的页面。页面大小由 limit 属性设置。这是一个可选属性。

  • offset: 它跳过前 N 个结果项。这是一个可选属性。

缓存结果

由于性能原因,我们可能想要缓存我们的结果。这可以大大减少查询执行时间,尤其是在使用分页时,因为未分页的结果被保存在缓存中,用户可以快速浏览其页面。

要启用缓存,只需在第二个参数中使用cache属性。

buyerCollection.query
({firstName: 'John'}, {limit:10, page:2, cache: true});

小贴士

缓存默认未设置,因为没有自动刷新缓存的方法,所以当启用缓存且集合正在更新时,缓存就会过时。

您应该意识到这个问题,并且每次集合或其中的模型更新时都手动执行缓存刷新。这可以通过调用reset_query_cache()方法来完成。

我们可以将集合的change事件绑定到reset_query_cache()方法,从而在集合更新时提供自动缓存刷新。

var BuyerCollection = Backbone.QueryCollection.extend
  ({
    initialize: function(){
      this.bind('change', this.reset_query_cache, this);
    }
  });

参见

在同一个集合中存储不同类型的模型

当构建复杂的 Backbone 应用程序时,您可能需要处理不同类型的模型,这些模型应以类似的方式处理,因此您可能希望它们存储在同一个集合中。幸运的是,有一个Backbone.Chosen扩展允许我们这样做。

准备工作

您可以从以下页面找到并下载Backbone.Chosengithub.com/asciidisco/Backbone.Chosen。要将Backbone.Chosen包含到您的项目中,将backbone.chosen.js文件保存到lib文件夹中,并在index.html中包含对其的引用。

注意

在第一章的使用插件扩展应用程序配方中详细描述了将 Backbone 扩展包含到您的项目中,理解 Backbone

如何做到...

假设我们有两个不同的模型类,即IndividualContactModelOrganizationContactModel,我们想要将它们组织到一个单独的集合中。我们可以通过以下步骤来完成:

  1. 定义模型。

    var IndividualContactModel = Backbone.Model.extend
      ({
        name: function() {
          return this.get('firstName') + ' ' + this.get('lastName');
        }
      });
    
    var OrganizationContactModel = Backbone.Model.extend
      ({
        name: function() {
          return this.get('businessName') + ', '
          + this.get('businessType');
        }
      });
    

    如我们所见,这些模型有不同的属性,但共享一个共同的name()方法。

  2. 使用chosen属性定义集合。

    var ContactCollection = Backbone.Collection.extend
      ({
        model: {
          // Pass chosen properties.
          chosen: {
              // Attribute that should contain model type.
              attr: 'type',
    
              // Default model class.
              defaults: IndividualContactModel,
    
              // Mapping attribute values to model classes.
              map: {
                individual: IndividualContactModel,
                organization: OrganizationContactModel
              }
            }
          }
      });
    
  3. 创建一个集合实例并在传入的 JSON 中指定映射属性。

    var contactCollection = new ContactCollection
      ([
        {
          firstName: 'John',
          lastName: 'Smith',
          type: 'individual'
        },
        {
          businessName: 'North American Veeblefetzer',
          businessType: 'LLC',
          type: 'organization'
        }
      ]);
    
  4. 检查结果。新添加到集合中的模型应该是正确模型类的实例。

    contactCollection.at(0) instanceof IndividualContactModel;
    //true
    
    contactCollection.at(0).name(); // John Smith
    
    contactCollection.at(1) instanceof OrganizationContactModel;
    //true
    
    contactCollection.at(1).name();
    // North American Veeblefetzer, LLC
    

它是如何工作的...

Backbone.Chosen覆盖了Backbone.Collection_prepareModel方法,以选择依赖于其映射属性值的正确模型对象。

还有更多...

本节解释了如何执行高级映射。

映射深层嵌套属性

Backbone.Chosen还支持嵌套属性。您可以使用点语法指定attr属性的值,例如,options.type,如果您的传入 JSON 看起来像以下代码:

var contactCollection = new ContactCollection
  ([
    {
      firstName: 'John',
      lastName: 'Smith',
      options: {type: 'individual'}
    },
    {
      businessName: 'North American Veeblefetzer',
      businessType: 'LLC',
      options: {type: 'organization'}
    }
  ]);

使用函数映射模型

有时,我们可能需要使用更复杂的计算来映射模型。这可以通过映射函数的帮助来完成。以下是它是如何完成的。

// Set up a collection
var ContactCollection = Backbone.Collection.extend({
  model: {
    chosen: function (rawData) {
      if (rawData.spice === 'salt') {
        return SaltyModel;
        }
      if (rawData.spice === 'sugar') {
        return SweetyModel;
        }
      return BoringModel;
      }
    }
  });

实现一对多关系

在第二章 模型 中,有一个关于在两个模型之间创建一对一关系的配方。在这个配方中,我们将学习如何创建一对多关系。

如果单个模型与另一类型模型集合之间的关联发生时,可以使用一对多关系。在我们的发票应用程序中,InvoiceModelInvoiceItemModel之间的关系就是这样一种关系。发票项模型可以有多个,因此存储在InvoiceItemCollection中。

准备工作

您可以从其 GitHub 页面github.com/PaulUithol/Backbone-relational下载 Backbone-relational 扩展。要将Backbone.Relational包含到您的项目中,将backbone-relational.js文件保存到lib文件夹中,并在index.html中包含对该文件的引用。

注意

在第一章 理解 Backbone使用插件扩展应用程序 配方中详细描述了将 Backbone 扩展包含到您的项目中。

如何做...

一对多关系的实现与一对一关系的实现类似,但我们需要使用Backbone.HasMany作为类型并指定collectionType,因为多个模型应该存储在集合中。我们可以通过以下步骤来完成:

  1. Backbone.RelationalModel扩展新的模型对象。

    var InvoiceItemModel = Backbone.RelationalModel.extend
      ({
      });
    
  2. 定义此模型类型的集合。

    var InvoiceItemCollection = Backbone.Collection.extend
      ({
        model: InvoiceItemModel
      });
    
  3. Backbone.RelationalModel扩展另一个模型对象,并传递带有关系定义的relations属性。

    var InvoiceModel = Backbone.RelationalModel.extend
      ({
        // Define one-to-many relationship.
        relations: [{
          // Relationship type
          type: Backbone.HasMany,
    
          // Relationship key in BuyerModel.
          key: 'items',
          // Related model.
          relatedModel: InvoiceItemModel,
    
          // Collection to store related models.
          collectionType: InvoiceItemCollection,
    
          // Define reverse relationship.
          reverseRelation: {
            key: 'invoice'
          }
        }]
      });
    
  4. 要使用一对多关系初始化模型,在创建新的InvoiceModel对象实例时,传递发票项数据的单个 JSON。

    var invoiceModel = new InvoiceModel
      ({
        referenceNumber: '12345',
        date: '2012-09-01',
        items: [
          { description:'Wooden Toy House', price:22, quantity:3 },
          { description:'Farm Animal Set', price:17, quantity:1 }
        ]
      });
    
    invoiceModel.get('items').at(0).get('description');
    // Wooden Toy House
    
    invoiceModel.get('items').at(0).get('invoice')
    .get('referenceNumber'); // 12345
    
  5. 当通过items属性访问相关集合时,可以使用add()方法向此关系添加新记录。

    // Add new model to a collection
    invoiceModel.get('items').add
      ({
        description: 'Powerboat',
        price: 12,
        quantity: 1
      });
    
    invoiceModel.get('items').at(2).get('invoice') == invoiceModel;
    // true
    

    或者,我们也可以创建一个invoiceItemModel的实例,并使用invoiceModel的实例设置发票属性;这样,在两个方向上都会创建一个新的关系。

    // Add new model
    invoiceItemModel = new InvoiceItemModel
      ({
        description: 'Jet Ski',
        price: 12,
        quantity: 1,
        invoice: invoiceModel
      });
    
    invoiceModel.get('items').at(3).get('description');
    // Jet Ski
    

它是如何工作的...

每个Backbone.RelationalModel在创建时都会将自己注册到Backbone.Store,并在销毁时从Store中移除。当创建或更新一个关系中的键属性时,被移除的相关对象会收到移除通知,并在Store中查找新的相关对象。

更多...

在本节中,我们将学习一些Backbone.Relational的高级用法。

实现多对多关系

默认情况下,无法创建两个模型之间的多对多关系,但可以通过使用这些模型之间的一对多关系和一个新的中间模型来轻松实现。

将相关模型导出到 JSON

当将模型导出为 JSON 时,它确实包括相关模型。这就是我们将 InvoiceModel 导出为 JSON 的方法。

JSON.stringify(invoiceModel.toJSON());

这里是这种导出的结果。

{
  "referenceNumber":"12345",
  "date":"2012-09-01",
  "items":[
    {
      "description":"Wooden Toy House","price":22,"quantity":3
    },
    {"description":"Farm Animal Set","price":17,"quantity":1},
    {"description":"Powerboat","price":12,"quantity":1},
    {"description":"Jet Ski","price":12,"quantity":1}
  ]
}

这就是我们如何导出 InvoiceItemModel 模型的方法。

JSON.stringify(invoiceModel.get('items').at(0).toJSON())

结果是以下代码片段:

{
  "description":"Wooden Toy House",
  "price":22,
  "quantity":3,
  "invoice":{includeInJSON
    "referenceNumber":"12345",
    "date":"2012-09-01",
    "items":[
      null,
      {
        "description":"Farm Animal Set","price":17,
        "quantity":1
      },
      {"description":"Powerboat","price":12,"quantity":1},
      {"description":"Jet Ski","price":12,"quantity":1}
    ]
  }
}

如我们所见,toJSON() 方法也导出了反向关系,但我们可以通过在直接和反向关系的 includeInJSON 属性中指定一个包含这些属性的数组来控制需要导出的相关模型的属性。

var InvoiceModel = Backbone.RelationalModel.extend
  ({
    relations: [{
      type: Backbone.HasMany,
      key: 'items',
      relatedModel: InvoiceItemModel,
      collectionType: InvoiceItemCollection,

      // Restrict related models properties when exporting
      // to JSON.
      includeInJSON: ['description', 'price', 'quantity'], 

      reverseRelation: {
        key: 'invoice',

        // Restrict related models properties when exporting
        // to JSON for reversed relations.
        includeInJSON: ['referenceNumber', 'items'],
      }
    }]
  });

参见

  • 在第二章 第二章:模型 中实现 一对一关系

  • 关于导出为 JSON 的更多信息,请参阅第七章 使用 RESTful 服务同步模型和集合 中的配方,第七章:REST 和存储。

  • Backbone-relational 扩展的完整文档可以在其 GitHub 页面上找到,网址为 github.com/PaulUithol/Backbone-relational

  • 此外,Backbone-relational 扩展还有一个替代方案,那就是 Backbone-associations。

第四章:视图

在本章中,我们将涵盖以下内容:

  • 在视图中渲染视图

  • 使用 jQuery 处理视图元素

  • 在视图中渲染模型

  • 在视图中渲染集合

  • 将视图拆分为子视图

  • 在视图中处理文档对象模型DOM)事件

  • 使用 Backbone.Router 切换视图

简介

本章致力于 Backbone.js 中的视图对象;它用于将数据渲染到 HTML 代码中。视图可以绑定到 DOM 树中的 HTML 元素,并可以处理其事件及其子元素的事件。

模型和集合通常通过视图渲染,视图充当业务逻辑和用户之间的交互式桥梁。例如,视图可以监听 DOM 事件,并作为结果操作模型和集合或引导用户到不同的页面。该过程也可以反向进行:模型和集合的变化触发视图更新,从而在 DOM 树中进行更改。

Backbone 视图在处理 HTML 元素和处理它们的事件时依赖于前端 JavaScript 库,例如 jQuery 或 Zepto。

在视图中渲染视图

当我们想要向用户输出任何数据时,我们通常使用 Backbone 视图。在本例中,我们将创建一个简单的视图并将其渲染。

我们的结果将类似于以下截图:

渲染视图

如何做到...

按照以下步骤创建一个简单的视图并将其渲染。

  1. 通过扩展Backbone.View对象来定义一个新的视图:

      var InvoiceItemView = Backbone.View.extend({
    
        // HTML element name, where to render a view.
        el: 'body',
    
        // Initialize view object values.
        initialize: function() {
          this.html = 'Description: Wooden Toy House. ' +
            'Price: $22\. Quantity: 3.'
        },
    
        // Render view.
        render: function() {
          // Set html for the view element using jQuery.
          $(this.el).html(this.html);
        }
      });
    
  2. 创建视图的实例:

        var invoiceItemView = new InvoiceItemView();
    
  3. 手动调用render()方法向用户输出 HTML 代码:

        invoiceItemView.render();
    

它是如何工作的...

在视图的initialize()方法中,我们生成 HTML 代码并将其保存到html属性中,这是我们最近在render()方法中使用过的,在那里我们将此代码分配给由el属性定义的 HTML 容器。为此,我们调用 jQuery 函数,如$()html()

当创建一个新的视图实例时,会自动触发initialize()方法。此外,我们可以在创建其实例时从对象外部传递任何标准属性给视图。可以通过以下代码片段实现:

var invoiceItemView = new InvoiceItemView({
  el: 'body'
});

如果我们希望el属性动态计算,也可以将其定义为函数。

当调用render()方法时,它会运行我们的代码,然后渲染视图。

更多...

在本节中,我们将学习一些处理视图时有用的技巧。

创建与视图相关的新 HTML 元素

有时,我们可能不想将视图渲染到 DOM 树中的现有 HTML 元素中;相反,我们可能想创建一个新的元素,然后将其添加到文档中。按照以下步骤创建与视图相关的新 HTML 元素。

  1. 通过将值分配给tagNameclassNameattributes属性来手动定义视图并设置其元素和属性:

      // Define new view.
      var InvoiceItemView2 = Backbone.View.extend({
        // Set tag name and its attributes.
        tagName: 'p',
        className: 'item',
        attributes: {
          'align': 'left'
        },
    
        // Initialize view object values.
        initialize: function() {
          this.html = 'Farm Animal Set. Price: $17\. Quantity: 1.'
        },
    
        // Render view.
        render: function() {
    
          // Set html for the view element using jQuery.
          $(this.el).html(this.html);
        }
      });
    
  2. 创建一个新的视图实例。在这样做的时候,Backbone 会自动将 el 分配一个合适的值:

        // Create new view instance.
        var invoiceItemView2 = new InvoiceItemView2();
    
        invoiceItemView2.el; // <p align="left" class="item"></p>
    
  3. 渲染此视图。我们的渲染代码将创建一个新的 HTML 对象:

        invoiceItemView2.render();
    
  4. 将新创建的 HTML 对象插入 DOM:

        $('body').append(invoiceItemView2.el);
    
  5. 检查结果。我们的 HTML 页面的主体应该包含以下代码片段:

    <body>
        <p align="left" class="item">
            Farm Animal Set. Price: $17\. Quantity: 1.
        </p>
    </body>
    

动态更改视图元素

在我们代码的工作过程中,我们可能想要更改视图元素。这可以通过 setElement() 方法来实现。以下两种方法都是有效的。

// Change existing element to the new one.
InvoiceItemView.setElement('li');

// Change existing element to the one already exists
// in the DOM tree.
InvoiceItemView.setElement($('body div'));

当调用 setElement() 方法时,Backbone 会取消委托之前元素分配的事件,并将它们分配给新元素。

移除视图

当我们完成对视图的工作并想要移除它时,我们还需要从 DOM 中移除其元素并停止监听事件。为此,我们只需调用 remove() 方法。

参见

  • 在这个菜谱中,我们使用 jQuery 方法 $() 来访问视图元素的属性。请参考下一个菜谱以获取有关 jQuery 的更多信息。

使用 jQuery 处理视图元素

毫无疑问,jQuery 是目前最受欢迎的 JavaScript 库。它通过 CSS 选择器简化文档遍历,并提供简单的事件处理、动画和 AJAX 交互。

Backbone.js 在处理视图时依赖于 jQuery。在本菜谱中,我们将学习如何使用 jQuery 与视图元素交互。

如何做到这一点...

按照以下步骤使用 jQuery 处理视图元素。

  1. 要使用 jQuery 访问视图元素,请使用 $(this.el)

    $(this.el).show();
    
  2. 使用 this.$el 作为 $(this.el) 的简写别名:

    this.$el.appendl('<li>An item</li>');
    
  3. 在视图范围内运行查询,请使用 this.$el.find():

    this.$el.find('li').html('Hey there');
    
  4. 使用 this.$() 作为 this.$el.find() 的简写别名:

    this.$el('li').addClass('highlighted');
    

它是如何工作的...

Backbone 与 jQuery 库以及 Zepto.js 和 Ender.js 集成。当 Backbone 被加载时,它会确定使用哪个库,并将一个引用分配给它,形式为 Backbone.$ 变量。

有几个别名,如 this.$elthis.$(),它们简化了对库的访问。

更多...

在本节中,我们将遇到一个名为 Zepto 的 jQuery 替代品。

使用 Zepto 作为 jQuery 的更快替代品

Zepto 是一个与 jQuery 兼容度达到 99% 的极简 JavaScript 库。Zepto 的设计目标是拥有小巧的库和更快的执行速度,这可以通过仅支持现代浏览器来实现。因此,Zepto 在移动设备上运行得更快。

要使用 Zepto 与 Backbone,你需要执行以下步骤:

  1. zeptojs.com 下载库并将其包含在项目的 lib 文件夹中。

  2. 将 Zepto 包含在 index.html 文件中,而不是 jQuery。

      <script src="img/zepto.js"></script>
    

参见

  • 你可以在其官方网站 jquery.com 上找到 jQuery 的完整文档。

在视图中渲染模型

当与模型一起工作时,我们可能经常想要在浏览器中渲染并显示它们。通常,这可以通过创建一个用于渲染模型的视图并传递模型实例作为参数来完成。

在这个菜谱中,我们将使用视图渲染一个简单的模型,结果将类似于以下截图:

在视图中渲染模型

如何操作...

按照以下步骤在视图中渲染一个模型。

  1. 定义一个新的模型:

      var InvoiceItemModel = Backbone.Model.extend({
    
      });
    
  2. 定义一个将渲染此模型的视图:

      var InvoiceItemView = Backbone.View.extend({
    
        // HTML element name, where to render a view.
        el: 'body',
    
        // Render view.
        render: function() {
          var html = 'Description: ' +
            this.model.get('description') + '. ' +
            'Price: ' + this.model.get('price') + '. ' +
            'Quantity: ' + this.model.get('quantity') + '.';
    
          // Set html for the view element using jQuery.
          $(this.el).html(html);
        }
      });
    
  3. 创建一个模型实例:

        var invoiceItemModel = new InvoiceItemModel({
          description: 'Farmer Figure',
          price: 8,
          quantity: 1
        });
    
  4. 创建一个视图实例并将模型作为参数传递给它:

        var invoiceItemView = new InvoiceItemView({
    
          // Pass model as a parameter to a view.
          model: invoiceItemModel
        });
    
  5. 渲染视图:

        invoiceItemView.render();
    

它是如何工作的...

当初始化一个新的视图对象时,我们向视图传递一个模型对象,该对象随后通过 Backbone 添加到其属性数组中。在视图的任何方法中,可以通过使用 this.model 属性来使分配的模型可用。

相关内容

  • 通常在视图中渲染模型时,如果模型对象需要更新,我们需要更新 HTML。这意味着每次模型更改时,我们都需要调用 setElement() 方法。幸运的是,Backbone 提供了一个自动执行此操作的事件处理机制。这已在 第五章 中描述,事件和绑定

在视图中渲染集合

在这个菜谱中,我们将学习在视图中渲染模型集合的简单方法。

输出的结果是 HTML 列表,如下截图所示:

在视图中渲染集合

如何操作...

按照以下步骤在视图中渲染一个集合:

  1. 定义一个模型:

      var InvoiceItemModel = Backbone.Model.extend({
    
      });
    
  2. 定义一个集合:

      var InvoiceItemCollection = Backbone.Collection.extend({
        model: InvoiceItemModel
      });
    
  3. 定义一个视图:

    var InvoiceItemListView = Backbone.View.extend({
    
      // HTML element name, where to render a view.
      el: 'body',
    
      // Render view.
      render: function() {
        var html = '';
        _.each(this.collection.models,function(model,index,list) {    
          var item_html = 'Description: ' +
            model.get('description') + '. ' +
            'Price: ' + model.get('price') + '. ' +
            'Quantity: ' + model.get('quantity') + '.';
          html = html + '<li>' + item_html + '</li>';
        });
    
        html = '<ul>' + html + '</ul>';
    
        // Set html for the view element using jQuery.
        $(this.el).html(html);
      }
    });
    
  4. 创建一个集合实例:

    var invoiceItemCollection = new InvoiceItemCollection([
      { description: 'Wooden Toy House', price: 22, quantity: 3 },
      { description: 'Farm Animal Set', price: 17, quantity: 1 },
      { description: 'Farmer Figure', price: 8, quantity: 1 },
      { description: 'Toy Tractor', price: 15, quantity: 1 }
    ]);
    
  5. 创建一个视图实例:

        var invoiceItemListView = new InvoiceItemListView({
    
          // Pass model as a parameter to a view.
          collection: invoiceItemCollection
        });
    
  6. 渲染视图:

        invoiceItemListView.render();
    

它是如何工作的...

当初始化一个新的视图对象时,我们向它传递集合对象,这样我们就可以在 render() 方法的帮助下在循环中稍后处理。因此,我们创建的结果 HTML 代码随后被分配给视图元素。

相关内容

  • 通常在视图中渲染集合时,如果集合正在排序或更新,我们需要更新 HTML。这意味着每次模型更改时,我们都需要调用 setElement() 方法。幸运的是,Backbone 提供了一个自动执行此操作的自动事件处理机制。这已在 第五章 中描述,事件和绑定

将视图拆分为子视图

在前面的菜谱中,我们使用了一个大视图来渲染集合。然而,有一种更好的方法来处理大视图,即通过将它们拆分成多个小视图。这种做法应该有几个优点。在我们的集合上下文中,观察到以下优点:

  • 能够在不重新渲染整个集合的情况下插入、删除或更新集合中的模型

  • 能够在其他程序部分重用子视图

  • 能够将单个大块代码拆分成小而简单的部分将视图拆分为子视图

在本例中,我们将把渲染集合的视图拆分成几个简单的子视图。让我们以表格的形式输出数据,而不是以列表形式,并应用一些层叠样式表CSS)以使其看起来更好。

如何操作...

按照给定的步骤将一个大视图拆分成小子视图。

  1. 确保你有模型和集合定义:

      var InvoiceItemModel = Backbone.Model.extend({
    
      });
      var InvoiceItemCollection = Backbone.Collection.extend({
        model: InvoiceItemModel
      });
    
  2. 定义一个用于渲染单个模型的视图:

      // Define new view to render a model.
      var InvoiceItemView = Backbone.View.extend({
    
        // Define element tag name.
        tagName: 'tr',
    
        // Render view.
        render: function() {
    
          // Add cells to the table row.
          $(this.el).html(_.map([
            this.model.get('quantity'),
            this.model.get('description'),
            this.model.get('price'), this.model.calculateAmount(),
          ], function(val, key){
            return '<td>' + val + '</td>'
          }));
    
          return this;
        }
      });
    
  3. 定义一个用于渲染集合的视图:

      // Define new view to render a collection.
      var InvoiceItemListView = Backbone.View.extend({
    
        // Define element tag name.
        tagName: 'table',
    
        // Define element class name.
        className: 'invoice-item-view',
    
        // Render view.
        render: function() {
    
          $(this.el).empty();
    
          // Append table with a table header.
          $(this.el).append($('<tr></tr>').html(
            _.map(['Quantity', 'Description', 'Price', 'Total'], 
              function(val, key){
                return '<th>' + val + '</th>'
            })
          ));
    
          // Append table  with a row.
          $(this.el).append(
            _.map(this.collection.models, function(model, key) {
              return new InvoiceItemView({
                model: model
              }).render().el;
            })
          );
    
          return this;
        }
      });
    
  4. 定义一个用于渲染整个页面的视图:

      var InvoiceItemListPageView = Backbone.View.extend({
    
        // Render whole page view.
        render: function() {
          $(this.el).html(new InvoiceItemListView({
            collection: this.collection
          }).render().el);
        }
      });
    
  5. 使用数据创建并初始化一个集合实例。

    var invoiceItemCollection = new InvoiceItemCollection([
      { description: 'Wooden Toy House', price: 22, quantity: 3 },
      { description: 'Farm Animal Set', price: 17, quantity: 1 },
      { description: 'Farmer Figure', price: 8, quantity: 1 },
      { description: 'Toy Tractor', price: 15, quantity: 1 }
    ]);
    
  6. 为整个页面创建一个视图实例并渲染它。

        new InvoiceItemListPageView({
          collection: invoiceItemCollection,
          el: 'body'
        }).render();
    

它是如何工作的...

在本例中,我们使用了InvoiceItemView来渲染模型,并使用InvoiceItemListView来渲染集合。

此外,我们还引入了新的视图InvoiceItemListPageView,用于渲染整个页面。在创建此视图的实例时,我们传递el属性;它包含视图应输出其结果的 HTML 元素名称。这为我们提供了更多的灵活性,因此我们可以将视图渲染到任何需要的地方。

在视图中处理文档对象模型(DOM)事件

Backbone 中的视图提供了一些与用户交互的功能。它允许在视图元素上下文中处理 DOM 中发生的事件。

在本例中,我们将修改前一个示例中给出的示例。让我们向表格的每一行添加一个编辑按钮,如图所示:

在视图中处理文档对象模型(DOM)事件

通过点击编辑按钮,我们将立即将文本值替换为输入框,以便用户可以输入新值。我们还将显示保存取消按钮以保存或取消更改。

在视图中处理文档对象模型(DOM)事件

如果用户点击保存按钮,模型将被更新。如果用户点击取消按钮,行中的值将被恢复。同时点击这两个按钮将使行视图再次以视图模式工作。

如何操作...

将以下更改应用到我们在前一个示例中创建的InvoiceItemView

  1. 定义一个视图:

      // Define new view to render a model.
      var InvoiceItemView = Backbone.View.extend({
    
        // Define tag name.
        tagName: 'tr',
      });
    
  2. 当用户查看项目时引入一个渲染函数:

        renderViewMode: function() {
         $(this.el).html(_.map([
            this.model.get('quantity'),
            this.model.get('description'),
            this.model.get('price'),
            this.model.calculateAmount(),
            '<button class="edit">Edit</button>'
          ], function(val, key){
            return '<td>' + val + '</td>'
          }));
        },
    
  3. 当用户编辑项目时引入一个渲染函数:

       renderEditMode: function() {
          $(this.el).html(_.map([
            '<input class="quantity" value="' + 
              this.model.get('quantity') + '">',
            '<input class="description" value="' + 
              this.model.get('description') +
            '">',
            '<input class="price" value="' + 
              this.model.get('price') + '">',
            this.model.calculateAmount(),
            '<button class="save">Save</button>' +
            '<button class="cancel">Cancel</button>'
          ], function(val, key){
            return '<td>' + val + '</td>'
          }));
        },
    
  4. 设置一个将包含在渲染视图时调用的函数名的属性:

        renderCallback: 'renderViewMode',
    
        render: function() {
          this[this.renderCallback]();
    
          return this;
        },
    
  5. 将 DOM 事件映射到处理程序:

        events: {
          'click button.edit': 'edit',
          'click button.save': 'save',
          'click button.cancel': 'cancel',
        },
    
  6. 定义事件处理程序:

        // Edit button click handler.
        edit: function() {
          this.renderCallback = 'renderEditMode';
    
          this.render();
        },
    
        // Save button click handler.
        save: function() {
          this.model.set({
            quantity: $(this.el).find('input.quantity').val(),
            description: 
              $(this.el).find('input.description').val(),
            price: $(this.el).find('input.price').val(),
          });
    
          this.renderCallback = 'renderViewMode';
    
          this.render();
        },
    
        // Cancel button click handler.
        cancel: function() {
          this.renderCallback = 'renderViewMode';
    
          this.render();
        }
    
  7. 使用数据创建并初始化一个集合实例:

    var invoiceItemCollection = new InvoiceItemCollection([
      { description: 'Wooden Toy House', price: 22, quantity: 3 },
      { description: 'Farm Animal Set', price: 17, quantity: 1 },
      { description: 'Farmer Figure', price: 8, quantity: 1 },
      { description: 'Toy Tractor', price: 15, quantity: 1 }
    ]);
    
  8. 为整个页面创建一个视图实例并渲染它:

        new InvoiceItemListPageView({
          collection: invoiceItemCollection,
          el: 'body'
        }).render();
    

它是如何工作的...

通过定义event属性,我们可以告诉 Backbone 如何将事件映射到处理程序。为此,我们将使用以下语法:

{"event selector": "callback"}

Backbone.js 使用 jQuery 的on()函数为视图内的 DOM 事件提供声明性回调。如果没有提供selector值,则假定视图的根元素(this.el)。

更多...

本节描述了委托和取消委托 DOM 事件的视图方法。

手动委托和取消委托事件

在某些情况下,我们可能需要视图从程序中的特定位置开始手动处理 DOM 事件。这可以通过调用delegateEvents()方法来实现。它接受一个包含事件名称及其回调的哈希表。如果没有提供参数,则使用this.events

如果我们需要一个视图停止处理 DOM 事件,我们应该调用undelegateEvents()方法。这在我们暂时隐藏视图并需要确保不会因 DOM 事件引起意外行为时非常有用。

参见...

使用 Backbone.Router 切换视图

在实际的 Backbone 应用中,我们经常需要从一个视图切换到另一个视图。这通常是通过 Backbone.Router 实现的;它允许我们将 URL 映射到渲染特定视图的特定回调。在第一章中,我们学习了 Backbone.js 中的路由器。然而,我们没有过多地讨论它与视图的交互。

在本食谱中,我们将构建一个 Backbone 应用,该应用将根据 URL 动态渲染适当的视图,同时更改和移除之前向用户展示的视图,以防止内存泄漏。视图的切换将不会重新加载页面,因为 Backbone.Router 支持 hash URL 和pushState

在我们的应用中,我们将实现InvoiceListViewInvoicePageView。第一个视图显示发票列表,如下截图所示:

使用 Backbone.Router 切换视图

当用户点击视图详情链接时,他们会看到一个像以下截图所示的发票详情屏幕:

使用 Backbone.Router 切换视图

如何实现...

假设我们已经有了一个模型、集合和视图对象定义。按照以下步骤创建一个可以切换视图的路由。

  1. 定义一个路由对象及其路由:

      var Workspace = Backbone.Router.extend({
    
        // Define routes
        routes: {
          '': 'invoiceList',
          'invoice': 'invoiceList',
          'invoice/:id': 'invoicePage',
        }
    
  2. 在路由对象中的initialize()方法中创建一个新的集合实例:

        initialize: function() {
    
          //  Create collection
          this.invoiceCollection = new InvoiceCollection([
            { referenceNumber: 1234},
            { referenceNumber: 2345},
            { referenceNumber: 3456},
            { referenceNumber: 4567}
          ]);
        }
    
  3. 在路由对象中定义路由回调:

        invoiceList: function() {
          this.changeView(new InvoiceListView({
            collection: this.invoiceCollection
          }));
        },
    
        invoicePage: function(id) {
          this.changeView(new InvoicePageView({
            model: this.invoiceCollection.get(id)
          }));
        }
    
  4. 在路由对象中定义一个changeView()方法,这将帮助我们更改当前视图:

        changeView: function(view) {
          if (this.currentView) {
            if (this.currentView == view) {
              return;
            }
    
            this.currentView.remove();
          }
    
          $('body').append(view.render().el);
    
          this.currentView = view;
        }
      });
    
  5. 创建一个路由实例并运行Backbone.history.start()方法以启动我们的应用:

        new Workspace();
        Backbone.history.start();
    

它是如何工作的...

changeView()方法中发生了许多有趣的事情。为了确保我们的操作正确,我们检查当前视图是否不是我们要切换到的视图,然后将其移除。在移除视图时,需要取消绑定它处理的所有事件,并将相应的 HTML 元素从 DOM 树中移除。然后,我们渲染一个新的视图并将其元素附加到 body 上。

移除之前使用的视图有助于我们避免内存泄漏,这可能会在应用长时间连续使用时发生。

参见

  • 请参阅第一章,理解 Backbone,以了解更多关于 Backbone.js 中路由器的信息。

第五章:事件和绑定

在本章中,我们将介绍以下内容:

  • Backbone.js中管理事件

  • 处理 Backbone 对象的事件

  • 将模型绑定到视图中

  • 将集合绑定到视图中

  • 使用Backbone.stickit进行双向绑定

  • 将模型和集合绑定到选择列表

  • 在视图中处理键盘快捷键

  • 处理路由事件

简介

本章专门介绍Backbone.Events对象及其在 Backbone 的其他对象(如模型、集合、视图和路由器)中的作用。

我们将学习如何将回调函数分配给特定事件或如何监听其他对象的事件。我们还将学习如何在两个方向上将模型或集合绑定到视图中。因此,如果模型被更新,视图会自动显示更改,或者如果用户在视图中输入数据,模型将被验证并更新。

在 Backbone.js 中管理事件

Backbone 提供了一种统一的方式来触发和处理其他 Backbone 对象(如ModelCollectionViewRouter)的事件。这得益于Backbone.Events对象,它提供了这种功能,因此可以将其混合到任何对象中,包括您自己的对象。

在本配方中,我们将学习如何将Backbone.Events混合到您的对象中,如何触发事件以及如何将回调绑定到事件。

如何做...

执行以下步骤以处理对象事件。

  1. 定义一个新对象。

    object1 = {};
    
  2. Backbone.Events混合到您的对象中。

    _.extend(object1, Backbone.Events);
    
  3. 定义一个回调函数。

    var hello = function(msg) {
      alert("Hello"+ msg);     
    }
    
  4. 使用on()方法绑定回调。

    object1.on("handshake", hello);
    

    或者,您可以使用once()方法在取消绑定之前一次性触发回调。

    object1.once("handshake", hello);
    

    注意

    如果一个对象有大量不同的事件,通常使用冒号来命名它们,例如poll:startchange:selection

  5. 通过调用trigger()方法来触发事件。

    object1.trigger("handshake", "world!");
    

它是如何工作的...

on()方法中,Backbone.Events将回调函数保存在关联数组_events中,然后在trigger()方法中迭代运行该事件的全部回调函数。

更多...

在本节中,我们将学习有关事件的一些重要主题:从事件中取消绑定回调和监听其他对象的事件。

从事件中取消绑定回调

要从事件中取消绑定回调,我们需要使用off()方法。以下代码行将取消绑定之前设置的特定回调。

object1.off("handshake", hello);

要从事件中取消绑定所有回调,请跳过第二个参数。

object1.off("handshake");

要从所有事件中取消绑定特定回调,请跳过第一个参数。

object1.off(null, hello);

要从所有事件中取消绑定所有回调,请跳过两个参数。

object1.off();

监听其他对象的事件

要监听其他对象的事件,我们可以使用listenTo()方法。

object2.listenTo(object1, 'handshake', object2.hello);

它的工作方式类似于on()方法,但其优势在于它允许我们跟踪事件,并且可以在稍后一次性删除它们。

object2.stopListening(object1);

要停止监听所有对象,请运行不带参数的stopListening()方法。

object2.stopListening();

处理 Backbone 对象的事件

所有 Backbone 对象都实现了 Backbone.Events,其中一些提供了内置事件,你的对象可以监听这些事件。

例如,当模型发生变化时,会触发 change 事件。特别是对于这个事件,Backbone.Model 中有几个方法可以在 change 事件回调中使用。在本教程中,我们将学习如何使用它们。

如何做到这一点...

执行以下步骤来处理模型事件。

  1. 创建一个新的 model 实例。

      var model = new Backbone.Model({
        firstName: 'John',
        lastName: 'Doe',
        age: 20,
      });
    
  2. 将回调绑定到 change 事件。

      model.on('change', function(model) {
    
      }
    
  3. 在事件回调中使用 hasChanged() 方法来检查特定属性自上次 change 事件以来是否已更改。

        model.hasChanged("age"); // true
        model.hasChanged("firstName"); // false
    
  4. 在事件回调中使用 changedAttributes() 方法来获取已更改属性的哈希值。

        model.changedAttributes(); // Object {age: 21}
    
  5. 在事件回调中使用 previous() 方法来获取先前属性的值。

        model.previous('age'); // 20
    
  6. 在事件回调中使用 previousAttributes() 方法来获取先前属性的哈希值。

        model.previousAttributes();
          // Object {firstName: "John", lastName: "Doe", age: 20}
    
  7. 更改 model 属性以触发 change 事件。

        model.set('age', 21);
    

还有更多...

在本节中,我们将学习更多关于 Backbone 对象的事件:在使用 Backbone 对象时避免事件触发和使用内置事件。

在使用 Backbone 对象时避免事件触发

在使用 Backbone 事件时,有一种方法可以避免事件触发。如果你想在不知道事件回调的情况下更新对象属性,这可能很有用。

例如,在更新模型值时,你可以传递 {silent: true}

model.set('age', 22, {silent: true});

以下行代码也是有效的:

model.set({ age: 25 }, {silent: true});

使用内置事件

以下事件与模型对象一起使用:

  • change (模型,选项):当模型的属性发生变化时触发

  • change:[属性] (模型,值,选项):当特定属性被更新时触发

  • destroy (模型,集合,选项):当模型被销毁时触发

  • invalid (模型,错误,选项):当模型在客户端验证失败时触发

  • error (模型,XMLHttpRequest,选项):当模型的保存调用在服务器上失败时触发

  • sync (模型,响应,选项):当模型与服务器成功同步时触发

以下事件与集合一起使用:

  • add (模型,集合,选项):当模型被添加到集合中时触发

  • remove (模型,集合,选项):当模型从集合中移除时触发

  • reset (集合,选项):当集合的全部内容被替换时触发

  • sort (集合,选项):当集合被重新排序时触发

  • sync (集合,响应,选项):当集合与服务器成功同步时触发

以下事件与路由对象一起使用:

  • route:[名称] (参数):当匹配特定路由时由路由器触发

  • route (路由器,路由,参数):当匹配任何路由时由历史记录(或路由器)触发

当执行存储操作时,以下事件被触发:

  • route:[name] (params):当匹配特定路由时由路由器触发

  • route (router, route, params):当任何路由匹配时由历史记录(或路由器)触发

要处理任何触发的事件,请使用特殊事件all

参见

  • 您可以从backbonejs.org/#Events-catalog找到完整的内置事件目录。

  • 要检查哪些 Backbone 方法支持{silent: true},请参阅官方文档。

将模型绑定到视图

Backbone.js中的一个有用特性是将模型变化绑定到视图,因此每次模型更改时视图都会重新渲染。这允许你编写更少的代码,并使你的应用程序像 AJAX 应用程序一样工作,例如,当从 REST 服务器获取新数据时,用户会立即看到更新。

让我们以第四章中的在视图中渲染模型配方为例,我们在视图中渲染了一个模型并对其进行了修改,因此每次模型更新时视图都会重新渲染。

我们将要实现的视图将按照以下截图所示进行渲染:

将模型绑定到视图

在浏览器控制台中,我们可以修改模型值,从而触发change事件并重新渲染视图。

将模型绑定到视图

如何做到...

执行以下步骤以将模型绑定到视图。

  1. 定义一个新的模型。

      var InvoiceItemModel = Backbone.Model.extend({
    
      });
    
  2. 定义一个渲染此模型的视图。

      var InvoiceItemView = Backbone.View.extend({
    
        // HTML element name, where to render a view.
        el: 'body',
    
        // Render view.
        render: function() {
          var html = 'Description: ' + 
            this.model.get('description') + '. ' +
            'Price: ' + this.model.get('price') + '. ' +
            'Quantity: ' + this.model.get('quantity') + '.';
          // Set html for the view element using jQuery.
          $(this.el).html(html);
        }
      });
    
  3. initialize()方法中将模型绑定到InvoiceItemView

        initialize: function() {
          this.listenTo(this.model, 'change', this.render, this);
        }
    
  4. 创建模型实例。

        var invoiceItemModel = new InvoiceItemModel({
          description: 'Farmer Figure',
          price: 8,
          quantity: 1
        });
    
  5. 创建一个视图实例并将model作为参数传递给它。

        var invoiceItemView = new InvoiceItemView({
    
          // Pass model as a parameter to a view.
          model: invoiceItemModel
        });
    
  6. 渲染视图。

        invoiceItemView.render();
    
  7. 要检查绑定的工作方式,将模型导出为一个全局变量,这样我们就可以在浏览器控制台中更新模型值。

    window.invoiceItemModel = invoiceItemModel;
    

它是如何工作的...

Backbone.ModelBackbone.View对象都实现了Backbone.Events,因此可以在视图中监听模型变化,并将render()方法绑定为change事件的回调。

将集合绑定到视图

在这个配方中,我们将学习如何将集合绑定到视图。如果我们有不同视图使用相同的集合,或者我们想要与 REST 服务器同步数据,这将非常有帮助。

让我们以第四章中的在视图中渲染模型配方为例,我们渲染了一个带有子视图的集合并对其进行了修改。我们将添加一个带有添加删除按钮的额外视图,这将更新集合。

此外,我们将在我们的第一个视图中绑定适当的回调到模型和集合事件,以便在集合更改时自动重新渲染。

将集合绑定到视图

当用户点击 添加 按钮时,会提示用户输入创建 InvoiceItemModel 所需的信息。

将集合绑定到视图

在用户完成所有问题后,会创建一个新的模型并将其添加到集合中,相应的视图也会更新。

将集合绑定到视图

当点击 删除 按钮时,用户会被提示输入要删除的项目位置。

将集合绑定到视图

如何做到这一点...

执行以下步骤以将集合绑定到视图。

  1. 确保你有模型和集合的定义。

      var InvoiceItemModel = Backbone.Model.extend({
    
      });
    
      var InvoiceItemCollection = Backbone.Collection.extend({
        model: InvoiceItemModel
      });
    
  2. 定义一个用于渲染单个模型的视图。

      // Define new view to render a model.
      var InvoiceItemView = Backbone.View.extend({
    
        // Define element tag name.
        tagName: 'tr',
    
        // Render view.
        render: function() {
    
          // Add cells to the table row.
          $(this.el).html(_.map([
            this.model.get('quantity'),
            this.model.get('description'),
            this.model.get('price'), this.model.calculateAmount(),
          ], function(val, key){
            return '<td>' + val + '</td>'
          }));
    
          return this;
        }
      });
    
  3. InvoiceItemView 对象的 initialize() 方法中,绑定回调以处理模型的 destroy 事件。

        initialize: function() {
          this.listenTo(this.model, 'destroy', this.destroy, this);
        }
    
  4. 添加 destroy() 方法,它移除与模型绑定的视图。

        destroy: function() {
          this.remove();
        }
    
  5. 定义一个用于渲染集合的视图。

      // Define new view to render a collection.
      var InvoiceItemListView = Backbone.View.extend({
    
        // Define element tag name.
        tagName: 'table',
    
        // Define element class name.
        className: 'invoice-item-view',
    
        // Render view.
        render: function() {
    
          $(this.el).empty();
    
          // Append table with a table header.
          $(this.el).append($('<tr></tr>').html(
            _.map(['Quantity', 'Description', 'Price', 'Total'], 
              function(val, key){
                return '<th>' + val + '</th>'
              }
            )
          ));
    
          // Append table  with a row.
          _.each(this.collection.models, function(model, key) {
            this.append(model);
          }, this);
    
          return this;
        },
    
        // Add invoice item row to the table.
        append: function(model) {
          $(this.el).append(
            new InvoiceItemView({ model: model }).render().el
          );
        }
      });
    

    这里我们使用了 append() 方法,它将 InvoiceItemView 添加到输出表中。我们稍后会使用这个方法。

  6. InvoiceItemListView 对象的 initialize() 方法中,绑定回调以处理集合的 add 事件。

        initialize: function() {
          this.listenTo(
            this.collection, 'add', this.append, this
          );
        },
    

    这里我们调用了相同的 append() 方法。

  7. 定义带有添加和删除控制的视图。

      var InvoiceItemListControlsView = Backbone.View.extend({
        render: function() {
          var html = 
            '<br><input id="add" type="button"' value="Add" id>' +
            ' <input id="remove" type="button" value="Remove">';
    
          $(this.el).html(html);
    
          return this;
        },
    
        // Handle HTML events.
        events: {
          'click #add': 'addNewInvoiceItem',
          'click #remove': 'removeInvoiceItem',
        },
    
        // Add button handler.
        addNewInvoiceItem: function() {
          var description = prompt('Enter item description', '');
          var price = prompt('Enter item price', '0');
          var quantity = prompt('Enter item quantity', '1');
    
          this.collection.add([{
            description: description,
            price: price,
            quantity: quantity
          }]);
        },
    
        // Remove button handler.
        removeInvoiceItem: function() {
          var position =
            prompt('Enter position of item to remove', '');
    
          model = this.collection.at(position);
          model.destroy();
        }
      }); 
    
  8. 定义一个用于渲染整个页面的视图。

      var InvoiceItemListPageView = Backbone.View.extend({
    
        // Render whole page view.
        render: function() {
          $(this.el).html(new InvoiceItemListView({
            collection: this.collection
          }).render().el);
    
          $(this.el).append(new InvoiceItemListControlsView({
            collection: this.collection
          }).render().el);
        }
      });
    
  9. 使用数据创建并初始化集合实例。

    var invoiceItemCollection = new InvoiceItemCollection([
      { description: 'Wooden Toy House', price: 22, quantity: 3 },
      { description: 'Farm Animal Set', price: 17, quantity: 1 }
    ]);
    
  10. 创建整个页面视图实例并渲染它。

        new InvoiceItemListPageView({
          collection: invoiceItemCollection,
          el: 'body'
        }).render();
    

它是如何工作的...

当新模型被添加到集合中时,会触发 add 事件,并将模型作为表格行渲染并附加到表格中。

当模型被销毁时,会触发 destroy 事件,并且与该模型对应的视图被移除,同时视图元素也会从 DOM 树中移除。

使用 Backbone.stickit 进行双向绑定

Backbone.js 中,我们可以直接将模型绑定到视图,但如果没有解析 HTML 元素值的需求,反向绑定并不容易。

在这个菜谱中,我们将讨论 Backbone.stickit 扩展,它允许开发者以简单和原生 Backbone.js 的方式实现模型属性和视图元素的双向绑定。

在许多类似的扩展中,Backbone.stickit 因其完美的文档、简洁性和为应用程序开发者带来的巨大优势而脱颖而出。它不久前被纽约时报撰写,并且其受欢迎程度每天都在增长。它无疑是 Backbone.js 中最酷的扩展之一。

在这个菜谱中,我们将构建一个简单的应用程序,它有两个视图绑定到同一个模型,因此如果用户在第一个视图中更改元素,第二个视图会自动更新。我们应用程序的用户界面将如下截图所示:

使用 Backbone.stickit 进行双向绑定

有几个视图绑定到了同一个模型上。当用户在表单中输入数据时,模型和其他视图都会被更新。

准备工作

您可以从 GitHub 页面 github.com/nytimes/backbone.stickit 下载 Backbone.stickit 扩展。要将此扩展包含到您的项目中,将 backbone.stickit.js 文件保存到项目的 lib 文件夹中,并在 index.html 中包含对该文件的引用。

注意

在 第一章 的 使用插件扩展应用程序 菜谱中详细描述了将 Backbone 扩展包含到您的项目中,理解 Backbone

如何实现...

执行以下步骤以执行双向绑定。

  1. 确保您已经定义了一个模型。

      var InvoiceItemModel = Backbone.Model.extend({
    
      });
    
  2. 定义表单视图。

      var InvoiceItemFormView = Backbone.View.extend({
    
        // Define class name of view element.
        className: 'invoice-item-form-view',
      });
    
  3. 将绑定哈希添加到视图中。

        bindings: {
          '#description': 'description',
          '#price': 'price',
          '#quantity': 'quantity'
        }
    

    在这里,我们使用了简短的绑定定义,它作为下一片段中显示的详细定义的别名。

        bindings: {
          '#description': { observe: 'description' },
          '#price': { observe: 'price' },
          '#quantity': { observe: 'quantity' }
        }
    
  4. render() 方法添加到视图,并在渲染后调用 this.stickit()

        render: function() {
          var html = '<label>Description:</label>' + 
            '<input type="text" id="description"></input><br>' +
            '<label>Price:</label>' +
            '<input type="text" id="price"></input><br>' +
            '<label>Quantity:</label>' +
            '<input type="text" id="quantity"></input><br>';
    
          // Set html for the view element using jQuery.
          $(this.el).html(html);
    
          // Here binding occurs.
          this.stickit();
    
          return this;
        }
    
  5. 以类似的方式定义其他视图。

      var InvoiceItemView = Backbone.View.extend({
    
        // Define class name of view element.
        className: 'invoice-item-view',
    
        // Bind HTML elements to the view model.
        bindings: {
          '#description': 'description',
          '#price': 'price',
          '#quantity': 'quantity'
        },
    
        // Render view.
        render: function() {
          var html = 'Description:' +
            '<span id="description"></span>, ' +
            'Price:  <span id="price"></span>, ' +
            'Quantity:  <span id="quantity"></span>.';
    
          // Set html for the view element using jQuery.
          $(this.el).html(html);
    
          // Here binding occurs.
          this.stickit();
    
          return this;
        },
      });
    
  6. 创建一个新的 model 实例。

        var invoiceItemModel = new InvoiceItemModel({
          description: 'Farmer Figure',
          price: 8,
          quantity: 1
        });
    
  7. 将两个视图都附加到 HTML 主体中。

        $('body').append(new InvoiceItemView({
          model: invoiceItemModel
        }).render().el);
        $('body').append(new InvoiceItemFormView({
          model: invoiceItemModel
        }).render().el);
    

它是如何工作的...

每当调用 stickit() 方法时,stickit 扩展会初始化我们定义在绑定哈希中的 HTML 元素的 innerHTML。由于这种初始化,stickit 允许我们保持模板的整洁,并且在渲染视图时,我们不需要手动将模型值传递到 html 变量中。

对于 InvoiceItemView 视图,配置了一对一绑定(模型到视图),因此每次模型属性发生变化时,相应的 HTML 元素都会被更新。

对于 InvoiceItemFormView 视图,stickit 设置了双向绑定(模型到视图,然后视图到模型),将视图元素的变化与绑定模型属性的变化连接和反映。

更多内容...

本节描述了 Backbone.stickit 扩展的高级用法:覆盖模型获取器和设置器、覆盖视图元素更新以及监听特定的 HTML 事件。

覆盖模型获取器和设置器

当获取或设置绑定到我们视图的模型的属性时,我们可以通过指定 onGetonSet 回调来覆盖获取或设置行为。

    bindings: {
      '#price': {
        observe: 'price',
        onGet: 'priceGetter',
        onGet: 'priceSetter'
      }
    },
    priceGetter: function(val, options) { 
      return '$ ' + val; 
    },
    priceSetter: function(val, options) { 
      return Number(val.replace(/[⁰-9\.]+/g, ''));
    }

覆盖视图元素更新

我们可以通过不同的方式覆盖和自定义视图元素更新。我们可以指定一个 update 回调,它在 HTML 元素更新时触发,或者我们可以指定 afterUpdate 回调,它将在之后执行。

    bindings: {
      '#price': {
        observe: 'price',
        update: function($el, val, model, options) { 
          $el.val(val);
        }
        afterUpdate: 'highlight',
      },
      },
      highlight: function($el, val, options) { 
        $el.animate({ backgroundColor: "#ff9999" }, "fast")
          .animate({ backgroundColor: "#ffffff" }, "fast");
      } 
    }

我们还可以通过指定 updateMethod 来覆盖视图元素的值更新。默认情况下,它使用 text 方法,但我们可以将其值更改为 html。如果使用 html 方法,并且我们想在将值分配给 HTML 元素之前对模型值进行转义,我们可以将 escape 选项设置为 true

    bindings: {
      '#price': {
        observe: 'price',
        updateMethod: 'html',
        escape: true
      }
    }

监听特定的 HTML 事件

默认情况下,对于文本框、文本区域和其他可编辑内容的 HTML 元素,Backbone.stickit扩展监听以下事件:keyupchangecutpaste。对于其他元素,Backbone.stickit扩展监听change事件。

然而,可以通过指定events数组来覆盖此设置。

    bindings: {
      '#price': {
        observe: 'price',
        events: ['blur'],
      },
    }

在这种情况下,视图到模型的绑定将在#price文本框的blur事件上发生。

相关内容

  • 在以下配方中,我们将继续学习关于 Stickit 扩展的内容。你还可以在GitHub页面nytimes.github.com/backbone.stickit/上找到关于Backbone.stickit的完整文档。

将模型和集合绑定到选择列表

在上一个配方中,我们讨论了如何将模型绑定到视图的 HTML 任意元素。在本配方中,我们将学习如何将模型绑定到选择元素。通过更改选择列表的值,我们需要更改绑定模型的关联属性。

这有点复杂,因为我们可能希望从数组或集合中获取选择选项的键值对。幸运的是,Backbone.stickit扩展允许我们轻松地做到这一点。

在本配方中,我们将创建一个简单的示例来演示我们如何将模型和集合绑定到选择列表。

将模型和集合绑定到选择列表

准备工作

你可以从GitHub页面github.com/nytimes/backbone.stickit下载Backbone.stickit扩展。要将此扩展包含到你的项目中,将backbone.stickit.js文件保存到lib文件夹中,并在index.html中包含对该文件的引用。

注意

在第一章的使用插件扩展应用程序配方中详细描述了将 Backbone 扩展包含到你的项目中,理解 Backbone

如何操作...

执行以下步骤以将模型和集合绑定到选择列表。

  1. 定义一个模型。

      var InvoiceModel = Backbone.Model.extend({
    
      });
    
  2. 定义一个视图。

      var InvoiceView = Backbone.View.extend({
    
        // Define class name of view element.
        className: 'invoice-item-view',
    
        },
    
        // Render view.
        render: function() {
          var html = 'Status: <select id="items"></select>';
    
          // Set html for the view element using jQuery.
          $(this.el).html(html);
    
          // Here binding occurs.
          this.stickit();
    
          return this;
        },
      });
    
  3. 将绑定哈希添加到视图中。

        // Bind HTML elements to the view model.
        bindings: {
          'select#items': {
            observe: 'status',
    
            // Define additional options for select element.
            selectOptions: {
    
              // You can return regular Backbone collection or
              // an array of objects.
              collection: function() {
                return [
                  {name: null, label: '- Status-'},
                  {name: 'in_progress', label: 'In Progress'},
                  {name: 'complete', label: 'Complete'}
                ]
              },
    
              // Set the path to the label value for select
              // options within the collection of objects.
              labelPath: 'label',
    
              // Define the path to the values for select options
              // within the collection of objects. 
              valuePath: 'name'
            }
          }
    
  4. 创建一个新的模型实例。

        var invoiceModel = new InvoiceModel({ 
          status: 'in_progress' 
        });
    
  5. 渲染视图。

    $('body').append(new InvoiceView({
      model: invoiceModel
    }).render().el);
    

它是如何工作的...

Backbone.stickitcollection属性获取选择列表选项的值,并假设它定义了相对于窗口对象的集合路径或一个返回集合的函数。也可以使用数组代替集合,如前一个示例所示。

labelPath指示一个指向集合对象属性的路径,该属性用作选择列表选项的标签,而valuePath定义了选项值的路径。

相关内容

在视图中处理键盘快捷键

为了提供最佳的用户体验,您的应用程序应该支持在应用程序内进行各种类型的导航。其中一种方法可以通过使用快捷键实现。快捷键是一组按键组合,它提供了更容易访问命令或操作的方式。

在本食谱中,我们将处理我们在 绑定集合到视图 食谱中实现的视图的一些快捷键。

要执行键盘快捷键处理,我们将使用 Moustrap 库和 Backbone.Moustrap 扩展,它们提供了我们需要的功能。

准备工作

您可以从 GitHub 页面分别下载 Moustrap 库和 Backbone.Moustrap 扩展:github.com/ccampbell/mousetrapgithub.com/elasticsales/backbone.mousetrap

要将它们包含到您的项目中,请将 mousetrap.jsbackbone.mousetrap.js 文件保存到 lib 文件夹中,并在 index.html 中包含对它们的引用。

注意

在 第一章 的 使用插件扩展应用程序 食谱中详细描述了将 Backbone 扩展包含到您的项目中。

如何做到...

要执行键盘快捷键处理,请将以下属性添加到视图对象中:

    keyboardEvents: {
      'shift+n': 'addNewInvoiceItem',
      'shift+d': 'removeInvoiceItem',
    },

它是如何工作的...

Backbone.Mousetrap 在创建视图时自动将键盘事件委派给视图,在移除视图或调用 undelegateEvents() 时取消委派。

以下键 shiftctrlaltoptionmetacommand 都是可用的。其他特殊键包括 backspacetabenterreturncapslockescescapespacepageuppagedownendhomeleftuprightdowninsdel

您应该能够通过名称引用任何其他键,例如 a/$*=

默认情况下,当浏览器聚焦在任何表单元素(如输入、文本区域或选择框)上时,Mousetrap 会阻止处理快捷键事件。但是,如果您想处理此类元素的快捷键事件,可以向其添加 mousetrap 类。

<textarea name="message" class="mousetrap"></textarea>

参见

处理路由事件

虽然处理路由事件的用例不多,但 Backbone.js 提供了一种机制来实现这一点。在本食谱中,我们将创建一个简单的应用程序来记录路由事件。

处理路由事件

如何做到...

按照以下步骤处理路由事件。

  1. 监听 Backbone.Historyroute 事件。

        initialize: function() {  
          Backbone.history.on('route', this.routeTracker);
        },
    
  2. 定义 route 事件回调。

        routeTracker: function(router, route, params) {
          console.log(
           'Route: ' + route + '. Params: ' + params + '.'
          );
        },
    

它是如何工作的...

在路由成功执行后,会触发 route 事件。route 事件回调函数接受以下参数:

  • router:此参数表示正在使用的当前路由器

  • 路由: 此参数指示一个路由回调名称

  • 参数: 这表示传递给路由回调的参数

还有更多...

要处理特定路由的特定事件,请监听 route:[name] 事件。

  var Workspace = Backbone.Router.extend({
    routes: {
      '': 'invoiceList',
      'invoice': 'invoiceList',
      'invoice/:id': 'invoicePage',
    },

    initialize: function() {
       this.on('route:invoicePage', this.invoicePageEvent);
    },

    invoicePageEvent: function(param1, param2) {
      console.log(param1);
    },
});

在这种情况下,事件回调接受 routes 参数。

参见

  • 更多关于 routes 的信息可以在第一章中找到,在实现应用程序中的 URL 路由配方中,理解 Backbone

第六章。模板和 UX 糖

在本章中,我们将介绍以下食谱:

  • 在视图中使用模板

  • 实现模板加载器

  • 使用 Mustache 模板

  • 定义一个表单

  • 向表单添加验证

  • 处理表单事件

  • 使用 Bootstrap 框架自定义表单

  • 使用 LayoutManager 组装布局

  • 构建语义化和易于样式的数据网格

  • 在 HTML5 画布上绘制

简介

本章向您介绍了模板,它们用于将 HTML 标记与应用程序代码分离。因此,应用程序变得更加结构化和整洁。我们将讨论 Underscore.js 提供的模板引擎,并学习如何将 Backbone 与第三方模板引擎(如 Mustache.js)集成。

此外,我们还将讨论有用的 Backbone 扩展,它允许使用表单、布局和网格。

在视图中使用模板

在这个食谱中,您将学习如何在 Backbone 视图中使用模板。默认情况下,Backbone.js 与 Underscore.js 提供的模板引擎集成。

让我们以 第四章 中 在视图中渲染集合 食谱的例子为例,我们在视图中渲染了一个集合,并使用 Underscore 的模板引擎更新了代码。结果将类似于以下图像:

在视图中使用模板

如何操作...

按照以下步骤在视图中使用模板:

  1. 确保您已定义模型和集合对象。

      var InvoiceItemModel = Backbone.Model.extend({
    
      });
    
      var InvoiceItemCollection = Backbone.Collection.extend({
        model: InvoiceItemModel
      });
    
  2. 使用包含模板的 template 属性定义一个视图。然后,在渲染视图时,使用 template() 返回渲染的 HTML。

      var InvoiceItemListView = Backbone.View.extend({
    
        // HTML element name, where to render a view.
        tagName: 'ul',
        // Define template.
        template: _.template(
          '<% _.each(items, function(item) { %>' + 
          '   <li>' + 
          '      Description: <%= item.description %>.' +
          '      Price: <%= item.price %>.' +
          '      Quantity: <%= item.quantity %>.' +
          '   </li>' +
          '<% }); %>'
        ),
    
        // Render view.
        render: function() {
    
          // Render template and set html for the view element 
          // using jQuery.
          this.$el.html(this.template({
            items: this.collection.toJSON()
          }));
    
          return this;
        }
      });
    
  3. 创建一个集合实例。

    var invoiceItemCollection = new InvoiceItemCollection([
      { description: 'Wooden Toy House', price: 22, quantity: 3 },
      { description: 'Farm Animal Set', price: 17, quantity: 1 },
      { description: 'Farmer Figure', price: 8, quantity: 1 },
      { description: 'Toy Tractor', price: 15, quantity: 1 }
    ]);
    
  4. 创建一个视图实例,渲染它,并将结果设置为 body 的值。

    $('body').html(new InvoiceItemListView({
      collection: invoiceItemCollection
    }).render().el);
    

它是如何工作的...

通过 Underscore.js 提供的 _.template() 方法,我们可以在 <% … %> 括号内包含 JavaScript 代码的 HTML 模板中定义。要将变量输出到模板中,我们需要使用 <%= … %> 语法,而要将转义后的 HTML 变量输出,我们可以使用 <%- … %> 语法。

此外,在 render() 方法中,我们将以 JSON 格式将集合项传递给模板。

更多内容...

在本节中,我们将学习如何将模板拆分为部分。

将模板拆分为部分

部分是一个可以从其他模板中调用的模板,作为一个函数。

如果我们想要重用现有模板的部分,我们可以将一个模板拆分为不同的部分。为此,请按照以下步骤操作:

  1. 定义模板部分。

    itemTemplate: _.template(
      'Description: <%= description %>.' + 
      'Price: <%= price %>.' +
      'Quantity: <%= quantity %>.'
    ),
    
  2. 定义主模板。

    template: _.template(
      '<% _.each(items, function(item) { %>' +
      '  <li>' +
      '    <%= itemTemplate(item) %>' +
      '  </li>' +
      '<% }); %>'
    ),
    
  3. 在渲染模板时,将部分方法作为设置传递。

    this.$el.html(this.template({
      items: this.collection.toJSON(),
      itemTemplate: this.itemTemplate
    }));
    

相关内容

要获取有关 Underscore.js 中模板的更多信息,您可以参考官方文档underscorejs.org/#template

实现模板加载器

在一个大型应用程序中,遵循关注点分离范式,将模板存储在视图之外是很重要的,这样网页设计师可以轻松地修改它们,而不会损害视图。这种做法也提供了应用程序内的模板可重用性。

小贴士

将所有模板存储在单个 HTML 文件中

对于服务器端应用程序,开发者通常将模板存储在单独的文件中,以实现方便访问和编辑它们。然而,这种方法几乎不能应用于客户端应用程序,因为它会使浏览器从服务器下载多个小文件,从而延迟应用程序的启动。

在这个菜谱中,我们将把模板存储在单独的 HTML 文件中,除了视图之外。我们还将编写一个模板加载器,它将加载这些模板到内存中,允许从应用程序的各个部分访问它们。

如何操作...

按照以下步骤实现模板加载器:

  1. 将包含在script标签中的模板添加到index.html文件的头部分。设置id属性以区分不同的模板。

    <head>
    
      …
    
      <script type="text/html" class="template" id="items">
        <% _.each(items, function(item) { %>
          <li>
            <%= itemTemplate(item) %>
          </li>
        <% }); %>
      </script>
    
      <script type="text/html" class="template" id="item">
        Description: <%= description %>.
        Price: <%= price %>.
        Quantity: <%= quantity %>
      </script>
    
    </head>
    
  2. 创建一个模板加载实用工具,并将其放置到js/template-loader.js文件中。

    (function($){
    
      $(document).ready(function () {
    
      	 // Store variable within global jQuery object.
        $.tpl = {}
    
        $('script.template').each(function(index) {
    
          // Load template from DOM.
          $.tpl[$(this).attr('id')] = _.template($(this).html());
    
          // Remove template from DOM.
          $(this).remove();
        });
      });
    
    })(jQuery);
    
  3. 将模板加载器包含到index.html文件中。

    <head>
      ...
      <script src="img/template-loader.js"></script>
      …
    </head>
    
  4. 在渲染视图时,使用全局$.tpl数组中定义的模板。

    this.$el.html($.tpl'items',
      itemTemplate: $.tpl['item']
    }));
    

它是如何工作的...

由于我们在index.html中定义了我们的模板,它们可以即时加载。然后,在模板加载器中,当文档完全加载后,我们将它们移动到全局变量$.tpl中,并从 DOM 中删除模板。这应该会加快我们模板的进一步使用,就像我们在 JS 文件中定义它们一样。现在,我们可以在应用程序的不同视图中使用这些模板。

使用 Mustache 模板

Mustache 是一种美丽且无逻辑的模板语法。它可以用于 HTML、配置文件、源代码等。存在各种针对不同语言的 Mustache 实现,例如 JavaScript、PHP、Ruby、Python 以及许多其他语言。

在本章中,我们将学习如何使用 Mustache.js,这是 JavaScript 的 Mustache 实现,与 Backbone.js 一起使用。

准备工作

您可以从其 GitHub 页面github.com/janl/mustache.js下载 Mustache.js。要将 Mustache.js 包含到您的项目中,将mustache.js文件保存到lib文件夹中,并在index.html中包含对其的引用。

在第一章的使用插件扩展应用程序菜谱中详细描述了如何将 Backbone 扩展包含到您的项目中。

如何操作...

按照以下步骤使用 Mustache 模板:

  1. 在视图中定义一个 Mustache 模板。

    // Define template.
    template: '{{#items}}<li>' +
              '  Description: {{description}}' +
              '  Price: {{price}}.' +
              '  Quantity: {{quantity}}.' +
              '</li>{{/items}}',
    
  2. 运行Mustache.render()方法来渲染模板。

    this.$el.html(
      Mustache.render(this.template, {
        items: this.collection.toJSON()
      })
    );
    

它是如何工作的...

Mustache.render()将模板字符串编译成 JavaScript 代码,然后执行它。模板字符串包含像{{placeholder}}这样的占位符,这些占位符将被第二个参数中提供的值替换。

还有更多...

本节描述了如何在 Mustache.js 中使用编译后的模板和部分。

使用编译后的模板

为了提高应用程序的性能,您可以在使用之前通过调用Mustache.compile()编译模板。此方法接受模板字符串作为单个参数,并返回一个 JavaScript 函数,可以调用以返回 HTML 代码。以下示例演示了如何操作:

  var InvoiceItemListView = Backbone.View.extend({
    tagName: 'ul',

    template: Mustache.compile(
                '{{#items}}<li>' + 
                '  Description: {{description}}' +
                '  Price: {{price}}.' +
                '  Quantity: {{quantity}}.' +
                '</li>{{/items}}'
              ),

    render: function() {
      this.$el.html(this.template({
        items: this.collection.toJSON()
      }));

      return this;
    }
  });

使用部分

与 Underscore 模板一样,Mustache.js 允许使用部分。要调用部分模板,请使用>语法。

{{#items}}
  <li>{{> item }}</li>
{{/items}}

部分模板将如下所示:

  Description: {{description}}
  Price: {{price}}.
  Quantity: {{quantity}}.

您可以通过以下几种方式传递部分模板:

  • 可以将部分对象(也是字符串)作为第三个参数传递给Mustache.render()。该对象应按部分的名称键入,其值应为部分文本。

    Mustache.render(
      this.template,
      { items: this.collection.toJSON() },
      { item: this.itemTemplate }
    );
    
  • 模板部分也可以使用Mustache.compilePartial()函数进行编译。此函数的第一个参数是部分在父模板中的名称。第二个参数是部分模板字符串。

    Mustache.compilePartial(
      'item', 
      'Description: {{description}}. Price: {{price}}.\
       Quantity: {{quantity}}.'
    );
    

相关内容

要了解更多关于 Mustache.js 语法的知识,您可以访问其官方 GitHub 页面github.com/janl/mustache.js

定义表单

几乎任何 Web 应用程序都需要 HTML 表单来收集用户输入。在前面的章节中,我们学习了如何手动渲染表单并将其绑定到视图模型。

然而,我们应该寻找允许我们通过编写更少的代码来更轻松地处理表单的 backbone-forms 扩展。在本配方和后续配方中,我们将学习如何使用此扩展。

让我们为BuyerModel创建一个简单的表单,它将如下截图所示:

定义表单

准备工作

要将 backbone-forms 添加到您的项目中,请从 GitHub 页面下载整个扩展存档(github.com/powmedia/backbone-forms),并将其提取到lib/backbone-forms目录中。然后,将扩展文件的引用包含到index.html中。

<link href="lib/backbone-forms/distribution/templates/default.css" rel="stylesheet" />

<script src="img/backbone-forms.min.js"></script>

在第一章的使用插件扩展应用程序配方中详细描述了将 Backbone 扩展添加到您的项目中,理解 Backbone

如何操作...

按照以下步骤定义一个表单:

  1. 在模型对象内部定义表单模式定义。

    var BuyerModel = Backbone.Model.extend({
      schema: {
        title:   { type: 'Select', options: ['Mr', 'Mrs', 'Ms'] },
        name:    'Text',
        email:   { validators: ['required', 'email'] },
        birthday:'Date',
      }
    });
    
  2. 创建一个视图,该视图应使用Backbone.Form对象渲染表单。

      var BuyerFormView = Backbone.View.extend({
        render: function() {
          this.form = new Backbone.Form({ model: this.model });
    
          this.$el.html('<h3>Enter buyer details below</h3>');
          this.$el.append(this.form.render().el);
          this.$el.append('<button>Submit</button>');
    
          return this;
        },
      });
    
  3. 向视图添加一个submit回调。在这个回调中,表单被验证,其值通过表单的commit()方法传递给模型。

        events: {
          'click button': 'submit'
        },
    
        submit: function() {
          this.form.commit();
    
          console.log(this.model.toJSON());
          // Object { title: "Mr", name: "John Doe",
          // email: "john.doe@example.com",
          // birthday: Thu Mar 20 1986 00:00:00 GMT+0200 (EET) }
        }
    

它是如何工作的...

Backbone.Form 对象通过重写 render() 方法扩展了 Backbone.Views,在该方法中,它根据我们传递给模型的模式定义构建表单。如果模型有初始值,则这些值将被分配给表单元素。

通过执行 commit() 方法,执行表单验证并将表单值分配给模型属性。如果将 {validate: true} 选项传递给此方法,则同时执行表单验证和模型验证。

更多...

本节描述了如何在没有模型的情况下构建表单。

使用无模型的表单

我们可以创建一个表单,而不需要将模式定义绑定到模型上。以下示例展示了如何实现:

var form = new Backbone.Form({
  data: {
    title: 'Mr',
    name: 'John Doe',
    email: 'john.doe@example.com',
    birthday: '1986-03-20'
  },

  schema: {
    title:   { type: 'Select', options: ['Mr', 'Mrs', 'Ms'] },
    name:    'Text',
    email:   { validators: ['required', 'email'] },
    birthday:'Date',
  }
}).render();

要获取表单值,请使用 getValue() 方法。

var this.data = this.getValue();

相关链接

要了解更多关于模式定义的信息,您可以查看他们的官方文档,链接为github.com/powmedia/backbone-forms#schema-definition。在后续的食谱中,我们将继续学习 backbone-forms 扩展。

向表单添加验证

在本食谱中,我们将继续学习 backbone-forms 扩展,并将讨论表单验证,这是一个非常有用的功能,几乎任何利用 backbone-forms 扩展的 Web 应用程序都需要。

如何实现...

按照以下步骤向表单添加验证:

  1. 确保您已经定义了模型模式。

      var BuyerModel = Backbone.Model.extend({
        schema: {
          email: 'Text',
        }
      });
    
  2. 添加验证器。

      var BuyerModel = Backbone.Model.extend({
        schema: {
          email: {
            type: 'Text',
            validators: ['required', 'email']
          }
        }
      });
    
  3. 设置验证消息。

      var BuyerModel = Backbone.Model.extend({
        schema: {
          email: {
            type: 'Text',
            validators: [
              {
                type: 'required',
                message: 'Email field is required'
              },
              'email'
            ],
          }
        }
      });
    

它是如何工作的...

要启用验证,我们需要将 validators 数组传递给模式字段定义。验证器可以是一个字符串、一个对象、一个正则表达式(regular expression)或一个函数。

使用字符串设置内置验证器,这些验证器不需要额外的参数。这些验证器是 requiredemailurl。如果验证器需要额外的参数(例如,matchregexp),或者如果我们想覆盖错误消息,我们需要使用对象来定义验证器。

password: {
  validators: [ {
    type: 'match',
    field: 'passwordConfirm', 
    message: 'Passwords must match!'
  }]
}

要执行自定义验证器,我们需要传递一个带有两个参数的验证函数:value,它是表单元素的值,以及 formValues,它是所有表单值的哈希。

//Custom function 
username: { validators: [ 
  function checkUsername(value, formValues) { 
    var err = { 
      type: 'username', 
      message: 'Usernames must be at least 3 characters long' 
    }; 

    if (value.length < 3) return err; 
  } 
] }

验证是在调用 form.validate()form.commit() 方法时执行的。

更多...

本节描述了更多关于表单验证的信息。

自定义错误信息

对于特定类型的所有内置验证器,可以同时覆盖错误消息。通过在 Backbone.Form.validators.errMessages(配置对象)中覆盖值,可以轻松地做到这一点。我们可以使用 Mustache 标签。以下是实现方式:

Backbone.Form.validators.errMessages.required =
  'Please enter a value for this field.';

Backbone.Form.validators.errMessages.match =
  'This value must match the value of {{field}}';

Backbone.Form.validators.errMessages.email =
  '{{value}} is an invalid email address.';

执行模型验证

如果您想在提交或验证表单时执行模型验证,请确保模型的 validate() 方法返回一个按字段名称键控的错误消息对象。

    validate: function(attrs) {
      var errs = {};

      if (this.usernameTaken(attrs.username)) {
        errs.username = 'The username is taken'
      }

      if (!_.isEmpty(errs)) return errs;
    },

相关链接

要了解更多关于表单验证的信息,您可以参考github.com/powmedia/backbone-forms#validation上的文档。

处理表单事件

Backbone.Form扩展提供了我们可以在应用程序中使用的一些事件。例如,通过利用这些事件,我们可以实现一些特定的功能,其中一个字段的值依赖于另一个字段的值。

在这个菜谱中,我们将为InvoiceModel模型创建一个表单,其中支付日期字段仅在支付选项作为状态字段值被选中时显示。我们的表单将如下截图所示:

处理表单事件

如何操作...

按照以下步骤处理表单事件:

  1. 定义模型和表单模式。

      var InvoiceModel = Backbone.Model.extend({
        schema: {
          referenceNumber: { type: 'Text'},
    
          date: { type: 'Date'},
    
          status: {
            type: 'Select',
            options: [
              { val: 'draft', label: 'Draft' },
              { val: 'issued', label: 'Issued' },
              { val: 'paid', label: 'Paid' },
              { val: 'canceled', label: 'Canceled' }
            ]
          }
    
          paidDate: { type: 'Date' },
        }
      });
    
  2. 根据Backbone.Form创建InvoiceForm

      var InvoiceForm = Backbone.Form.extend({
    
      }
    
  3. 覆盖父类的initialize()方法,将status字段的change事件绑定到回调函数,该函数将更新相关字段。

        initialize: function() {
    
          // Call parent method.
          InvoiceForm.__super__.initialize.apply(this, arguments);
    
          // Bind change status change event to the
          // update callback.
          this.on('status:change', this.update);
        }
    
  4. 实现表单的update方法,该方法将更新相关字段。

        update: function(form, editor) {
          if (form.fields.status.editor.getValue() == 'paid') {
            form.fields.paidDate.$el.show();
          }
          else {
            form.fields.paidDate.$el.hide();  
          }
        }
    
  5. 覆盖表单的render方法,在那里我们需要运行update方法以确保相关字段正确显示。

        render: function() {
    
          // Call parent method. 
          InvoiceForm.__super__.render.apply(this, arguments);
          // Esnure dependent are shown properly.
          this.update(this);
    
          return this;
        }
    

它是如何工作的...

Backbone.Form提供了几个表单事件,我们可以使用on()方法将它们绑定到我们的回调函数。它们是:

  • change:每当发生影响form.getValue()结果的事件时,都会触发此事件。

  • focus:每当此表单获得焦点时,即当此表单内编辑器的输入成为document.activeElement时,都会触发此事件。

  • blur:每当此表单失去焦点时,即当此表单内编辑器的输入停止成为document.activeElement时,都会触发此事件。

  • <key>:<event>changefocusblur事件会针对由key指定的表单元素触发。

Backbone.Form扩展了Backbone.Views并实现了initialize()render()方法。在我们的子对象中,我们需要使用这些方法,因此我们需要确保父方法被执行,这是由于 JavaScript 的__super__关键字所实现的。然后,应用该方法。

参见

  • 第五章中处理 Backbone 对象的事件菜谱,事件和绑定

使用 Bootstrap 框架自定义表单

默认的 backbone-form 样式看起来相当无聊,我们可能想用像 Bootstrap 这样的酷炫样式来替换它们。在这种情况下,我们的表单看起来会更好,如下面的截图所示:

使用 Bootstrap 框架自定义表单

在这里,我们也使用一个列表元素(即编辑器)来允许用户输入发票项目详情。当用户点击添加按钮时,以下弹出窗口会生成并显示给用户:

使用 Bootstrap 框架自定义表单

准备工作

按照以下步骤准备使用 Bootstrap.js:

  1. 从其 GitHub 页面twitter.github.com/bootstrap下载 Bootstrap 框架存档,并将其提取到应用程序的lib文件夹中。

  2. index.html中移除default.css样式的引用。

  3. 将 Bootstrap 文件包含到index.html中。

    <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.css" />
    
    <script src="img/bootstrap.js"></script>
    
  4. 包含对Backbone.Forms扩展、列表编辑器、Bootstrap 模态适配器、Bootstrap 模板和样式的链接。

    <script src="img/backbone-forms.js">
    </script>
    
    <script src="img/list.js">
    </script>
    
    <script src="img/backbone.bootstrap-modal.js">
    </script>
    
    <script src="img/bootstrap.js">
    </script>
    
    <link rel="stylesheet" href="lib/backbone-forms/distribution/templates/bootstrap.css" />
    

在第一章的使用插件扩展应用程序配方中详细描述了将 Backbone 扩展包含到您的项目中。

如何做...

按照以下步骤使用 Bootstrap 框架自定义表单:

  1. 将以下代码行添加到main.js中,以设置默认模态适配器:

    Backbone.Form.editors.List.Modal.ModalAdapter = Backbone.BootstrapModal;
    
  2. 将发票项目字段定义添加到 Backbone 模式中。

          items:    {
            type: 'List', itemType: 'Object', subSchema: {
              description: { validators: ['required'] },
              price: 'Number',
              quantity: 'Number',          
            }
          }
    

它是如何工作的...

我们包含了覆盖默认 backbone-forms 模板和样式的文件,以实现与 Bootstrap 框架的集成。此外,我们还使用了列表元素,它会对 Bootstrap 模态适配器进行特殊调用,以显示一个漂亮的模态弹出窗口。

更多...

本节描述了如何覆盖表单模板。

覆盖表单模板

在上一个示例中,我们将lib/backbone-forms/distribution/templates/bootstrap.js包含到我们的项目中,以确保使用适当的模板,以便与 Bootstrap 引擎提供集成。在此文件中,通过调用Backbone.Form对象的setTemplates()方法来覆盖默认模板。

  var Form = Backbone.Form;

  Form.setTemplates({
    form:
      '<form class="form-horizontal">{{fieldsets}}</form>',

    // ...
    field:
      '<div class="control-group field-{{key}}">' +
      '  <label class="control-label" for="{{id}}">' +
      '    {{title}}' +
      '  </label>' +
      '  <div class="controls">' +
      '    {{editor}}' +
      '    <div class="help-inline">{{error}}</div>' +
      '    <div class="help-block">{{help}}</div>' +
      '  </div>' +
      '</div>',
  }, {
    error: 'error'
    // Set error class on the field tag when validation fails
  });

模板定义使用 Mustache 语法,可以覆盖的模板有:formfieldsetfieldnestedFieldlistlistItemdatedateTime'list.Modal'

要使用与为表单元素定义的模板不同的特定模板,添加一个模板,并在模式定义中的模板参数中传递其名称。

title: { type: 'Select', options: ['Mr', 'Mrs', 'Ms'], template: 'customField'}

要为表单使用特定的模板,在创建新表单时传递其名称。

this.form = new Backbone.Form({
  model: this.model, template: 'customForm'
});

参见

您可以通过查看twitter.github.com/bootstrap上的官方 Bootstrap.js 文档来了解更多信息。还可以查看lib/backbone-forms/distribution/templates/default.js文件,以找出所有可覆盖的模板。

使用 LayoutManager 组装布局

Backbone.LayoutManager是 Backbone.js 最有用的扩展之一。它允许轻松构建由面板组成的布局,并且与仅使用 Backbone 视图相比,可以减少许多代码行。LayoutManager 还提供了从主 HTML 文件或外部文件加载模板的机制。

让我们构建一个将有两个面板的应用程序。在第一个面板中,用户将看到发票列表,而在另一个面板中,他将看到发票详情。

使用 LayoutManager 组装布局

通过单击第一个面板中的发票号码,我们的应用程序将立即更新第二个面板。

准备工作

您可以从其 GitHub 页面github.com/tbranyen/backbone.layoutmanager下载 Backbone.LayoutManager。要将 LayoutManager 包含到您的项目中,将backbone.layoutmanager.js文件保存到lib文件夹中,并在index.html中包含对其的引用。

在第一章的“使用插件扩展应用程序”食谱中详细描述了如何将 Backbone 扩展添加到您的项目中,理解 Backbone

如何做到这一点...

按照以下步骤组装布局:

  1. 确保您已定义模型和集合对象。

      var InvoiceModel = Backbone.Model.extend({
    
      });
    
      var InvoiceCollection = Backbone.Collection.extend({
        model: InvoiceModel
      });
    
  2. 定义发票列表面板。

      var InvoiceListPane = Backbone.Layout.extend({
    
        // Returns selector for template.
        template: "#invoice-list-pane",
    
        // Set selector for template.
        serialize: function() {
          return {
            // Wrap the collection.
            invoices: _.chain(this.collection.models)
          };
        }
      });
    
  3. 定义发票面板。

      var InvoicePane = Backbone.Layout.extend({
    
        // Set selector for template.
        template: "#invoice-pane",
    
        // Returns data for template.
        serialize: function() {
          return {
            invoice: this.model
          };
        }
      });
    
  4. 定义一个带有路由的 router 并在其initialize()方法中创建集合实例。

      var Workspace = Backbone.Router.extend({
        routes: {
          '': 'page',
          'invoice/:id': 'page',
        },
    
        // Initialize function run when Router object instance// is created.
        initialize: function() {
          //  Create collection
          this.collection = new InvoiceCollection([
            {
              referenceNumber: 'AB 12345',
              date: new Date().toISOString(),
              status: 'draft'
            },
            {
              referenceNumber: 'ZX 98765',
              date: new Date().toISOString(),
              status: 'issued'
            },
          ]);
        },
    
      });
    
  5. 向路由器添加页面回调,它创建一个Backbone.Layout对象并将其渲染。

        page: function(id) {
          if (!id) {
            // Set default id.
            id = this.collection.at(0).cid;
          }
    
          var layout = new Backbone.Layout({
            // Attach the layout to the main container.
            el: "body",
    
            // Set template selector.
            template: "#layout",
    
            // Declaratively bind a nested View to the layout.
            views: {
              "#invoice-list-pane": new InvoiceListPane({
                collection: this.collection
              }),
              "#invoice-pane": new InvoicePane({
                model: this.collection.get(id)
              }),
            }
          });
    
          // Render the layout.
          layout.render();
        },
    
  6. 将模板添加到页面元素的<head>标签中。

      <script class="template" type="template" id="layout">
        <h1>Invoice application</h1>
        <div id="invoice-list-pane"></div>
        <div id="invoice-pane"></div>
      </script>
    
      <script class="template"
          type="template"id="invoice-list-pane">
        <h3>Invoices:</h3>
        <ul>
          <% invoices.each(function(invoice) { %>
            <li>
              <a href="#invoice/<%= invoice.cid %>">
                <%= invoice.get('referenceNumber') %>
              </a>
            </li>
          <% }); %>
        </ul>
      </script>
    
      <script class="template" type="template" id="invoice-pane">
        <h3>Invoice details:</h3>
        Reference Number:
          <%= invoice.get('referenceNumber') %><br>
        Date: <%= invoice.get('date') %><br>
        Status: <%= invoice.get('status') %><br>
      </script>
    

它是如何工作的...

Backbone.LayoutManager对象实现了模板加载器、render()方法,并提供了许多其他酷炫的功能,这通常是开发者所做的事情。在视图选项中,我们可以选择将哪个布局面板或 Backbone 视图附加到主模板中指定的 HTML 元素。

参见

请参阅 LayoutManager 文档以了解有关扩展的更多信息,github.com/tbranyen/backbone.layoutmanager/wiki

构建语义化和易于样式的数据网格

在您的应用程序中,您可能希望以可排序、可过滤和可编辑的网格形式输出数据,这直接从头开始做并不容易。在本食谱中,我们将学习使用 Backgrid.js,Backbone 应用程序构建数据网格的强大扩展,来快速解决这个问题。

在此应用程序中,我们将使用 Backgrid 示例创建一个简单的网格。它将看起来像以下截图:

构建语义化和易于样式的数据网格

当用户点击列标题时,网格将按此列排序。

构建语义化和易于样式的数据网格

如果用户双击特定单元格,则该单元格将被输入元素替换,用户可以在其中输入新值。

构建语义化和易于样式的数据网格

准备工作

按照以下步骤准备使用 Backgrid 扩展:

  1. 从其官方网站backgridjs.com/下载 Backgrid.js 扩展。

  2. 通过将此扩展提取到lib/backgrid文件夹中来将 Backgrid.js 包含到您的项目中。

  3. index.html中包含对扩展文件的引用。

    <link rel="stylesheet" href="lib/backgrid/lib/backgrid.css" />
    <script src="img/backgrid.js"></script>
    

在第一章的“使用插件扩展应用程序”食谱中详细描述了如何将 Backbone 扩展添加到您的项目中,理解 Backbone

如何操作...

按照以下步骤构建网格:

  1. 确保您已定义了模型和集合对象。

      var InvoiceModel = Backbone.Model.extend({
    
      });
    
      var InvoiceCollection = Backbone.Collection.extend({
        model: InvoiceModel
      });
    
  2. 创建一个集合实例。

        var invoiceCollection = new InvoiceCollection();
    
  3. 定义网格列设置。

        var columns = [
          {
            name: "referenceNumber",
            label: "Ref #",
            editable: false,
            cell: 'string'
          },
          {
            name: "date",
            label: "Date",
            cell: "date"
          },
          {
            name: "status",
            label: "Status",
            cell: Backgrid.SelectCell.extend({
              optionValues: [
                ['Draft', 'draft'],
                ['Issued', 'issued']
              ]
            })
          }
        ];
    
  4. 初始化一个新的网格实例。

        var grid = new Backgrid.Grid({
          columns: columns,
          collection: invoiceCollection
        });
    
        $('body').append(grid.render().$el);
    
        invoiceCollection.add([
          {
            referenceNumber: 'AB 12345',
            date: new Date().toISOString(),
            status: 'draft'
          },
          {
            referenceNumber: 'ZX 98765',
            date: new Date().toISOString(),
            status: 'issued'
          },
        ]);
    
  5. 将模型添加到集合中。

        invoiceCollection.add([
          {
            referenceNumber: 'AB 12345',
            date: new Date().toISOString(),
            status: 'draft'
          },
          {
            referenceNumber: 'ZX 98765',
            date: new Date().toISOString(),
            status: 'issued'
          },
        ]);
    
  6. 启动应用程序。

      Backbone.history.start();
    

它是如何工作的...

Backgrid.Grid 扩展了 Backbone.View,因此您可以创建其实例并传递通过 columns 参数键控的列设置。列设置定义为数组,每一行具有以下属性:

  • name:它是模型属性的名称。

  • label:它是标题列的标签。

  • sortable:它返回一个布尔值以检查列是否可排序。

  • editable:它返回一个布尔值以检查列是否可编辑。

  • cell:它是单元格类型,可以是以下之一:datetimedatetimenumberintegerstringuriemailbooleanselect

如果需要为单元格类型指定额外的参数,您可以扩展相应的类并将其传递给 cell 属性。

Backgrid.SelectCell.extend({
  optionValues: [
    ['Draft', 'draft'],
    ['Issued', 'issued']
  ]
})

还有更多...

在本节中,我们将使用几个 Backgrid 扩展,这些扩展可以为我们的网格添加额外功能。

在网格模型上执行批量操作

我们将向我们的网格添加一个额外的列,该列将包含复选框,允许用户在网格中选择特定的模型并对其执行批量操作,例如删除。以下截图显示了我们的表格将看起来像什么:

在网格模型上执行批量操作

要完成此任务,请按照以下步骤操作:

  1. 将 SelectAll 扩展文件包含到 index.html 中。

    <link rel="stylesheet" href="lib/backgrid/lib/extensions/select-all/backgrid-select-all.css" />
    
    <script src="img/backgrid-select-all.js"></script>
    
  2. 将网格包裹在 TableView 中。

      var TableView = Backbone.View.extend({
        initialize: function(columns, collection) {
          this.collection = collection;
    
          this.grid = new Backgrid.Grid({
            columns: columns,
            collection: this.collection
          });
        },
    
        render: function() {
          this.$el.html(this.grid.render().$el);
    
          return this;
        },
      });
    
  3. initialize() 方法中添加复选框的列。

        initialize: function(columns, collection) {
          this.collection = collection;
    
          columns = [{
            name: "",
            cell: "select-row",
            headerCell: "select-all",
          }].concat(columns)
    
          this.grid = new Backgrid.Grid({
            columns: columns,
            collection: this.collection
          });
        },
    
  4. render() 方法中追加删除按钮。

        render: function() {
          this.$el.html(this.grid.render().$el);
    
          this.$el.append('<button class="delete">Delete</button>');
    
          return this;
        },
    
  5. 处理按钮点击事件。

        events: {
          'click button.delete': 'delete'
        },
    
        delete: function() {
          _.each(this.grid.getSelectedModels(), function (model) {
            model.destroy();
          });
        }
    
  6. 创建一个新的 TableView 实例并将其追加到 body 元素中。

    $('body').append(new TableView(columns, invoiceCollection).render().$el);
    

执行记录过滤

为了允许用户过滤记录,我们将使用 Select 扩展和 Lunr.js 库,这些库包含在 Backgrid 包中。此外,我们将应用 Bootstrap 样式以使搜索框看起来整洁。

执行记录过滤

按照以下步骤执行记录过滤:

  1. 将 Select 扩展、Lunr 库和 Bootstrap 文件包含到 index.html 中。

    <link rel="stylesheet" href="lib/backgrid/assets/css/bootstrap.css" />
    
    <link rel="stylesheet" href="lib/backgrid/lib/extensions/filter/backgrid-filter.css" />
    
    <script src="img/lunr.js"></script>
    
    <script src="img/backgrid-filter.js"></script>
    
  2. 如我们在 在网格模型上执行批量操作 部分中所做的那样,将网格包裹在 TableView 中。

  3. TableView.initalize() 方法中初始化 ClientSideFilter

    this.clientSideFilter =
          new Backgrid.Extension.ClientSideFilter({
            collection: collection,
            placeholder: "Search by Ref #",
            fields: ['referenceNumber'],
            wait: 150
          });
    
  4. TableView.render() 方法中预置 ClientSideFilter

          this.$el.prepend(this.clientSideFilter.render().$el);
    

参见

Backgrid 扩展实际上非常广泛,无法在本食谱中完全考虑。因此,您可以查看官方 Backgrid 文档,网址为 backgridjs.com/

在 HTML5 canvas 上绘制

有时,我们可能希望将视图渲染到 HTML5 canvas 元素上,这可以提供更多的自由和灵活性。canvas 可以用于渲染图表,也可以用于创建在线游戏。

在这个例子中,我们将可视化 HTML5 画布上的模型集合。我们代码的输出将类似于以下截图:

在 HTML5 画布上绘制

准备工作

在本食谱中,我们将从第四章中“将视图拆分为子视图”的食谱中取一个例子,以改变InvoiceItemViewInvoiceItemListView

如何做到这一点...

按照以下步骤操作:

InvoiceItemView  var InvoiceItemView = Backbone.View.extend({

});
  1. InvoiceItemViewinitialize()方法中设置矩形框的边界。

      initialize: function() {
        // Set box size
        this.w = 100;
        this.h = 75;
    
        // Set random position
        this.x = Math.random() * (this.options.canvasW - this.w);
        this.y = Math.random() * (this.options.canvasH - this.h);
      }
    
  2. InvoiceItemViewrender()方法中绘制一个矩形框,并在ctx(画布上下文)上输出模型值。

        render: function() {
    
          // Get canvas context from parameters.
          ctx = this.options.ctx;
    
          // Draw transparent box
          ctx.fillStyle = '#FF9000';
          ctx.globalAlpha = 0.1;
          ctx.fillRect(this.x, this.y, this.w, this.h);
    
          // Stroke the box
          ctx.strokeStyle = '#FF9900';
          ctx.globalAlpha = 1;
          ctx.lineWidth = 2;
          ctx.strokeRect(this.x, this.y, this.w, this.h);
    
          // Output text in the box
          ctx.fillStyle = '#009966';
          ctx.font = 'bold 12px Arial';
          var textX = this.x + 4,
              textY = this.y + 4,
              textMaxW = this.w - 8,
              lineHeight = 12;
    
          ctx.fillText(
            this.model.get('description'),
            textX,textY + lineHeight, textMaxW
          );
          ctx.fillText(
            'Price: $' + this.model.get('price'),
            textX, textY + lineHeight*3,
            textMaxW
          );
          ctx.fillText(
            'Quantity: ' + this.model.get('quantity'),
             textX, textY + lineHeight*4, textMaxW
          );
          ctx.fillText(
            'Total: $' + this.model.calculateAmount(),
             textX, textY + lineHeight*5, textMaxW
          );
    
          return this;
        }
    
  3. 定义InvoiceItemListView,它创建一个空白的画布并触发模型视图的迭代渲染,同时传递ctx作为选项。

      var InvoiceItemListView = Backbone.View.extend({
    
        // Set a canvas as element tag name and define it's size.
        tagName: 'canvas',
        attributes: {
          width: 400,
          height: 200
        },
    
        // Render view.
        render: function() {
    
          // Get canvas context and it's size.
          var ctx = this.el.getContext("2d")
              canvasW = this.el.width,
              canvasH = this.el.height;
          // Clear canvas.
          ctx.clearRect(0, 0, canvasW, canvasH);
    
          // Iterate through models in collection and render them.
          this.collection.each(function(model) {
            new InvoiceItemView({
              model: model,
    
              // Pass canvas context and it's size.
              ctx: ctx,
              canvasW: canvasW,
              canvasH: canvasH
            }).render();
          }, this);
    
          return this;
        }
      });
    

它是如何工作的...

InvoiceItemListView将画布定义为主要的视图元素,并设置其边界。在render()方法中,我们通过调用getContext()方法获取ctx,即画布的上下文对象。上下文对象允许我们通过运行特殊的 HTML5 方法在画布上绘制。

通过将ctx和画布尺寸作为选项传递给子视图,我们允许它们用于输出到画布上的文本和形状。

参考以下内容

HTML 5 画布参考可以在www.w3schools.com/html/html5_canvas.asp找到。

第七章. REST 和存储

在本章中,我们将介绍以下菜谱:

  • 为后端设计 REST API

  • 使用 MongoLab 原型化 RESTful 后端

  • 与 RESTful 服务同步模型和集合

  • 使用 Backbone 构建 RESTful 前端

  • 使用轮询技术获取数据

  • 与本地存储一起工作

简介

本章重点介绍 Backbone.js 如何与 RESTful 后端同步模型和集合,或者将它们存储在 HTML5 本地存储中。

我们将学习如何设计后端的 REST API,这可以用几乎任何编程框架实现,例如 Symphony、Ruby on Rails、Django 或 Node.js。

在本章中,我们将使用 MongoLab (mongolab.com),它是 MongoDB 的云版本,具有 RESTful 接口。我们还将学习在前端应用程序尚未构建之前,如何使用工具调试 RESTful 服务。

最后,我们将使 Backbone 应用程序与 RESTful 服务通信,执行 REST 服务器支持的完整 CRUD 操作集。我们还将学习如何使用轮询技术动态更新应用程序中的集合数据。

我们还将讨论一个扩展,允许我们将数据保存在 HTML5 的本地存储中,而不是保存在远程服务器上。

为后端设计 REST API

表征状态转移REST)是设计网络应用程序的架构风格,这些应用程序相互通信。与 COBRA 或 SOAP 不同,REST 可以轻松地建立在纯 HTTP 之上。

REST 风格的架构由客户端和服务器组成。客户端调用 HTTP 请求方法(POSTGETPUTDELETE)来在资源上执行 CRUD(创建、读取、更新和删除)操作,资源可以是集合或单个元素。

在这个菜谱中,我们将设计用于计费应用程序的 REST 服务器 API。

如何操作...

按照以下步骤设计 RESTful 服务的 API:

  1. 定义客户端用于访问服务器上存储资源的基 REST URI;例如,它可以看起来像http://example.com/resources

  2. 定义 URI 以访问您的应用程序特定资源。这些 URI 应相对于基本 REST URI:

    • 发票集合: <rest-uri>/invoices

    • 发票: <rest-uri>/invoices/<invoice-id>

    • 买家集合: <rest-uri>/buyers

    • 买家: <rest-uri>/buyers/<buyer-id>

    • 卖家: <rest-uri>/seller

它是如何工作的...

访问资源的 URI 可以看起来像http://example.com/resources/items and data,这些数据通过 REST 传输,通常是 JSON 格式、XML 或任何其他有效的 Internet 媒体类型。

以下表格描述了在特定资源类型上执行 REST 操作时发生的情况:

资源 URI 集合:http://example.com/resources/items 元素:http://example.com/resources/items/1
POST 此请求在集合中创建一个新项目并返回新创建的项目或其 URI。 通常不使用。如果使用,它执行与集合资源 POST 查询相同的任务。
GET 此请求列出集合项目或它们的 URI。 此请求通过它们的 URI 检索集合项目。
PUT 此请求用另一个集合替换整个集合。 此请求替换集合项目或如果不存在则创建一个。
DELETE 此请求删除整个集合。 此请求从集合中删除项目。

参考 Roy Fieldings 的博士论文,这是关于 REST 的第一份也是最为完整的工作,以了解更多关于 REST 的信息,请访问 www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm

使用 MongoLab 构建 RESTful 后端

假设我们想要创建一个将与 RESTful 服务通信的 Backbone 应用程序。我们应该从创建后端还是前端开始?这个问题听起来像是一个困境,但答案非常简单。

最简单的方法是使用具有 REST 风格界面的简单数据库创建原型,这样我们就可以在将来快速将其替换为我们自己的后端。

有一个名为 MongoLab 的好工具(mongolab.com),它是具有 REST 风格界面的 MongoDB 的云版本。MongoDB 是一个与类似 JSON 的数据一起工作的 NoSQL 文档型数据库。MongoLab 将不需要我们在后端编写任何一行代码,因此它非常适合我们作为原型工具。

要测试和调试 MongoLab 后端,我们将使用高级 REST 客户端,这是一个 Chrome 浏览器的扩展。它允许对 RESTful 服务执行 HTTP 查询并可视化 JSON 数据。

准备工作...

按以下步骤为这个食谱做好准备:

  1. 在 MongoLab 网站上创建账户 (mongolab.com),或者如果您已有账户则登录。

  2. 使用 URL chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo 在您的浏览器上安装高级 REST 客户端。如果您使用 Firefox 或 Safari,您可以轻松找到用于此目的的类似扩展。

如何做...

按以下步骤创建 MongoLab 数据库并填充数据:

  1. 访问 mongolab.com/newdb 并在您的 MongoLab 账户中创建一个名为 billing-app 的新数据库。

  2. 访问 https://mongolab.com/user?username=<username> 并获取一个 API 密钥,您可以使用它进行身份验证。

  3. 要检查您账户中的数据库,使用高级 REST 客户端在 URI 上执行一个GET请求,例如 https://api.mongolab.com/api/1/databases?apiKey=<your-api-key>如何做...

    结果将类似于以下截图:

    如何操作...

  4. 要获取数据库中集合的列表,请在 URI https://api.mongolab.com/api/1/databases/billing-app/collections?apiKey=<your-api-key> 上执行 GET 请求。结果将类似于以下截图:如何操作...

  5. 要创建一个新的集合,请使用此 URI 上的 POST 查询发送定义在 JSON 格式的集合项:https://api.mongolab.com/api/1/databases/billing-app/collections/invoices?apiKey=<your-api-key>。请确保设置了 application/json Content-Type 标头。如何操作...

    此操作的输出结果将类似于以下截图:

    如何操作...

    通过再次对同一资源执行 GET 请求,我们将以 JSON 格式返回插入的项目及其 ID。

    如何操作...

  6. 要更新集合中现有的项目,我们需要在 URI https://api.mongolab.com/api/1/databases/billing-app/collections/invoices/<invoice-id>?apiKey=<your-api-key> 上对指定 ID 的集合项目资源执行 PUT 请求。在 PUT 请求中,我们应该以 JSON 格式传递更新的模型。结果也以 JSON 格式返回,如下所示截图:如何操作...

工作原理...

MogoLab 将 HTTP 请求转换为 MongoDB 查询,执行查询并返回 MongoDB 扩展 JSON 格式的结果。

参见

将模型和集合与 RESTful 服务同步

在本食谱中,我们将学习如何将模型和集合与 RESTful 服务同步。就像我们在所有其他食谱中所做的那样,我们将使用 MongoLab 作为 RESTful 服务。

如何操作...

按照以下步骤熟悉 Backbone.js 中的 REST:

  1. 创建一个配置对象,该对象存储服务器 URL 和 MongoLab 的身份验证密钥。

    var appConfig = {
      baseURL: 'https://api.mongolab.com/api/1/databases/billing-app/collections/',
    
      addURL: '?apiKey=kNCrqJUqB4n1S_qW7wnXH43NH9XKjdIL'
    }
    
  2. 定义一个 url() 方法,该方法返回用于执行 REST 请求的资源 URL。如果模型已经与 RESTful 服务同步,则此类 URL 应包含模型 ID。此外,此类 URL 应包含 MongoLab 的身份验证密钥。

      var InvoiceModel = Backbone.Model.extend({
        url: function() {
          if (_.isUndefined(this.id)) {
            return appConfig.baseURL +
              'invoices' + appConfig.addURL;
          }
          else {
            return appConfig.baseURL + 'invoices/' +
              encodeURIComponent(this.id) + appConfig.addURL;
          }
        },
      });
    

    另一种方法是定义 urlRoot 属性,尽管它不允许向 URL 添加参数。

      var InvoiceModel = Backbone.Model.extend({
        urlRoot: appConfig.baseURL;
      });
    
  3. 定义一个新的集合和一个 url 属性或 url() 方法,该方法应返回集合资源的 URL。

      var InvoiceCollection = Backbone.Collection.extend({
        model: InvoiceModel,
        url: appConfig.baseURL +'invoices' + appConfig.addURL
      });
    
  4. 要从服务器加载数据到集合中,请使用fetch()方法。您可以传递successerror回调作为参数。如果同步成功或失败,任一回调都将异步调用。

      var collection = new InvoiceCollection();
    
      collection.fetch({
        success: function(collection, response, options) {
          $('body').html(
            new View({ collection: collection}).render().el
          );
        },
        error: function(collection, response, options) {
          alert('error!');
        }
      });
    

    fetch()方法运行时,会触发读取事件。成功时,还会触发同步事件。

  5. 您也可以使用fetch()方法加载服务器上存在的特定模型,它与集合的工作方式类似。

      var model = new InvoieModel();
    
      model.id = '5176396ce4b0c62bf3e53d79';
      model.fetch(
        success: function(model, response, options) {
          // success
        },
        error: function(collection, response, options) {
          // error
        }
      );
    
  6. 要与 RESTful 服务同步模型,请使用save()方法。

      model.save();
    

    要更新特定属性,请在第一个参数中传递更改的属性哈希,在第二个参数中传递{patch: true}

      model.save({ status: 'complete'}, {patch: true});
    

    默认情况下,save()方法以异步方式工作,因此您需要在successerror回调中处理结果。但是,如果您需要以同步方式运行save()方法,请在第一个参数中传递null,在第二个参数中传递{wait: true}

      model.save(null, {wait: true});
    
  7. 您可以使用create()方法在集合内创建一个新的模型。在这种情况下,Backbone.js 会自动调用save()方法,并将新的模块推送到服务器。

      var model = collection.create(
        { referenceNumber: '123', status: complete },
        { wait: true }
      );
    
  8. 您可以使用destroy()方法销毁一个模型,这将从集合和服务器中删除模型。

      model.save(null, {
        success: function(model, response, options) {
          // success
        },
        wait: true
     });
    

它是如何工作的...

fetch()save()destroy()方法调用sync()方法以执行 HTTP 查询,同步模型和集合与 RESTful 服务。sync()方法接受以下参数:

  • method:它可以是 create、update、patch、delete 或 read。

  • model:它是一个用于同步的模型或集合。

  • options:这是$.ajax变量接受的选项。

如果需要覆盖同步或在没有 REST 支持的情况下使用存储,您可以覆盖sync()方法。

还有更多...

MongoLab 以 MongoDB 扩展 JSON 格式返回数据,而 Backbone.js 默认不支持这种格式。在本教程中,我们将解决这个问题,并找到在 Backbone 应用程序中直接处理 MongoDB 扩展 JSON 的好方法。

处理 MongoDB 扩展 JSON

MongoLab (mongolab.com) 是一个 RESTful 服务,它将 HTTP 请求转换为 MongoDB 查询,并以 MongoDB 扩展 JSON 格式返回结果,如下代码片段所示:

{ 
    "_id": { 
        "$oid": "516eb001e4b0799160e0e864" 
    }, 
}

为了得到适当的结果,我们需要处理这样的 ID。背后的想法是覆盖parse()方法,该方法处理 JSON 并从中初始化模型属性。我们将在这里替换 ID 的格式:

Backbone.Model.prototype.parse = function(resp, options) {
  if (_.isObject(resp._id)) {
    resp[this.idAttribute] = resp._id.$oid;
      delete resp._id;
  }
  return resp;
},

此外,当sync()方法运行时,我们需要确保数据以 MongoDB 扩展 JSON 格式导出。在其他所有情况下,应导出为常规 JSON 格式。数据导出仅在toJSON()方法中执行,因此我们可以在sync()方法执行期间替换toJSON()方法。

// Convert regular JSON into MongoDB extended one.
  Backbone.Model.prototype.toExtendedJSON= function() {
    var attrs = this.attributes;

    var attrs = _.omit(attrs, this.idAttribute);
    if (!_.isUndefined(this[this.idAttribute])) {
      attrs._id = { $oid: this[this.idAttribute] };
    }

    return attrs;
  },

// Substute toJSON method when performing synchronization.
  Backbone.Model.prototype.sync = function() {
    var toJSON = this.toJSON;
    this.toJSON = this.toExtendedJSON;

    var ret = Backbone.sync.apply(this, arguments);

    this.toJSON = toJSON;

    return ret;
  }

参见

使用 Backbone 构建 RESTful 前端

在这个配方中,我们将编写一个前端应用程序,它将作为 RESTful 服务的客户端。对于后端,我们将使用 MongoLab 服务,这是一个具有 REST 接口的 MongoDB 云版本。

我们将使用布局管理器扩展来以整洁的格式输出我们的视图。为了构建我们的应用程序,我们将从第六章的示例应用程序中获取,模板和 UX 糖,并进行修改,使其支持通过 REST 进行数据同步,并看起来如下截图所示:

使用 Backbone 构建 RESTful 前端

在左侧窗格中,我们可以看到一系列发票标题,在右侧窗格中,我们可以看到发票详情。默认情况下,这些详情为第一个发票显示,直到用户点击左侧窗格中的链接。

如果用户点击编辑按钮,将显示以下表单:

使用 Backbone 构建 RESTful 前端

当用户点击保存按钮时,模型被更新,并且它的 JSON 通过 REST 发送到服务器,并且左侧窗格中的列表也被更新。

如果用户点击删除按钮,将出现一个删除确认表单,如下截图所示:

使用 Backbone 构建 RESTful 前端

如果用户确认删除,模型将被销毁并通过 REST 从服务器中移除。

用户还可以通过点击页面顶部的添加发票链接来创建一个新的发票。然后,会显示一个添加发票表单,它与编辑表单相同,但没有显示任何数据。

使用 Backbone 构建 RESTful 前端

准备中...

按照以下步骤为此配方做好准备:

  1. 确保已安装布局管理器扩展。该扩展的用法和安装描述在第六章的使用 LayoutManager 组装布局配方中,模板和 UX 糖

  2. 覆盖Backbone.Model以支持 MongoLab 中使用的 MongoDB 扩展 JSON 格式。

      // Convert MongoDB Extended JSON into regular JSON.
      Backbone.Model.prototype.parse = function(resp, options) {
        if (_.isObject(resp._id)) {
          resp[this.idAttribute] = resp._id.$oid;
          delete resp._id;
        }
    
        return resp;
      },
    
      // Convert regular JSON into MongoDB extended one.
      Backbone.Model.prototype.toExtendedJSON= function() {
        var attrs = this.attributes;
    
        var attrs = _.omit(attrs, this.idAttribute);
        if (!_.isUndefined(this[this.idAttribute])) {
          attrs._id = { $oid: this[this.idAttribute] };
        }
    
        return attrs;
      },
    
      // Substute toJSON method when performing synchronization.
      Backbone.Model.prototype.sync = function() {
        var toJSON = this.toJSON;
        this.toJSON = this.toExtendedJSON;
    
        var ret = Backbone.sync.apply(this, arguments);
    
        this.toJSON = toJSON;
    
        return ret;
      }
    This allows Backbone to work correctly with data IDs in a format like this:
    { 
        "_id": { 
            "$oid": "516eb001e4b0799160e0e864" 
        }, 
    }
    
    

    这允许 Backbone 正确地与如下格式的数据 ID 一起工作:

    { 
        "_id": { 
            "$oid": "516eb001e4b0799160e0e864" 
        }, 
    }
    

如何做到这一点...

按照以下步骤创建一个使用 Backbone 的 RESTful 应用程序:

  1. 创建一个配置对象,我们将存储服务器 URL 和认证密钥。

    var appConfig = {
      baseURL: 'https://api.mongolab.com/api/1/databases/billing-app/collections/',
    
      addURL: '?apiKey=kNCrqJUqB4n1S_qW7wnXH43NH9XKjdIL'
    }
    
  2. 定义 InvoiceModel 并设置 url() 方法,该方法将返回执行 REST 请求的模型资源 URL。

      var InvoiceModel = Backbone.Model.extend({
        url: function() {
          if (_.isUndefined(this.id)) {
            return appConfig.baseURL +
              'invoices' + appConfig.addURL;
          }
          else {
            return appConfig.baseURL + 'invoices/' +
              encodeURIComponent(this.id) + appConfig.addURL;
          }
        },
      });
    
  3. 定义 InvoiceCollection 和模型的 url() 方法。

      var InvoiceCollection = Backbone.Collection.extend({
        model: InvoiceModel,
        url: function() {
          return appConfig.baseURL +
            'invoices' + appConfig.addURL;
        },
      });
    
  4. 定义一个路由并添加 initialize() 方法,该方法创建一个空集合和布局对象并渲染布局。

      // Define router object.
      var Workspace = Backbone.Router.extend({
        initialize: function() {
    
          //  Create collection.
          this.collection = new InvoiceCollection();
    
          // Create new layout.
          this.layout = new Backbone.Layout({
              // Attach the layout to the main container.
              el: 'body',
    
              // Set template selector.
              template: '#layout',
    
              // Declaratively bind a nested View to the layout.
              views: {
                '#first-pane': new InvoiceListPane({
                  collection: this.collection
                }),
              },
          });
    
          // Render whole layout for the first time.
          this.layout.render();
        },
      });
    
  5. 将布局模板添加到 index.html 中。

      <script class="template" type="template" id="layout">
        <h1>Billing application</h1>
        <div id="links-pane">
          <a href="#invoice/add">+ Add an invoice</a>
        </div>
        <div id="first-pane"></div>
        <div id="second-pane"></div>
      </script>
    
  6. routes 和回调添加到路由对象中。每个回调都调用 switchPane() 方法,该方法切换布局的右侧面板。

        routes: {
          '': 'invoicePage',
          'invoice': 'invoicePage',
          'invoice/add': 'addInvoicePage',
          'invoice/:id/edit': 'editInvoicePage',
          'invoice/:id/delete': 'deleteInvoicePage',
          'invoice/:id': 'invoicePage',
        },
    
        // Page callbacks.
        invoicePage: function(id) {
          this.switchPane('InvoicePane', id);
        },
        addInvoicePage: function() {
          this.switchPane('EditInvoicePane', null);
        },
        editInvoicePage: function(id) {
          this.switchPane('EditInvoicePane', id);
        },
        deleteInvoicePage: function(id) {
          this.switchPane('DeleteInvoicePane', id);
        },
    
  7. switchPane() 方法添加到路由中,该方法从 RESTful 服务获取集合并切换右侧面板。

        switchPane: function(pane_name, id) {
    
          // Define panes array.
          // This will allow use to create new object from string. 
          var panes = {
            InvoicePane: InvoicePane,
            EditInvoicePane: EditInvoicePane,
            DeleteInvoicePane: DeleteInvoicePane 
          };
    
          // Update collection.
          this.collection.fetch({ success: function(collection) {
    
            // Get model by id or take first model
            // from collection.
            var model = _.isUndefined(id) ?
              collection.at(0) : collection.get(id);
    
            // Create new pane and pass model and collection.
            pane = new panes[pane_name] ({
              model: model, collection: collection
            });
    
            // Render pane.
            pane.render();
    
            // Switch views.
            window.workspace.layout.removeView('#second-pane');
            window.workspace.layout.setView('#second-pane', pane);
    
          }, reset: true });
        },
    
  8. 定义发票列表面板。

      var InvoiceListPane = Backbone.Layout.extend({
    
        // Returns selector for template.
        template: '#invoice-list-pane',
    
        // Set selector for template.
        serialize: function() {
          return { invoices: _.chain(this.collection.models) };
        },
    
        // Bind callbacks to collection event.
        initialize: function() {
          this.listenTo(this.collection, 'reset', this.render);
        }
      });
    

    index.html 中为它添加一个模板。

     <script class="template" type="template" id="invoice-list-pane">
        <h3>Invoices:</h3>
        <ul>
          <% invoices.each(function(invoice) { %>
            <li>
              <a href="#invoice/<%= invoice.id %>">
                <%= invoice.get('referenceNumber') %>
              </a>
            </li>
          <% }); %>
        </ul>
      </script>
    
  9. 定义查看发票面板。

      var InvoicePane = Backbone.Layout.extend({
    
        // Set selector for template.
        template: '#invoice-pane',
    
        // Returns data for template.
        serialize: function() {
          return { invoice: this.model };
        },
    
        // Bind callbacks to model events.
        initialize: function() {
          this.listenTo(this.model, 'change', this.render);
        }
      });
    

    index.html 中为它添加一个模板。

      <script class="template" type="template" id="invoice-pane">
        <h3>Invoice details:</h3>
        Reference Number:
          <%= invoice.get('referenceNumber') %><br>
        Date: <%= invoice.get('date') %><br>
        Status: <%= invoice.get('status') %><br>
        <br>
        <a href="#invoice/<%= invoice.id %>/edit" class="btn">
          Edit
        </a>
        <a href="#invoice/<%= invoice.id %>/delete" class="btn">
          Delete
        </a>
      </script>
    
  10. 定义编辑发票面板。

      var EditInvoicePane = Backbone.Layout.extend({
    
        // Set selector for template.
        template: '#edit-invoice-pane',
    
        // Returns data for template.
        serialize: function() {
    
          // Create new model if no model is given.
          return {
            invoice:
              _.isEmpty(this.model) ?
                new InvoiceModel() : this.model
          };
        },
    
        // Bind callbacks form events.
        events: {
          "click .submit": "save"
        },
    
        // Save model
        save: function() {
          var data = {
            referenceNumber: 
              this.$el.find('.referenceNumber').val(),
            date: this.$el.find('.date').val(),
            status: this.$el.find('.status').val(),
          };
    
          var success = function(model, response, options) {
            window.workspace.navigate('#invoice/' + model.id, {
              trigger: true
            });
          };
    
          // Run appropriate method.
          if (_.isEmpty(this.model)) {
            this.collection.create(data, {success: success});
          }
          else {
            this.model.save(data, { success: success});
          }
        }
      });
    

    index.html 中为它添加一个模板。

      <script class="template" type="template"
          id="edit-invoice-pane">
        <h3>Enter invoice details:</h3>
        Reference Number:<br>
        <input class="referenceNumber" type="text"
              value="<%= invoice.get('referenceNumber') %>"><br>
        Date:<br>
        <input class="date" type="text"
              value="<%= invoice.get('date') %>"><br>
        Status:<br>
        <input class="status" type="text"
              value="<%= invoice.get('status') %>"><br>
        <button class="btn btn-primary submit">Save</button>
      </script>
    
  11. 定义删除发票面板。

      var DeleteInvoicePane = Backbone.Layout.extend({
    
        // Set selector for template.
        template: '#delete-invoice-pane',
    
        // Returns data for template.
        serialize: function() {
          return { invoice: this.model };
        },
    
        // Bind callbacks to form events.
        events: {
          "click .submit": "delete"
        },
    
        // Delete model.
        delete: function() {
          this.model.destroy({
            success: function(model, response) {
              window.workspace.navigate('#invoice', {
                trigger: true 
            });
          }});
        }
      });
    

    index.html 中为它添加一个模板。

    <script class="template" type="template"
        id="delete-invoice-pane">
      <h3>Are you sure you want to delete invoice
      <%= invoice.get('referenceNumber') %>?</h3>
      <button class="btn submit btn-primary">Yes</button>
      <a href="#invoice/<%= invoice.id %>" class="btn">No</a>
    </script>
    
  12. 创建一个路由实例并启动应用程序。

      // Create the workspace.
      window.workspace = new Workspace();
    
      // Start the application.
      Backbone.history.start();
    

它是如何工作的...

要从 RESTful 服务加载集合,我们需要调用 fetch() 方法,它以异步方式运行,就像常规 AJAX 调用一样。如果我们需要在数据成功获取后运行任何代码,我们需要在第二个参数中传递一个以 success 为键的回调函数。如果我们需要在出错时执行回退行为,我们应该在函数参数中以 error 为键传递回调函数。

collection.fetch({
  success: function(collection, response, options){
    // success behavior
  },

  error: function(collection, response, options){
    // fall back behavior
  }
 })

要通过 REST 与远程服务器同步模型,我们使用 save() 方法。要从远程服务器完全删除模型,我们使用 destroy() 方法。这两个方法都接受 successerror 回调函数。

参见

  • 在第六章 使用 LayoutManager 组装布局 的菜谱中,模板和 UX 糖分,请参阅 Chapter 6。

  • 参考官方文档以获取有关我们在本菜谱中使用的 Backbone 方法的更多信息,请参阅 backbonejs.org/

使用轮询技术获取数据

在之前的菜谱中,我们每次路由处理 URL 变化时都会将数据获取到一个集合中。我们可能会想知道如果其他人更新了相同存储中的数据会发生什么?我们能否立即看到更新?

你可能已经看到 Facebook 或 Twitter 如何实时更新新闻源,你可能想在应用程序中实现类似的行为。通常,这可以通过轮询技术完成,我们将在本菜谱中学习这项技术。

我们将创建一个网络应用程序,它将使用轮询技术动态更新集合视图。

准备工作...

覆盖 Backbone.ModelBackbone.Collection 以支持 MongoDB 扩展 JSON 格式,该格式用于 MongoLab。

    // Convert MongoDB Extended JSON into regular JSON.
  Backbone.Model.prototype.parse = function(resp, options) {
    if (_.isObject(resp._id)) {
      resp[this.idAttribute] = resp._id.$oid;
      delete resp._id;
    }

    return resp;
  },

  // Convert regular JSON into MongoDB extended one.
  Backbone.Model.prototype.toExtendedJSON= function() {
    var attrs = this.attributes;

    var attrs = _.omit(attrs, this.idAttribute);
    if (!_.isUndefined(this[this.idAttribute])) {
      attrs._id = { $oid: this[this.idAttribute] };
    }

    return attrs;
  },

  // Substute toJSON method when performing synchronization.
  Backbone.Model.prototype.sync = function() {
    var toJSON = this.toJSON;
    this.toJSON = this.toExtendedJSON;

    var ret = Backbone.sync.apply(this, arguments);

    this.toJSON = toJSON;

    return ret;
  }

如何做...

按照以下步骤实现轮询技术:

  1. 创建一个新的轮询集合,它递归地获取数据并提供启动或停止轮询的方法。

      var PollingCollection = Backbone.Collection.extend({
        polling: false,
    
        // Set default interval in seconds.
        interval: 1,
    
        // Make all object methods to work from its own context.
        initialize: function() {
          _.bindAll(this);
        },
    
        // Starts polling.
        startPolling: function(interval) {
          this.polling = true;
    
          if (interval) {
            this.interval = interval;
          }
          this.executePolling();
        },
    
        // Stops polling.
        stopPolling: function() {
          this.polling = false;
        },
    
        // Executes polling.
        executePolling: function() {
          this.fetch({
            success: this.onFetch, error: this.onFetch
          });
        },
    
        // Runs recursion.
        onFetch: function() {
          setTimeout(this.executePolling, 1000 * this.interval)
        },
      });
    
  2. 定义配置对象。

      var appConfig = {
        baseURL:'https://api.mongolab.com/api/1/databases/billing-app/collections/',
        addURL: '?apiKey=kNCrqJUqB4n1S_qW7wnXH43NH9XKjdIL'
      }
    Define a model and a collection.
      var InvoiceModel = Backbone.Model.extend({
        url: function() {
          if (_.isUndefined(this.id)) {
            return appConfig.baseURL + 'invoices' +
              appConfig.addURL;
          }
          else {
            return appConfig.baseURL + 'invoices/' +
              encodeURIComponent(this.id) + appConfig.addURL;
          }
        },
      });
    
      var InvoiceCollection = PollingCollection.extend({
        model: InvoiceModel,
        url: function() {
          return appConfig.baseURL + 'invoices' +
            appConfig.addURL;
        },
      });
    
  3. 定义发票视图并将回调绑定到模型事件。

      var InvoiceView = Backbone.View.extend({
    
        // Define element tag name.
        tagName: 'li',
    
        // Define template.
        template: _.template('Invoice #<%= referenceNumber %>.'),
    
        // Render view.
        render: function() {
          $(this.el).html(this.template(this.model.toJSON()));
    
          return this;
        },
    
        // Bind callback to the model events.
        initialize: function() {
          this.listenTo(this.model, 'change', this.render, this);
          this.listenTo(this.model, 'destroy', this.remove, this);
        }
      });
    
  4. 定义一个发票列表视图并将回调绑定到集合事件。

      var InvoiceListView = Backbone.View.extend({
    
        // Define element tag name.
        tagName: 'ul',
    
        // Render view.
        render: function() {
          $(this.el).empty();
    
          // Append table  with a row.
          _.each(this.collection.models, function(model, key) {
            this.append(model);
          }, this);
    
          return this;
        },
    
        // Add invoice item row to the table.
        append: function(model) {
          $(this.el).append(
             new InvoiceView({ model: model }).render().el
          );
        },
    
        // Remove model from collection.
        remove: function(model) {
          model.trigger('destroy');
        },
    
        // Bind callbacks to the collection events.
        initialize: function() {
         this.listenTo(this.collection,'reset',this.render,this);     
         this.listenTo(this.collection,'add',this.appen,this);
         this.listenTo(this.collection,'remove',this.remove,this);
        },
      });
    
  5. 创建一个集合并渲染相应的视图。

        collection = new InvoiceCollection();
    
        $('body').append('<h3>Invoices</h3>')
        $('body').append(new InvoiceListView({
          collection: collection,
        }).render().el);
    
  6. 开始轮询。

        collection.startPolling();
    

它是如何工作的...

轮询背后的想法是定期从服务器获取数据。然而,我们无法在简单的循环中这样做,因为获取操作是异步的,我们需要确保 AJAX 请求不会相互重叠。因此,我们需要确保之前的获取操作成功完成后再执行下一个。

在这个配方中,我们从 Backbone.Collection 继承了一个集合,并添加了我们需要实现轮询的新方法和属性。在 executePolling() 方法中,我们执行 fetch() 方法,并将 onFetch() 方法作为成功回调传递。在 onFetch() 方法中,我们使用超时调用 executePolling() 方法。

与本地存储一起工作

有时候,我们需要在浏览器存储上而不是在远程服务器上存储数据。借助名为 localStorage Adapter 的 Backbone 扩展,这相当容易做到,该扩展覆盖了 Backbone.sync() 方法的行为以同步数据到 HTML5 local storage。在这个配方中,我们将学习如何使用这个扩展。

准备工作...

您可以从其 GitHub 页面 github.com/jeromegn/Backbone.localStorage 下载 Backbone localStorage 适配器。要将此扩展包含到您的项目中,将 backbone.localStorage.js 文件保存到 lib 文件夹,并在 index.html 中包含对其的引用。

在 第一章 的 使用插件扩展应用程序 配方中详细描述了将 Backbone 扩展包含到你的项目中,理解 Backbone

如何做...

扩展集合并设置 localStorage 键如下:

  var InvoiceCollection = Backbone.Collection.extend({
    model: InvoiceModel,

    // Use local storage.
    localStorage:
      new Backbone.LocalStorage("InvoiceCollection")
  });

在这里,我们创建了一个 Backbone.LocalStorage 的实例,并将存储名称作为构造函数参数传递。存储名称应该在您的应用程序中是唯一的。

它是如何工作的...

Backbone 的 localStorage 适配器覆盖了 Backbone.sync() 方法,当集合启用时,它会执行代码以同步数据到 HTML5 localStorage。

小贴士

创建新模型时请注意

当使用 localStorage 适配器时,你应该避免的唯一事情是创建新模型并通过调用模型的 save() 方法来保存它们。相反,你应该调用集合对象的 create() 方法,因为否则模型尚未与集合关联,localStorage 适配器不知道应该使用哪个本地存储。

在模型与集合关联之后,save() 方法工作得相当好。

参见

第八章. 特殊技术

在本章中,我们将涵盖:

  • 使用 Backbone 对象与混合器

  • 使用 Grunt 创建 Backbone.js 扩展

  • 使用 QUnit 为 Backbone 扩展编写测试

  • 使用 jQuery Mockjax 在异步测试中模拟 RESTful 服务

  • 使用 jQuery Mobile 开发移动应用程序

  • 使用 PhoneGap 构建 iOS/Android 应用程序

  • 使用 Require.js 组织项目结构

  • 确保与搜索引擎的兼容性

  • 在 Backbone 应用程序中避免内存泄漏

简介

本章旨在展示如何在 Backbone 开发过程中解决可能遇到的最具挑战性的问题。

我们将学习如何混合现有的 Backbone 对象以添加任何额外的功能。我们将使用 Grunt 创建一个 Backbone 扩展。

我们还将为我们的扩展创建测试,这将帮助我们确保在扩展中添加任何新功能时它都能按预期工作。

然后,我们将集成 jQuery MobileBackbone.js,并使用 PhoneGap 为 iOS 和 Android 等移动平台构建原生应用程序。

我们将学习如何处理 Require.js,如何使用它来组织项目结构,以及如何在我们的移动应用程序中使用它。

最后,我们将了解如何让搜索引擎索引使用 Backbone.js 创建的 AJAX 应用程序。

本章假设你正在使用类 Unix 的 shell,并且已经在你的系统中安装了 Node.js 和 npm(Node 包模块)。

使用 Backbone 对象与混合器

尽管有数百个 Backbone 扩展提供了额外的功能,但一个项目可能需要使用一些自定义功能扩展 Backbone 对象。

有几种方法可以做到这一点。通常,你可以使用以下代码扩展 Backbone 对象:

  Backbone.ExtraModel = Backbone.Model.extend({
    // Add new method.
    hello: function() {

    },

    // Override existing method.
    toJSON: function() {

    }
  });

它工作得很好,除非你遇到以下情况之一:

  • 你想一次性修改 Backbone.Model 对象及其所有子对象

  • 你有不同的扩展,它们共同修改同一个对象,因此你需要避免冲突

解决方案是使用混合器,我们将在本食谱的范围内处理它。

如何做到这一点...

执行以下步骤以定义 mixin 并将其添加到 Backbone.Model

  1. 按照以下方式定义 mixin 对象:

      var mixin = {
        // Add new method.
        hello: function() {
    
        },
    
        // Override existing method.
        toJSON: function() {
    
        }
      }
    
  2. 按照以下代码将 mixin 添加到现有对象中:

      Backbone.NewModel = Backbone.Model.extend(mixin);
    
  3. 保存 mixin 以便在需要时将其混合到其他模型对象中。

      Backbone.NewModel.mixin = mixin;
    
  4. 另一种方法是应用混合器到 Backbone.Model.prototype。这将使所有 Backbone.Model 子对象都具有这样的混合器。

      _.extend(Backbone.Model.prototype, mixin);
    
  5. 如果需要定义更多功能,你可以将它们定义在不同的混合器中,并以类似的方式扩展 Backbone 对象:

      _.extend(Backbone.Model.prototype, mixin2);
    

它是如何工作的...

为了创建一个新的模型对象,我们使用了祖先模型提供的 extend() 方法。为了一次性扩展所有 Backbone 模型,我们使用 Undercore.jsextend() 方法在 Backbone.Model 的原型上执行混合操作。

参见

使用 Grunt 创建 Backbone.js 扩展

对于开发者来说,创建一个将被全世界共享或甚至在未来项目中重用的 Backbone 扩展可能非常重要。在这个食谱中,我们将学习如何使用 Grunt 创建我们自己的扩展,并将其上传到 GitHub

Grunt 是一个 JavaScript 任务运行器,允许自动化不同的任务,如压缩、编译、单元测试和代码检查。这些重复性任务在 Gruntfile.js 文件中定义,并从控制台触发。Grunt 有许多不同的包,作为 npm 扩展可用。我们将使用其中之一,名为 grunt-init,从模板中构建 Backbone 扩展。

我们扩展将提供与 MongoDB 的兼容性。在上一章中,我们使用了 MongoLab (mongolab.com),这是一个具有 RESTful 接口的 MongoDB。MongoLab 提供的数据是 MongoDB 扩展 JSON,这是 Backbone 默认不支持的。以下代码是资源 ID 在 MongoDB 扩展 JSON 中的表示示例:

{
  "$oid": "<id>"
}

默认情况下,Backbone.js 文件不处理此类 ID,但我们的扩展将允许我们这样做。

准备工作...

执行以下步骤以准备此食谱:

  1. 确保已安装 Node.js 和 npm。

  2. 安装 grunt-init,它允许从模板生成项目。

    npm install -g grunt-init
    
  3. 安装 grunt-cli,它允许从命令行运行 grunt 命令。

    grunt-init-backbone-plugin npm install -g grunt-cli
    
  4. 下载 grunt-init-backbone-plugin 并将其放置在您的本地 grunt-init 目录中。

    git clone --recursive https://github.com/dealancer/grunt-init-backbone-plugin.git ~/.grunt-init/backbone-plugin
    
  5. github.com 上创建公共仓库,我们将上传我们的扩展。

如何做...

执行以下步骤以使用 Grunt 创建 Backbone 扩展:

  1. 创建一个目录,该目录将包含我们扩展的源代码。此目录应命名为 backbone-mongodb

     $ mkdir backbone-mongodb
     $ cd backbone-mongodb
    
    
  2. 从 Grunt 模板构建一个扩展项目。运行下一个命令并遵循 Grunt 提出的步骤。

     $ grunt-init backbone-plugin
    
    
  3. 使用以下扩展代码更新 backbone-mongodb.js 文件:

    // backbone-mongodb 0.1.0
    //
    // (c) 2013 Vadim Mirgorod
    // Licensed under the MIT license.
    
    (function(Backbone) {
    
      // Define mixing that we will use in our extension.
      var mixin = {
    
        // Convert MongoDB Extended JSON into regular one.
        parse: function(resp, options) {
          if (_.isObject(resp._id))  {
            resp[this.idAttribute] = resp._id.$oid;
            delete resp._id;
          }
    
          return resp;
        },
    
        // Convert regular JSON into MongoDB extended one.
        toExtendedJSON: function() {
          var attrs = this.attributes;
    
          var attrs = _.omit(attrs, this.idAttribute);
          if (!_.isUndefined(this[this.idAttribute]))  {
            attrs._id = { $oid: this[this.idAttribute] };
          }
    
          return attrs;
        },
    
        // Substitute toJSON method when performing synchronization.
        sync: function() {
          var toJSON = this.toJSON;
          this.toJSON = this.toExtendedJSON;
    
          var ret = Backbone.sync.apply(this, arguments);
    
          this.toJSON = toJSON;
    
          return ret;
        }
      }
    
      // Create new MongoModel object.
      Backbone.MongoModel = Backbone.Model.extend(mixin);
    
      // Provide mixin to extend Backbone.Model.
      Backbone.MongoModel.mixin = mixin;
    
      // Another way to perform mixin.
      //_.extend(Backbone.Model.prototype, mixin);
    
    }).call(this, Backbone);
    
  4. 通过访问 github.com/new 链接并填写出现的表单来创建 GitHub 项目。如何做...

  5. 初始化仓库并将代码推送到 GitHub 项目。

     $ git init
     $ git remote add origin https://github.com/dealancer/backbone-mongo.git 
     $ git add *
     $ git add .gitignore
     $ git commit -m "initial commit"
     $ git push -u origin master
    
    

它是如何工作的...

当我们使用 backbone-plugin 参数运行 grunt-init 命令时,它将从 backbone-plugin 模板构建一个新的项目,该模板我们已下载并保存在 ~/.grunt-init/backbone-plugin 目录中。

新生成的项目结构如下:

  • node_modules/:此选项为我们提供应用程序的 Node.js 模块

    • grunt/

    • grint-contrib-qunit/

  • test/:此选项对我们的应用程序进行测试

    • index.html

    • mongodb.js

  • vendor/: 此选项列出应用程序中使用的库

    • backbone/
  • backbone-mongodb.js: 这是我们的应用程序的主要文件

  • Gruntfile.js: 这是 Grunt 文件

  • LICENSE-MIT

  • README.md

  • package.json: 这是 Node.js 模块文件

参见

使用 QUnit 为 Backbone 扩展编写测试

如果你正在处理一个复杂的项目或 Backbone 扩展,你需要确保新的提交不会破坏任何现有功能。这就是为什么许多开发者选择在编写新代码之前或之后创建测试。

对于 JavaScript 应用程序,有大量不同的测试工具可以与 Backbone 完美集成。在本食谱中,我们将学习一个名为 QUnit 的工具。

当我们使用 Grunt 从模板构建项目时,QUnit 被包含在项目中,并创建了test/mongodb.js文件。让我们向之前食谱中做的扩展添加一个简单的测试。

如何操作...

执行以下步骤以测试应用程序:

  1. 编辑test/mongodb.js文件,并添加一些基本模型和集合到扩展中,如下面的代码所述:

      var Book = Backbone.MongoModel.extend({
        urlRoot: '/books'
      });
    
      var Library = Backbone.Collection.extend({
        url: '/books',
        model: Book
      });
    
  2. 添加一些我们将使用的变量,如下面的代码所示:

      var library;
    
      var attrs = {
        id: 5,
        title: "The Tempest",
        author: "Bill Shakespeare",
      };
    
  3. 添加setup()teardown()方法,这些方法将在每个测试前后运行,如下面的代码所示:

      module('Backbone.Mongodb', _.extend(new Environment, {
    
        setup : function() {
    
          // Create new library.
          library = new Library();
    
          // Set init values.
          library.create(attrs, {wait: false});
        },
    
        teardown: function() {
    
        },
      }));
    
  4. 通过以下方式调用test()函数来定义所需的测试数量:

      test("Export to MongoDB Extended JSON", 2, function() {
        var book = library.get(5);
        ok(book);
    
        var json = book.toJSON();
        equal(json._id.$oid, 5);
      });
    
  5. 通过在浏览器中打开test/index.html文件来运行测试,如下面的截图所示:如何操作...

  6. 你也可以使用以下命令在控制台中运行测试,如下面的截图所示:

    $ grunt
    
    

    如何操作...

它是如何工作的...

QUnit 运行由test()函数定义的所有测试,该函数接受以下参数:名称断言数量回调函数。在测试回调内部,我们可以使用以下断言:

  • ok(): 这是一个布尔断言,等同于CommonJS 的 assert.ok()JUnit 的 assertTrue()。如果第一个参数为真,则通过。

  • equal(): 这是一个非严格比较断言,大致等同于JUnit assertEquals

  • notEqual(): 这是一个非严格比较断言,用于检查不等式。

  • strictEqual(): 这是一个严格的类型和值比较断言。

  • throws(): 这是一个断言,用于测试当运行时回调是否抛出异常。

  • notStrictEqual(): 这是一个非严格比较断言,用于检查不等式。

  • deepEqual(): 这是一个深度递归比较断言,适用于原始类型、数组、对象、正则表达式、日期和函数。

  • notDeepEqual(): 这是一个反转的深度递归比较断言,适用于原始类型、数组、对象、正则表达式、日期和函数。

如果达到所需的断言数量,则测试被认为是成功的。

在运行每个测试之前,QUnit 运行 setup() 函数,之后运行 teardown() 函数。如果我们需要更改一些全局设置然后恢复更改,这可能很有用。

由 Grunt 生成的 index.html 文件源代码如下:

<!doctype html>
<html>
<head>
  <meta charset='utf8'>
  <title>Backbone Test Suite</title>
  <link rel="stylesheet"
    href="../vendor/backbone/test/vendor/qunit.css"
    type="text/css" media="screen">
  <script src="img/json2.js">
  </script>
  <script src="img/jquery.js">
  </script>
  <script src="img/qunit.js">
  </script>
  <script src="img/underscore.js">
  </script>
  <script src="img/backbone.js"></script>
  <script src="img/backbone-mongodb.js"></script>
  <script src="img/environment.js">
  </script>
  <script src="img/noconflict.js">
  </script>
  <script src="img/events.js"></script>
  <script src="img/model.js"></script>
  <script src="img/collection.js">
  </script>
  <script src="img/router.js"></script>
  <script src="img/view.js"></script>
  <script src="img/sync.js"></script>

  <script src="img/mongodb.js"></script>
</head>
<body>
  <div id="qunit"></div>
  <div id="qunit-fixture">
    <div id="testElement">
      <h1>Test</h1>
    </div>
  </div>
  <br>
  <br>
  <h1 id="qunit-header">
    <a href="#">Backbone Speed Suite</a>
  </h1>
  <div id="jslitmus_container" style="margin: 20px 10px;">
  </div>
</body>
</html>

此外,描述 Grunt 命令的 Gruntfile.js 文件源代码如下:

module.exports = function(grunt) {
  grunt.initConfig({
    qunit: {
      all: ['test/index.html']
    }
  });

  grunt.loadNpmTasks('grunt-contrib-qunit');

  grunt.registerTask('default', ['qunit']);
};

参见

在异步测试中使用 jQuery Mockjax 模拟 RESTful 服务

在前面的菜谱中,我们熟悉了 QUnit 并测试了 toJSON() 方法,该方法用于将数据推送到 RESTful 服务。在这个菜谱中,我们将测试 fetch() 方法,这是一个异步操作。幸运的是,QUnit 允许我们创建异步测试。我们还将使用 jQuery Mockjax 模拟 RESTful 服务。

准备中...

从其 GitHub 页面下载 jQuery Mockjax 扩展,github.com/appendto/jquery-mockjax,并将其放置在扩展的 vendor 目录中。然后,在 test/index.html 文件中包含其主 JS 文件。

  <script src="img/jquery.mockjax.js"></script>

如何做...

执行以下步骤以模拟异步测试中的 RESTful 服务:

  1. setup() 方法中,以 JSON 格式定义模拟的 URL 及其输出。

          $.mockjax({
            url: '/books',
            responseTime: 10,
            responseText: [
              {_id: { "$oid": "10" }, one: 1},
              {id: "20", one: 1}
            ]
          });
    
          $.mockjax({
            url: '/books/10',
            responseTime: 10,
            responseText: {_id: { "$oid": "10" }, one: 1}
          });
    
          $.mockjax({
            url: '/books/20',
            responseTime: 10,
            responseText: {id: "20", one: 1}
          });
    
  2. teardown() 方法中取消模拟。

          $.mockjaxClear();
    
  3. 添加异步测试,从模拟的 RESTful 服务同步数据。

      asyncTest("Read MongoDB Extended JSON", 1, function() {
        library.fetch();
    
        setTimeout(function() {
          ok(library.get('10'));
          start();
        }, 50);
      });
    
      asyncTest("Read regular JSON", 1, function() {
        library.fetch();
    
        setTimeout(function() {
          ok(library.get('20'));
          start();
        }, 50);
      });
    

它是如何工作的...

在前面的代码中,我们在 asyncTest() 函数中定义了我们的测试,该函数几乎与 test() 函数相同,除了它不会继续到下一个测试,除非调用 start() 函数。

也可以使用 test()stop() 函数定义异步测试。

test("Read MongoDB Extended JSON", 1, function() {
  // do not proceed on the next stop unless start() is called 
  stop();

  library.fetch();

  setTimeout(function() {
    ok(library.get('10'));
    start();
  }, 50);
});

从前面的代码中,我们看到了 asyncTest() 函数是 test() 函数的等价物,它立即调用 stop() 函数。

了解模拟服务中发生的事情很有趣。jQuery Mockjax 用它自己的方法替换了 jQuery.ajax() 方法,该方法是模拟对服务器的 AJAX 调用。

使用 $.mockjax() 定义模拟的 URL,并使用一些帮助从 $.mockjaxClear() 取消。

参见

使用 jQuery Mobile 开发移动应用程序

jQuery Mobile 是一个用于构建移动应用程序的有用的 HTML5/JavaScript 框架。它为移动设备提供外观和感觉组件,如列表、按钮、工具栏和对话框。通过自定义 jQuery Mobile,我们很容易创建自己的主题。

默认情况下,所有移动页面都可以存储在一个 HTML 文件中的不同 div 中,或者即时渲染。jQuery Mobile 还允许我们使用过渡效果在页面之间切换。

在这个食谱中,我们将使用 jQuery Mobile 和Backbone.js创建一个简单的 iOS 外观的应用程序,允许用户查看和创建帖子。数据存储在mongolab.com/welcome/并通过 REST 访问。

我们的应用程序将类似于以下截图:

使用 jQuery Mobile 开发移动应用程序

准备工作...

执行以下步骤为这个食谱做准备:

  1. 从其 GitHub 页面github.com/dealancer/backbone-mongodb/下载 backbone-mongodb 扩展,并将其保存到lib/backbone-mongodb.js中。我们将使用 backbone-mongodb 连接到mongolab.com/welcome/,这是一个 RESTful MongoDB 服务。

  2. jquerymobile.com/下载 jQuery Mobile 库,并将其提取到lib/jquery.mobile/文件夹中。

  3. 从其 GitHub 页面github.com/taitems/iOS-Inspired-jQuery-Mobile-Theme下载 jQuery Mobile 的 iOS 灵感主题,并将其提取到lib/ios_inspired/文件夹中。

  4. www.glyphish.com/下载我们将在移动应用程序中使用的图标,并将它们提取到lib/glyphish/文件夹中。

如何做到这一点...

执行以下步骤来创建一个移动应用程序:

  1. 使用默认浏览器宽度在移动浏览器中渲染页面,否则页面可能会以 980 像素的屏幕宽度渲染然后缩小。将以下行包含到index.html的头部:

    <meta name="viewport" content="width=device-width, initial-scale=1">
    
  2. 将 CSS 文件包含到index.html的头部。

    <link rel="stylesheet" href="lib/jquery.mobile/jquery.mobile-1.1.0.min.css"/>
    <link rel="stylesheet" href="lib/ios_inspired/styles.css"/>
    <link rel="stylesheet" href="css/styles.css"/>
    
  3. 创建一个js/jqm-config.js文件,该文件将保留 jQuery Mobile 配置,并将其包含到index.html中。确保它在 jQuery 之后和 jQuery Mobile 之前被包含。

  4. js/jqm-config.js中将回调绑定到mobileinit事件。

    $(document).bind("mobileinit", function () {
    
    });
    
  5. 通过在之前步骤中定义的mobileinit事件回调中添加以下代码来禁用 jQuery Mobile 路由:

      $.mobile.ajaxEnabled = false;
      $.mobile.linkBindingEnabled = false;
      $.mobile.hashListeningEnabled = false;
      $.mobile.pushStateEnabled = false;
    
  6. 通过在mobileinit事件回调中添加以下代码来设置过渡和效果:

      $.extend($.mobile, {
        slideText: "slide",
        slideUpText: "slideup",
        defaultPageTransition: "slideup",
        defaultDialogTransition: "slideup"
      });
    
  7. 在替换页面时从文档对象模型DOM)中删除该页面。将以下代码添加到mobileinit事件回调中:

      $('div[data-role="page"]')
        .live('pagehide', function (event, ui) {
          $(event.currentTarget).remove();
        }
      );
    
  8. index.html中包含 Backbone-mongodb 扩展。

      <script src="img/backbone-mongodb.js"></script>
    
  9. 通过在js/app-config.js中添加以下代码启用跨站脚本并禁用 AJAX 缓存。同时,确保在应用程序的主文件之前包含此文件。

    jQuery.support.cors = true;
    jQuery.ajaxSetup({ cache: false });
    
  10. js/app-config.js中添加以下命令行以将Backbone.MongoModel混合到Backbone.Model中,以支持 MongoDB 扩展 JSON:

    _.extend(Backbone.Model.prototype, Backbone.MongoModel.mixin);
    
  11. js/app-config.js中添加 RESTful 服务 URL。

    var appConfig = {
      baseURL: 'https://api.mongolab.com/api/1/databases/social-mobile-app/collections/',
      addURL: '?apiKey=yGobEjzhT76Pjo9RaOLGfA89xCJXegpl'
    }
    
  12. js/template-loader.js中添加模板加载器,并在index.html中包含此文件,在主应用程序文件之前。

    $(document).ready(function () {
    
        // Create global variable within jQuery object.
        $.tpl = {}
    
        $('script.template').each(function(index) {
    
          // Load template from DOM.
          $.tpl[$(this).attr('id')] = _.template($(this).html());
    
          // Remove template from DOM.
          $(this).remove();
        });
    
    });
    
  13. js/main.js中定义路由对象,其中包含路由和回调,这是我们的主应用程序文件。它应该在所有其他文件之后包含。

    var Workspace = Backbone.Router.extend({
      routes: {
        "": "main",
        "post/list": "postList",
        "post/add": "postAdd",
        "post/details/:id": "postDetails",
        "post/delete/:id": "postDelete",
        "settings": "settings",
        "about": "about",
      },
    
      main: function() {
        this.changePage(new MainPageView());
      },
    
      postList: function() {
        var postList = new PostList();
        this.changePage(
          new PostListPageView({collection: postList})
        );
        postList.fetch();
      },
    
      postAdd: function() {
        this.changePage(new PostAddPageView());
      },
    
      postDetails: function(id) {
        var post = new Post({id: id});
        this.changePage(new PostDetailsPageView({model: post}));
        post.fetch();
      },
    
      postDelete: function(id) {
        var post = new Post({id: id});
        this.showDialog(new PostDeleteDialogView({model: post}));
        post.fetch();
      },
    
      settings: function() {
        this.changePage(new SettingsPageView());
      },
    
      about: function() {
        this.changePage(new AboutPageView());
      }
    }
    
  14. changePage()方法添加到路由对象以切换到当前视图页面。

      changePage: function (page) {
        $(page.el).attr('data-role', 'page');
    
        page.render();
    
        $('body').append($(page.el));
    
        $.mobile.changePage($(page.el), {
          changeHash: false,
          transition: this.historyCount++ ?
            $.mobile.defaultPageTransition : 'none',
        });
      }
    
  15. showDialog()方法添加到路由对象中以显示对话框。

      showDialog: function(page) {
        $(page.el).attr('data-role', 'dialog');
    
        page.render();
    
        $('body').append($(page.el));
    
        $.mobile.changePage($(page.el), {
          allowSamePageTransition: true,
          reverse: false,
          changeHash: false,
          role: 'dialog',
          transition: this.historyCount++ ? 
            $.mobile.defaultDialogTransition : 'none',
        });
      },
    
  16. js/models/post.js中定义模型和集合,并在index.html中包含此文件。

    var Post = Backbone.Model.extend({
      defaults: {
        title: "",
        body: "",
        created: new Date().toString(),
      },
    
      url: function() {
        if (_.isUndefined(this.attributes.id)) {
          return appConfig.baseURL + 'posts' + appConfig.addURL;
        }
        else {
          return appConfig.baseURL + 'posts/' + 
            encodeURIComponent(this.attributes.id) + 
            appConfig.addURL;
        }
      },
    });
    
    var PostList = Backbone.Collection.extend({
      model: Post,
      url: function() {
        return appConfig.baseURL + 'posts' + appConfig.addURL;
      }
    });
    
  17. js/views/post-details-page.js中定义PostDetailsViewPostDetailsPageView,并在index.html中包含此文件。

    var PostDetailsView = Backbone.View.extend({
      initialize: function() {
        this.model.bind('change', this.render, this);
        this.template = $.tpl['post-details'];
      },
    
      render: function() {
        $(this.el).html(this.template(this.model.toJSON())).
          trigger('create');
        return this;
      },
    });
    
    var PostDetailsPageView = Backbone.View.extend({
      initialize: function () {
        this.template = $.tpl['post-details-page'];
      },
    
      render: function (eventName) {
        $(this.el).html(this.template(this.model.toJSON()));
        this.postDetailsView = new PostDetailsView({
          el: $('.post-details', this.el), model: this.model
        });
    
        return this;
      }
    });
    
  18. index.html中为所有视图添加模板。这将使它们加载更快。以下是我们之前定义的视图的模板代码:

      <script type="text/html" class="template"
              id="post-details-page">
        <div data-role="header">
          <h1>Post Details</h1>
          <a href="#post/list" data-rel="back" data-theme="a">
            Back
          </a>
          <a href="#about" data-theme="a">About</a>
        </div>
    
        <div data-role="content" class="post-details"></div>
    
        <div data-role="footer" data-position="fixed">
          <div data-role="navbar" data-theme="a">
            <ul>
              <li><a href="#post/list" id="list-button"
                     data-icon="custom">
                  View Posts
              </a></li>
              <li><a href="#post/add" id="add-button" 
                     data-icon="custom">
                Add Post</a></li>
              <li><a href="#settings" id="settings-button" 
                     data-icon="custom">
                Settings
              </a></li>
            </ul>
          </div>
        </div>
      </script>
    
      <script type="text/html" class="template" id="post-details">
        <h1><%= title %></h1>
        <small>Posted on <%= created %>.</small>
        <p><%= body %></p>
    
        <a href="#post/delete/<%= id %>" name="delete-post"
          id="delete-post" data-role="button">Delete Post
        </a>
      </script>
    
      <script type="text/html" class="template"
              id="post-list-item">
        <div class="ui-btn-inner ui-li">
          <div class="ui-btn-text">
            <a class="ui-link-inherit"
               href="#post/details/<%= id %>">
              <%= title %>
              <br><small><%= created %></small>
            </a>
          </div>
        </div>
      </script>
    
  19. 添加视图和模板以显示其他页面。

  20. index.html中添加样式以显示工具栏底部的 Glyphish 图标。

    #list-button span.ui-icon-custom {
      background:
        url(../lib/glyphish/152-rolodex.png) 0 0 no-repeat;
    }
    
    #add-button span.ui-icon-custom {
      background:
        url(../lib/glyphish/187-pencil.png) 0 0 no-repeat;
    }
    
    #settings-button span.ui-icon-custom {
      background: url(../lib/glyphish/20-gear2.png) 0 0 no-repeat;
    }
    
  21. 检查index.html中 CSS 和 JS 包含的顺序。它应该看起来像以下代码:

      <!-- CSS -->
      <link rel="stylesheet"
        href="lib/jquery.mobile/jquery.mobile-1.1.0.min.css"/>
      <link rel="stylesheet" href="lib/ios_inspired/styles.css"/>
      <link rel="stylesheet" href="css/styles.css"/>
    
      <!-- Libraries -->
      <script src="img/jquery.min.js"></script>
      <script src="img/jqm-config.js"></script>
      <script src="img/jquery.mobile-1.1.0.min.js">
      </script>
      <script src="img/underscore-min.js"></script>
      <script src="img/backbone-min.js"></script>
      <script src="img/backbone-mongodb.js"></script>
    
      <!-- Config -->
      <script src="img/app-config.js"></script>
    
      <!-- Template loader -->
      <script src="img/template-loader.js"></script>
    
      <!-- SMA models and views -->
      <script src="img/post.js"></script>
      <script src="img/post-list-page.js"></script>
      <script src="img/post-add-page.js"></script>
      <script src="img/post-details-page.js"></script>
      <script src="img/post-delete-dialog.js"></script>
      <script src="img/main-page.js"></script>
      <script src="img/settings-page.js"></script>
      <script src="img/about-page.js"></script>
    
      <!-- SMA main file and router -->
      <script src="img/main.js"></script>
    

它是如何工作的...

此食谱的主要挑战是将 jQuery Mobile 与Backbone.js集成。基本上,除非您尝试使用 Backbone 路由,否则不应该有任何问题。Backbone.js和 jQuery Mobile 都提供了自己的路由机制,当一起使用时会相互冲突。

默认情况下,jQuery Mobile 启用了路由。如果您想使用Backbone.Router,则需要手动禁用它。这是我们之前在js/jqm-comfig.js中做的。

然而,我们仍然使用 jQuery Mobile 来切换页面。为此,我们在 div 中动态创建一个新页面,然后调用$.mobile.changePage,传递新页面元素和其他参数。如果配置了过渡效果,则执行动画。

参见

使用 PhoneGap 构建 iOS/Android 应用程序

PhoneGap 是一个免费且开源的框架,允许使用 HTML/CSS/JavaScript 构建移动应用程序。它支持 iOS、Android、Windows Phone、Blackberry 以及一些其他移动平台。此外,开发者可以访问移动设备的功能,例如相机、联系人、地理位置和存储。

要构建移动应用程序,您需要下载适用于您正在工作的移动平台的特定版本的 PhoneGap。此外,还有一个名为 PhoneGap Build 的付费在线服务,允许在线构建移动应用程序。它与 GitHub 集成,可以提取代码的最新版本。

在这个菜谱中,我们将使用 PhoneGap Build 构建一个移动应用程序。这将既简单又酷。

准备中...

请确保您已在网站上创建了账户 build.phonegap.com/apps

如何操作...

执行以下步骤以使用 PhoneGap 构建 iOS/Android 应用程序:

  1. 在与 index.html 文件相同的目录中创建 config.xml 文件。

  2. 将以下 PhoneGap 配置以 XML 格式保存在 config.xml 文件中。

    <?xml version="1.0" encoding="UTF-8" ?>
        <widget xmlns = "http://www.w3.org/ns/widgets"
            xmlns:gap = "http://phonegap.com/ns/1.0"
            id = "com.phonegap.example"
            versionCode ="1"
            version = "0.0.2">
        <!-- versionCode is optional and Android only -->
    
        <preference name="phonegap-version" value="2.7.0" />
    
        <name>Social Mobile App</name>
    
        <description>
          An example application to demonstrate Backbone.js and 
          jQueryMobile capabilities.
        </description>
    
        <author href="http://vmirgorod.name"
            email="dealancer@gmail.com">
            Vadim Mirgorod
        </author>
    
        <icon src="img/icon.png" gap:role="default" />
    
        <preference name="orientation" value="portrait" />
    </widget>
    
  3. 将包含应用程序图标的 icon.png 文件放置在根目录中。

  4. 前往 build.phonegap.com/apps/ 并点击 + 新应用 按钮。如何操作...

  5. 在表单中输入仓库 URL git://github.com/dealancer/sma.git。如何操作...

  6. 如果您想输入非 GitHub 账户或从您的计算机上传应用程序,请点击 私有 选项卡。PhoneGap 允许您免费创建一个私有应用程序。

  7. 在从 GitHub 仓库提取项目后,点击 准备构建 按钮,这将启动多个平台的构建过程。要为 iOS 或 Blackberry 构建应用程序,您需要输入开发者的密钥。如何操作...

  8. 现在,项目已准备好下载。您可以通过扫描移动设备上的二维码来完成此操作。二维码包含指向您应用程序的链接。然而,对于许多平台,您需要将构建的应用程序放置在特殊的应用市场中如何操作...

  9. 当您准备好构建应用程序的新版本时,点击 更新代码 按钮,然后点击 重建所有 按钮。

参见

使用 Require.js 组织项目结构

在这个菜谱中,我们将使用Require.js库中实现的异步模块定义(AMD)技术,该技术有助于将更多秩序带入你的项目。它允许你以类似于 PHP 中使用include命令的方式,从代码的其他部分动态定义和加载 JavaScript 模块。它还可以优化和压缩 JavaScript 文件,以便它们可以更快地加载和执行。

我们将使用上一个菜谱中的社交移动应用程序示例,并使用Require.js库进行重构。

我们应用程序的目录结构将如下所示:

  • css/

    • main.css
  • js/

    • collection/

    • post.js

  • model/

    • post.js
  • view/

    • about-page.js

    • main-page.js

    • post-add-page.js

    • post-delete-dialog.js

    • post-details-page.js

    • post-list-page.js

    • settings-page.js

  • app-config.js

  • app.js

  • jqm-config.js

  • router.js

  • template-loader.js

  • lib/

    • glyphish/

    • ios_inspired/

    • jquery.mobile/

    • backbone-mongodb.js

    • backbone.js

    • jquery.js

    • require.js

    • underscore.js

  • config.xml

  • icon.png

  • index.html

  • README.md

准备中...

www.requirejs.org/docs/download.html下载Require.js文件,并将其放置在lib目录下。

如何做...

执行以下步骤以使用Require.js组织移动应用程序:

  1. js/model/post.js中提取集合定义,并将其放置在路径js/collection/post.js下的单独文件中。

  2. index.html文件中删除所有 CSS 包含,只保留一个应该包含指向其他文件的链接的单个链接。

    @import url("../lib/jquery.mobile/jquery.mobile-1.1.0.min.css");
    @import url("../lib/ios_inspired/styles.css");
    
    // Custom styles
    // ...
    
  3. index.html文件中删除所有脚本包含,只保留一个将加载Require.js的脚本。确保使用相对于主应用程序文件的相对路径定义data-main属性。不需要.js扩展名。

    <script data-main="js/app"  src="img/require.js"></script>
    
  4. js/app.js文件中,添加Require配置,该配置定义了库的别名。我们将在以后使用其他别名。

    require.config({
    
      paths: {
        jquery            : '../lib/jquery',
        'jquery.mobile':
         '../lib/jquery.mobile/jquery.mobile-1.1.0',
        underscore: '../lib/underscore',
        backbone: '../lib/backbone',
        'backbone-mongodb': '../lib/backbone-mongodb',
      }
    
    });
    
  5. 通过在Require配置中添加 shim 属性来定义模块依赖。

      shim: {
        'backbone-mongodb': {
          deps: ['backbone'],
          exports: 'Backbone'
        },
        'backbone': {
          deps: ['underscore', 'jquery'],
          exports: 'Backbone'
        },
        'underscore': {
          exports: '_'
        },
        'jquery.mobile': ['jquery','jqm-config'],
        'jqm-config': ['jquery'],
        'jquery': {
          exports: '$',
        }
      }
    

    在这里,我们让Require了解第三方库依赖;例如,jquery.mobile需要jqueryjqm-config,并且应该先加载。如果你使用没有 AMD 支持的常规 JS 库,你应该定义那些库提供的对象(例如,jQuery 中的$)。这可以通过在export属性中定义对象名称来完成。

  6. Require配置中添加映射设置,以在应用程序的所有 JS 文件中加载backbone-mongodb对象而不是backbone对象;然而,要加载backbone-mongodb,我们仍然需要加载backbone

      map: {
        '*': {
          'backbone': 'backbone-mongodb',
        },
        'backbone-mongodb': {
          'backbone': 'backbone'
        }
      }
    
  7. requirejs()函数调用添加到js/app.js中以启动应用程序。第一个参数包含应加载的模块数组,第二个参数提供要执行的回调函数。此类回调函数的参数是requirejs()函数第一个参数中定义的模块返回的对象。

    requirejs([ 'app-config', 'router' ],
    function (appConfig, Router) {
    
      $(document).ready(function () {
    
        window.router = new Router();
        Backbone.history.start({ pushState : false });
    
      });
    
    });
    

    上述代码意味着在执行回调函数中的代码之前,会包含并实现app-config.jsrouter.js文件。

  8. 将所有自定义 JS 文件重构为 AMD 兼容。添加与requirejs()函数语法相似的define()函数调用。如果模块提供了一个对象(或值)供其他模块使用,那么这个对象应该由模块返回。app-config.js文件将类似于以下代码:

    // Filename: app-config.js
    
    define(['jquery', 'backbone'],
      function($, Backbone) {
    
        // Enable cross site scripting.
        $.support.cors = true;
    
        // Disable ajax cache.
        $.ajaxSetup({ cache: false });
    
        // Add support of MongoDB Extended JSON.
        _.extend(Backbone.Model.prototype,     
          Backbone.MongoModel.mixin);
    
        // Return app configuration.
        return {
          baseURL: 'https://api.mongolab.com/api/1/databases/
            social-mobile-app/collections/',
          addURL: '?apiKey=yGobEjzhT76Pjo9RaOLGfA89xCJXegpl'
        }
      }
    );
    
  9. 虽然Require.js文件可以从文本文件中加载模板,但让我们处理我们之前使用的模板加载器。它也需要是 AMD 兼容的。

    // Filename: template-loader.js
    
    define(['jquery', 'underscore'],
      function($, _) {
    
        // Create global variable within jQuery object.
        var tpl = {};
    
        $('script.template').each(function(index) {
    
          // Load template from DOM.
          tpl[$(this).attr('id')] = _.template($(this).html());
    
          // Remove template from DOM.
          $(this).remove();
        });
    
        return tpl;
      }
    );
    
  10. 确保所有视图文件也被重构。它们可能看起来像以下代码:

    // Filename: about-page.js
    
    define(['jquery', 'backbone', 'template-loader'],
      function($, Backbone, tpl) {
        return Backbone.View.extend({
          initialize: function () {
            this.template = tpl['about-page'];
          },
    
          render: function (eventName) {
            $(this.el).html(this.template());
            return this;
          },
        });
      }
    );
    
  11. 确保所有必需的模块依赖项都包含在router.js文件中。

    // Filename: router.js
    
    define([
      'jquery',
      'jquery.mobile',
      'backbone',
      'model/post',
      'collection/post',
      'view/about-page',
      'view/main-page',
      'view/post-add-page',
      'view/post-delete-dialog',
      'view/post-details-page',
      'view/post-list-page',
      'view/settings-page',
    ], function($, mobile, Backbone, PostModel, PostCollection,
          AboutPageView, MainPageView, PostAddPageView,
          PostDeleteDialogView, PostDetailsPageView,
          PostListPageView, SettingsPageView) {
    
      return Backbone.Router.extend({
        // Router code
      });
    });
    
  12. 删除main.js文件,因为我们已经将所有功能从它移动到了app.jsrouter.js文件中。

它是如何工作的...

Require.js库提供了两个主要函数,define()requirejs(),用于加载其他模块。requirejs()函数用于启动应用程序。这两个函数具有相似的语法。第一个参数用于列出当前模块所需的全部库,第二个参数包含要执行的回调函数。

define(['jquery', 'backbone', 'template-loader'],
  function($, Backbone, tpl) {

    return Backbone.View.extend({
      initialize: function () {
        this.template = tpl['about-page'];
      },

      render: function (eventName) {
        $(this.el).html(this.template());
        return this;
      },
    });
});

回调函数的参数是模块所需的库返回的对象/值。它们的顺序与所需的模块顺序相同。

如果模块定义了一个其他模块需要的对象,它应该返回这样一个对象。

如果你正在处理没有 AMD 库的情况,但它提供了一个供你的应用程序其他模块使用的对象,你应该在require.config()函数中定义这样的对象。

require.config({
  shim: {
    'jquery': {
      exports: '$',
    }
  }
});

如果你需要确保模块总是按照特定的顺序加载,你应该在require.config()函数中定义依赖项。

require.config({
  shim: {
    'jquery.mobile': ['jquery','jqm-config'],
    'jqm-config': ['jquery'],
    'jquery': {
      exports: '$',
    }
  }
});

默认情况下,Require.js文件使用相对于主项目目录的路径来加载库。在引用此类库时跳过.js扩展名。在require.config()函数中还可以定义路径别名。

require.config({

  paths: {
    jquery            : '../lib/jquery',
    'jquery.mobile':
      '../lib/jquery.mobile/jquery.mobile-1.1.0',
  }

});

当应用程序启动时,主应用程序文件运行,所有必需的模块和库都按正确的顺序和定义及配置加载。

还有更多...

使用 r.js 优化 JS 文件

R.jsRequire.js的一个子模块,可以通过将它们合并成一个文件并最小化来优化 JavaScript 或 CSS 文件,从而使其加载和执行得更快。

要从本地主机加载我们的社交移动应用程序,浏览器需要执行 27 次请求,大约需要 308 毫秒。

使用 r.js 优化 JS 文件

现在优化后的相同应用程序,只需 4 次请求在 53 毫秒内加载完成。

使用 r.js 优化 JS 文件

在这里,我们看到性能提升了六倍,这是一个很好的结果。实际上,对于加载速度较慢的互联网连接的大型项目,这个提升可能更大。

要优化您的应用程序,请执行以下步骤:

  1. 确保您已安装Node.jsnpm

  2. Require.js作为 Node 模块安装。

    $ npm install -g requirejs
    
    
  3. 创建一个名为 src 的新子目录并将所有项目文件移至其中。

  4. www.requirejs.org/docs/download.html下载r.js并将其保存到根项目目录中。

  5. 在项目根目录中创建app.build.js文件。此文件应包含一个R.js构建配置。

    ({ 
        appDir: "./src", 
        baseUrl: "js", 
        dir: "build", 
        mainConfigFile: "src/js/app.js", 
        modules: [ 
            { 
                name: "app" 
            }, 
        ] 
    }) 
    
  6. 执行以下命令以构建项目:

    $ node r.js -o app.built.js
    
    

    您可以在build目录中找到构建的应用程序。

参见

确保与搜索引擎兼容性

当搜索引擎发现一个由 AJAX 驱动的 Web 应用程序时,它无法索引这样的应用程序,因为搜索引擎不会执行复杂的 JavaScript 代码。搜索引擎想要的是静态 HTML。

在本食谱中,我们将学习如何使搜索引擎索引 AJAX Web 应用程序。我们将主要处理 Google,但也会考虑如何与其他搜索引擎合作。

此食谱背后的想法是,我们可以在服务器上将 AJAX 应用程序渲染为静态 HTML 页面,并通过代理重定向将其发送给搜索引擎蜘蛛。

要在服务器上渲染 JavaScript,我们将使用Node.jsPhantom.js文件,这是一个作为 Node 模块提供的无头 WebKit 浏览器。我们还将使用一个名为 Seoserver 的 Node 模块,它帮助我们运行Phantom.js并输出结果。

为了区分搜索引擎蜘蛛和普通客户端,并使用代理重定向到 Seoserver,我们将使用 Apache 的mod_rewritemod_proxymod_proxy_http模块。

准备工作...

执行以下步骤以准备此食谱:

  1. 确保您已安装Node.js和 npm。

  2. Phantom.js作为 Node 模块安装。

    $ sudo npm install -g phantomjs
    
    
  3. 安装 Seoserver,它也是一个 Node 模块。

    $ sudo npm install -g seoserver 
    
    
  4. 确保您已安装 Apache。

  5. 确保您已安装并配置以下 Apache 扩展:mod_rewritemod_proxymod_proxy_http

  6. 确保您有权限覆盖.htaccess文件中的配置。

如何操作...

执行以下步骤以确保与搜索引擎兼容性:

  1. 通过在index.html的标题部分添加以下行,告诉Google蜘蛛使用_escaped_fragement_而不是#!

    <meta name="fragment" content="!"> 
    

    我们将在稍后了解其含义。

  2. 创建 .htaccess 文件,并将以下行放置其中以通过代理将重定向操作执行到运行在 3000 端口的 Seoserver。

    <IfModule mod_rewrite.c>
     RewriteEngine on
    
     RewriteCond %{QUERY_STRING} ^_escaped_fragment_=(.*)$
     RewriteRule (.*) http://<host>:3000/<path>/index.html#%1? [P]
    </IfModule>
    
  3. 要通过代理将其他搜索引擎(例如 Yandex)重定向到 Seoserver,请将以下行添加到 .htaccess 文件中。

    RewriteCond %{HTTP_USER_AGENT} ^YandexBot
    RewriteRule (.*) http://<host>:3000/<path/>index.html#%1?
    
  4. 通过运行以下命令启动 Seoserver。

    $ seoserver -p 3000 start > seoserver.log
    
    
  5. 可选地,创建一个具有以下格式的网站地图:http://<host>/<path>index.html#!route

  6. 您可以使用以下链接检查结果并查看 Googlebot 看到的内容:support.google.com/webmasters/bin/answer.py?hl=en&answer=158587

    小贴士

    您也可以通过访问 http://<host>/<path>index.html?_escaped_fragement_=route 来手动检查结果。在这种情况下,请确保您已禁用浏览器中的 JavaScript,以避免任何冲突。

它是如何工作的...

有一种方法可以让 Googlebot 理解该网站支持 AJAX 爬取方案。它简单地尝试使用类似 http://<host>/</path>index.html#!route 的 URL 访问网站,并检查是否有任何显著的结果。#! 被用来代替 #,以向网站管理员表明这正是 Googlebot 在尝试访问资源时想要的。Googlebot 还会扫描网站地图,并尝试找到具有相同 URL 方案的 URL。

网站管理员应实现处理此类 URL 的处理,并输出可以被搜索引擎轻松索引的 HTML 快照。如果服务器无法处理带有 #! 的 URL,则允许使用以下 URL 方案:http://<host>/</path>index.html?_escaped_fragement_=route。这应通过在 HTML 输出中添加特殊元标签来指示。

<meta name="fragment" content="!">

这种易于 Apache 和 Googlebot 处理的 URL 方案通过代理重定向到输出 HTML 快照的服务器。

我们将传递所有参数到运行在 3000 端口的 Seoserver,并调用 phantom 来获取请求资源的 HTML 快照。

Seoserver 是用 Node.js 编写的。让我们看看 seoserver.js 中的源代码。

var express = require('express');
var app = express();
var arguments = process.argv.splice(2);
var port = arguments[0] !== 'undefined' ? arguments[0] : 3000;
var getContent = function(url, callback) {
  var content = '';

  var phantom = require('child_process').spawn(
    'phantomjs', [__dirname + '/phantom-server.js', url]
  );

  phantom.stdout.setEncoding('utf8');
  phantom.stdout.on('data', function(data) {
    content += data.toString();
  });

  phantom.stderr.on('data', function (data) {
    console.log('stderr: ' + data);
  });

  phantom.on('exit', function(code) {
    if (code !== 0) {
      console.log('We have an error');
    } else {
      callback(content);
    }
  });
};

var respond = function (req, res) {
  res.eader("Access-Control-Allow-Origin", "*");
  res.eader(
    "Access-Control-Allow-Headers", "X-Requested-With"
  );

  var url;
  if(req.headers.referer) {
    url = req.headers.referer;

  }
  if(req.headers['x-forwarded-host']) {
    url = 'http://' + req.headers['x-forwarded-host'] + 
      req.params[0];

  };

  console.log('url:', url);

  getContent(url, function (content) {
    res.send(content);
  });
}

app.get(/(.*)/, respond);
app.listen(port);

Seoserver 还包含以下代码的 phantom-server.js 文件:

var page = require('webpage').create();
var system = require('system');
var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];

page.viewportSize = { width: 1024, height: 768 };

page.onResourceReceived = function (response) {
    if(requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};

page.onResourceRequested = function (request) {
    if(requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

page.open(system.args[1], function () {

});

var checkComplete = function () {
  if(new Date().getTime() - lastReceived > 300 && requestCount 
      === responseCount)  {
    clearInterval(checkCompleteInterval);
    console.log(page.content);
    phantom.exit();
  } else {

  }
}
var checkCompleteInterval = setInterval(checkComplete, 1);

相关内容

避免在 Backbone 应用程序中发生内存泄漏

内存泄漏是计算机程序中可能发生的问题,由于内存分配不正确。在 JavaScript 等高级面向对象语言中,内存泄漏通常与存储在内存中但未被应用程序代码使用的对象有关。内存泄漏可能导致更严重的问题,例如耗尽可用系统内存。

以下示例演示了由闭包(匿名函数)引起的内存泄漏:

var div = document.createElement("div");
div.onclick = function () {  }

在前面的代码中,创建了一个新的 HTML 元素,并将 onclick 回调分配给一个匿名函数。这样的代码会产生内存泄漏,因为 div 引用了闭包,而闭包引用了 div,因为 div 变量可以在闭包作用域中访问。这种循环引用可以产生内存泄漏,因为垃圾收集器既没有利用 div 也没有利用闭包。

在这个菜谱中,我们将学习如何在 Backbone 应用程序中检测内存泄漏以及如何修复它们。我们将使用 Google Chrome Heap Profiler,它是 Google Chrome 浏览器的一部分。

准备中...

在这个菜谱中,我们将从一个绑定集合到第五章视图的示例应用程序开始,事件和绑定进行修改。这些修改在生产应用程序中不是必需的,但将帮助我们使用 Google Chrome Heap Profiler 检测内存泄漏。

  1. 在你的程序中,为每个从标准 Backbone 对象(如 Model 或 View)扩展的对象添加一个命名构造函数。在这个构造函数内部,调用父构造函数。

    1. 通过使用类名查找对象实例,在 Google Chrome Heap Profiler 中检测内存泄漏可能会更容易,这只有在我们使用命名构造函数定义这样的类时才可能。

    2. 以下代码显示了定义了命名构造函数的 InvoiceItemModel 对象。

        var InvoiceItemModel = Backbone.Model.extend({
          calculateAmount: function() {
            return this.get('price') * this.get('quantity');
          },
      
          constructor: function InvoiceItemModel() {
            InvoiceItemModel.__super__.constructor.apply(
              this, arguments
            );
          }
        });
      
  2. 确保你的应用程序代码在全局范围内执行。这将使我们在 Google Chrome Heap Profiler 中找到 Backbone 对象更容易。你的 main.js 文件的内容不应该被任何函数包围。以下几行代码应该从你的 main.js 文件中移除。

    (function($){
      $(document).ready(function () {
    
      });
    })(jQuery);
    

    main.js 包含到 index.htmlbody 部分中,如下所示:

    <body><script src="img/main.js"></script></body>
    
  3. 通过添加一个删除 InvoiceItemsTableView 的按钮来修改 ControlsView,以演示内存泄漏。以下代码解释了它是如何工作的:

      var ControlsView = Backbone.View.extend({
        render: function() {
          var html = '<br><input id="addModel" type="button" ' +
            'value="Add model" id><input id="removeModel" ' +
            'type="button" value="Remove model"><input ' +
            'id="removeTableView" type="button" ' +
            'value="Remove table view">';
          $(this.el).html(html);	
    
          return this;
        },
    
        // Handle HTML events.
        events: {
          'click #addModel': 'addNewInvoiceItemModel',
          'click #removeModel': 'removeInvoiceItemModel',
          'click #removeTableView': 'removeInvoiceItemTableView',
        },
    //...
    
        // Remove a view button handler.
        removeInvoiceItemTableView: function() {
          this.options.invoiceItemTableView.remove(); 
        },
      });
    
      //...
    
      invoiceItemTableView = new InvoiceItemTableView({
        collection: invoiceItemCollection
      });
    
      $('body').append(invoiceItemTableView.render().el);
    
      $('body').append(new ControlsView({
        collection: invoiceItemCollection,
        invoiceItemTableView: invoiceItemTableView
      }).render().el);
    

我们准备的应用程序应该看起来像以下图片:

准备中...

如何做到这一点...

执行以下步骤以检测和修复此应用程序中的内存泄漏:

  1. Chrome 浏览器中打开一个 Web 应用程序。

  2. 按下 F12 键以打开 Chrome DevTool

  3. 点击 配置文件 选项卡并选择 获取堆快照 项。如何做到这一点…

  4. 点击 获取快照 按钮。

  5. 类过滤器 字段中输入 Invoice

  6. 你将在对象计数列下看到所有以Invoice开头以及它们实例数量的所有类。如何操作…

  7. 点击移除表格视图按钮,再次获取堆快照以查看内存泄漏。

  8. 你会发现对象计数对于任何类都没有减少,但应该是减少的。如何操作…

  9. 当不需要这些引用时,从其他对象中删除对对象的任何引用。

  10. 在调用remove()方法后删除对InvoiceItemsTableView实例的引用。

      var ControlsView = Backbone.View.extend({
    
        // ...
    
        removeInvoiceItemTableView: function() {
          this.options.invoiceItemTableView.remove(); 
          delete this.options.invoiceItemTableView;
        },
      });
    
  11. 当父视图被移除时,删除所有子子视图。

  12. 在以下代码中,当创建新的子视图时,我们将它的移除方法作为处理程序分配给父视图的清除事件。在父视图的remove()方法中,我们触发清除事件。

      var InvoiceItemTableView = Backbone.View.extend({
    
        // ...
    
        append: function(model) {
          var view = new InvoiceItemView({ model: model });
    
          $(this.el).append(
            view.render().el
          );
    
          view.listenTo(this, 'clear', this.remove);
        },
    
        remove: function() {
    this.trigger('clear'); 
    
          return InvoiceItemTableView.__super__.remove.
     apply(
     this, arguments
     );
        }
      });
    
  13. 使用listenTo()方法而不是on()方法来绑定回调到事件。

  14. listenTo()方法跟踪绑定的事件,当对象被移除时解除绑定,以确保没有循环引用。

      var InvoiceItemView = Backbone.View.extend({
    
        // ...
    
        initialize: function() {
          // Bind callback to destroy event of the model.
          this.listenTo(
     this.model, 'destroy', this.destroy, this
     );
        }
      });
      var InvoiceItemTableView = Backbone.View.extend({
    
        // ...
    
        initialize: function() {
          // Bind callback to add event of the collection.
          this.listenTo(
            this.collection, 'add', this.append, this
          );
        }
      });
    
  15. 重新加载页面,移除表格视图,然后创建一个新的堆快照以确保没有发票视图泄漏。我们仍然可以看到一些模型被保留在内存中,但这发生是因为它们被ControlsView使用。如何操作…

参见

posted @ 2025-09-24 13:53  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报