Backbone-Marionette-入门指南-全-
Backbone Marionette 入门指南(全)
原文:
zh.annas-archive.org/md5/b7cef06d807e3b5011ac5e74f583df82译者:飞龙
前言
Backbone Marionette 是一个用于Backbone.js的复合应用程序库,旨在简化大型 JavaScript 应用程序的构建。它包含我们在使用 Backbone 构建的应用程序中发现的常见设计和实现模式,并包括受复合应用程序、事件驱动和消息架构启发的部分。
本书涵盖的内容
第一章,从 Backbone Marionette 开始,介绍了 Marionette 是什么以及它旨在解决的问题。在本章中,我们还了解了其先决条件、下载源和文档。
第二章,我们的第一个应用程序,介绍了 Marionette 的三个主要概念——应用程序、控制器和路由对象——并详细说明了构建小型应用程序的过程。
第三章,Marionette 视图类型及其使用,深入探讨了 Marionette 具有的视图类型以及如何使用它们。
第四章,管理视图,回顾了从触发视图、关闭视图和重新打开视图的视图管理过程。我们还将介绍一些非常有用的对象,例如Renderer对象和TemplateCache对象,这些对象对于构建应用程序非常有价值。
第五章,分而治之——模块化一切,讨论了如何模块化一个应用程序并将其分解为小的子应用程序。能够做到这一点将提高其生产力,因为模块允许在不影响现有代码的情况下添加新功能。
第六章,消息传递,解释了为了构建一个松散耦合的应用程序,组件之间需要了解彼此的非常少;然而,这些组件仍然需要协同工作。在本章中,我们还学习了如何通过消息和事件来实现这一点。
第七章,改变和成长,帮助我们学习如何管理大型应用程序带来的问题:文件爆炸,以及如何保持整洁的结构。
您需要为本书准备的内容
您只需要一个现代浏览器和一个文本编辑器就可以跟随本书的示例。您将找到如何设置您的开发环境的详细说明,以及在哪里获取 Marionette 及其依赖项。
本书面向的对象
如果您是一位对使用 Backbone Marionette 进行实际项目开发的 Web 应用程序开发者,这本书就是为您准备的。了解 JavaScript 以及熟悉Backbone.js是先决条件。
习惯用法
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词如下所示:“Marionette.ItemView是”
代码块设置为如下所示:
var CategoryView = Backbone.Marionette.ItemView.extend({
tagName : 'li',
template: "#categoryTemplate",
});
注意
警告或重要注意事项以这种方式出现在一个框中。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们欢迎读者的反馈。让我们知道您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书名。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲所有者,我们有多种方法可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现了错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误清单提交表单链接,并输入您的错误清单详情来报告它们。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站,或添加到该标题的错误清单部分。任何现有的错误清单都可以通过从www.packtpub.com/support选择您的标题来查看。
侵权
侵权是互联网上所有媒体持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过<copyright@packtpub.com>与我们联系,并提供涉嫌侵权材料的链接。
我们感谢您在保护我们作者方面的帮助,以及我们为您提供有价值内容的能力。
问题和答案
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章. 从 Backbone Marionette 开始
这本实用指南提供了使用 Marionette.js 编写可扩展应用程序的基本步骤。随着你通过初始示例的进展,你将了解框架组件如何协同工作以创建复合应用程序。但在我们深入示例之前,以下是你在本介绍章节中可以找到的一些内容:
-
Marionette.js的描述和特性 -
Marionette.js在 Backbone 应用程序中的作用 -
框架的好处
-
架构和可扩展性的概述
-
安装和文档说明
介绍 Marionette.js
Backbone.js 的复合应用程序库是 Backbone.marionette,也称为 Marionette.js。它为我们提供了核心结构,并简化了你的 JavaScript 应用程序需要可扩展性的许多模式和惯例。
Backbone 需要 Marionette.js
Backbone.js 是一个越来越受欢迎的框架,用于构建单页应用以及中小型应用。它提供了一套优秀的构建块来组织你的前端开发,并构建支持移动设备的应用程序。然而,它将大部分的应用设计、架构和可扩展性留给了开发者。尽管如此,Marionette.js 通过填补 Backbone.js 自身未提供的空白,并提供了你可以利用的约定来构建自己的自定义对象。简单来说,Marionette.js 在开发 Backbone 应用程序时使你的生活变得更简单。
Marionette.js 的关键好处
添加了许多用于创建现实世界应用程序的关键模式和工具,Marionette.js 在 Backbone 中找到了自己的位置。以下是一些你可以在这个框架中找到的好处:
-
结构、组织和模式
-
复合应用程序架构
-
带有事件聚合器的事件驱动架构
-
可以减少视图的样板代码
-
企业消息模式的影响
-
模块化选项
-
渐进式使用;因为它不是一个全有或全无的框架,这意味着你可以只使用你需要的组件
-
在视觉区域内部支持嵌套视图和布局
-
视图、区域和布局中的内置内存管理和僵尸清除
Marionette.js 提供了大量构建任何模块大小应用程序所需的应用基础设施组件。
Marionette.js 提供了一系列对象,这些对象有助于创建几乎任何大小和复杂性的良好结构化应用程序。它通过提供一组常见的设计和实现模式来实现这一目标,这些模式是创建者 Derick Bailey 在开发模块化 Backbone 应用程序时使用的。
构建大型应用程序
在规划应用程序的架构时,你通常会尽可能提前思考,试图实现一个解耦的架构,将功能分解成独立的模块,并避免在这些模块之间进行直接调用。因此,你可以添加和删除模块而不影响其行为。
| "构建大型应用程序的秘诀永远不是构建大型应用程序。将你的应用程序分解成小块。然后,将这些可测试的、易于管理的块组装成你的大型应用程序" | ||
|---|---|---|
| -- Justin Meyer,JavaScriptMVC 的作者 |
考虑到相互关联的组件难以更改和扩展而不影响彼此。Marionette.js通过使用模块定义提供了非常增量且模块化的方法。它依赖于事件聚合器在模块之间发送消息以协调来自系统其他部分的功能,而不在许多其他对象类型中添加对它们的直接引用,这些对象类型有助于简化应用程序的设计。
逐步使用
这是Marionette.js的创造者用来创建框架的基本概念之一。Marionette.js通过提供应用程序对象和模块架构,促进了增量且模块化的方法,以跨子应用程序、功能和文件扩展应用程序。所有这些都是为了独立运行并与 Backbone 的核心组件协同工作,以满足应用程序的具体需求。
| "关键是从一开始就承认你不知道这一切会如何发展。当你接受自己不知道一切时,你开始设计具有防御性的系统...你应该花大部分时间思考接口而不是实现。" | ||
|---|---|---|
| -- Nicholas Zakas,《高性能 JavaScript 网站》的作者 |
Marionette.js的主要优点之一是你不需要使用它的所有组件。jQuery-jQuery UI 的比较也适用于这里。你无论如何使用 jQuery 日历的事实迫使你必须使用整个 UI 库。同样,这也适用于 Marionette,因为你只使用对你应用程序有意义的其中一个组件,这并不强制你使用 Marionette 的其他组件。
安装 Marionette.js
我们将介绍如何下载和设置开发环境,以便你可以通过一些简单的步骤开始使用Marionette.js。如果你已经熟悉安装Marionette.js,你可能想跳过本章剩余的部分。请随意跳转到第二章,我们的第一个应用程序。
文本编辑器
在构建大型且可扩展的应用程序时,你将大部分时间花在代码编辑器上。我们建议你使用适合你的编辑器。Notepad++或 Sublime Text 无疑是很好的选择。Sublime Text 已经有很多片段和包可以帮助你在开发中。
网络浏览器
与复杂的客户端应用程序一起工作需要一套良好的开发者工具。为了编写本指南,我们将主要使用 Google Chrome 和 Mozilla Firefox,但所有代码和示例都应在所有现代浏览器(IE9+、Opera 和 Safari)中工作。
我们将使用 jsfiddle.net 来展示运行示例。使用这个网站非常简单,大多数情况下,你只需要运行代码就能看到它的实际效果。
前置条件
在撰写本文时,Marionette.js的当前稳定版本是 v1.3.0,它依赖于以下库:
-
JSON2.js -
jQuery(v1.7, v1.8, and v1.9) -
Underscore.js(v1.4.4) -
Backbone.js(v1.0.0) -
backbone.wreqr.js -
backbone.babysitter.js
我们想指出,对 Backbone 和 Underscore 有基本了解将帮助你从这本书中获得最大收益,并理解 Marionette 相对于纯 Backbone 开发的优点。
获取 Marionette.js
获取Marionette.js最新构建的最佳方式是访问项目网站,marionettejs.com/。
他们有一个预包装选项。.zip文件包含了你开始使用Marionette.js(包括 Backbone、jQuery 和其他前置条件)所需的全部文件。你也可以下载不包含所有依赖的Marionette.js文件,如果这些库的 CDN 版本可用,你也可以只使用这些库的 CDN 版本。
文档
你可以从github.com/Marionette.jsjs/Marionette.js/tree/master/docs下载Marionette.js每个部分的文档。文档分为多个文件。带注释的源代码可以在marionettejs.com/docs/backbone.marionette.html找到。你可以在其维基页面上找到文章、博客文章、演示文稿、常见问题解答等,github.com/marionettejs/backbone.marionette/wiki。
Marionette 的创造者 Derick Bailey 创建了一个示例应用程序,可以用作使用Marionette.js构建 Backbone 应用程序的参考。该应用程序的名称是BBCloneMail,它是一个展示复合应用程序的绝佳例子。你可以在bbclonemail.heroku.com上找到BBCloneMail,源代码在github.com/derickbailey/bbclonemail。
摘要
在本章中,我们探讨了Marionette.js为构建可扩展应用程序提供的核心概念和优点。我们还提供了查找、下载和安装进行开发所需的基本工具的链接。在下一章中,你将介绍构成Marionette.js应用程序的组件或构建块。
第二章 我们的第一个应用
在上一章中,我们学习了 Marionette 是什么,在哪里可以找到源代码和文档,以及其他有助于我们学习 Marionette 的有用资源。但我们认为,学习最好的方式是将所学应用于实践。因此,在本书中,我们将构建一个具有适度复杂性的应用,即复杂到足以突破 Hello World!的障碍,使我们能够发现 Marionette 提供的优势,但简单到足以在本书中完成。我们将展示一些独立的代码片段来介绍每个新概念;然而,大多数时候我们将坚持应用代码。
在本章中,我们将回顾如何设置开发环境以构建我们的第一个应用。我们还将学习Marionette.js的三个重要部分:Marionette 路由器、Marionette 控制器和 Marionette 应用。
我们正在构建的简介
本书我们将构建的应用是一个书店网站。我们应能够在网站上执行以下操作:
-
显示书籍类别列表
-
选择一个类别并显示相关书籍
-
展示书籍的描述、价格和其他重要细节
-
将书籍添加到购物车
-
显示购物车中的商品
我们将要构建的网站只是一个示例应用。必须遵循本书中提出的结构,因为每个应用都有不同的需求。尽管如此,它是一个良好的起点,我们的想法是展示 Marionette 的每个组件是如何解决问题以及如何使这些组件协同工作的。
此外,请记住,我们将关注代码的 Marionette 组件,详细解释它们的优点,以及如何将它们添加到应用中。然而,我们不会深入探讨 Backbone 的细节,如Backbone.Model和Backbone.Collection,这些是 Backbone 的核心组件,因为假设读者已经了解这些内容。
Marionette 为 Backbone 添加的一个概念是应用对象——Backbone.Marionette.Application。我们将从本书的这个主题开始,因为这个对象将是所有 Backbone 视图和模型的容器。其一个职责是在用户开始与网站交互之前,必须初始化一些组件,例如将监听应用路由(URL)变化的Backbone.Router组件。这个对象提供了一些方便的方法来执行这个初始化。但在我们深入细节之前,让我们首先看看我们正在构建的内容。
以下截图有助于我们展示我们将要构建的书店应用的结构:

我们有一个导航部分,提供书籍的分类。然后在中间,我们有两个部分。顶部的是按名称、作者和价格列出的书籍列表。此部分还允许用户订购书籍。
屏幕中央的第二部分将在用户从顶部列表中选择时显示每本书的描述。最后,屏幕右侧是我们的订单部分,它将包含有关我们订单的详细信息。
在本书的结尾,应用程序应如下截图所示:

本章的目标是构建书店网站的基础,其中一部分基础是让 Backbone.Marionette.Application 对象具有足够的功能,以便我们可以称之为应用程序。我们的理念是一步一步地前进,然后检查我们的位置。那么,让我们开始吧!
设置我们的开发环境
由于我们将一起构建应用程序,我们需要设置我们的开发环境。以下是为此执行此操作的步骤:
-
创建一个文件夹,并将其命名为
Bookstore。 -
在此文件夹内,创建两个新的文件夹——一个命名为
源代码,另一个命名为库。 -
在
库文件夹中,放置以下四个库:-
Underscore.js -
jQuery.js -
Backbone.js -
Backbone.Marionette.js
为了样式化目的,我们将使用 Twitter bootstrap v2。下载默认包,解压它,并将整个解压的 bootstrap 文件夹放置在
库文件夹中的.js文件旁边。 -
-
在
源代码文件夹中,创建一个名为js的新文件夹,因为这是我们保存所有 JavaScript 文件的位置。 -
在
源代码文件夹下,创建一个 HTML 文件,并将其命名为Index.html。它应放置在js文件夹同一级别。 -
确保您的文件夹结构如下截图所示,并且在
库文件夹中包含正确的库文件。![设置我们的开发环境]()
您的
源代码文件夹应如下截图所示:![设置我们的开发环境]()
我们正在构建一个单页应用程序,在本节中,我们将为我们的应用程序构建初始的 HTML 页面结构。这是服务器首次渲染用户输入网站 URL 时将渲染的 HTML 文件。
-
在您偏好的代码编辑器中打开
Index.html文件。 -
为了避免手动编写本章的 HTML 文件这一繁琐的任务,我们已将其提供给您,请访问
jsfiddle.net/。代码可在jsfiddle.net/rayweb_on/hsrv7/11/获取。注意
jsfiddle.net——如果您还不知道,这是一个测试您 JavaScript 代码小部分并轻松分享代码片段的优秀工具。
我确信如果你正在阅读一本 Marionette 书,那是因为你已经有足够的经验将 CSS 和 JS 标签放在正确的位置。所以请随意跳过以下步骤。
-
复制 CSS 部分,并将其粘贴到 HTML 文件的
<head>部分。 -
复制 HTML 部分,并将其粘贴到 HTML 文件的
<body>部分。 -
在
jsfiddle.net/,脚本已经为你准备好了。但在我们的本地环境中,我们必须添加它们。我们将在<html>标签的底部进行操作,但仍然在<body>标签内。 -
当你完成复制初始结构后,你的 HTML 文件应该看起来像下面的截图(截图中的样式脚本和模板脚本已折叠)。在本章中,我们将使用你浏览器的控制台,并且目前我们不会与 HTML 文件进行交互,但你的
Index.html文件遵循以下截图所示的结构是很重要的:![设置我们的开发环境]()
注意
引导和样式化你的页面超出了本书的范围。但这是一个非常方便的库,它允许我们为这个演示应用程序设置一个看起来不错的 HTML 文件。
但是等等……最后一个脚本
js/BookStore.js指的是什么?嗯,那是我们在下一步将要创建的 JavaScript 代码。
Backbone.Marionette.Application 对象
在 js 文件夹内创建一个新文件,并将其命名为 BookStore.js。要创建一个新的应用程序,我们只需要在 Bookstore.js 中输入以下行:
var bookStoreApp = new Backbone.Marionette.Application();
我们将命名应用程序为 BookStoreApp,并将开始将我们的 Backbone 组件附加到这个应用程序上。但是,我们已经提到过 Marionette 引入了应用程序对象的概念,并且从文档中我们也可以知道,这是一个将帮助我们协调应用程序各个组件的对象。你可能想知道,哪些组件;例如,一个 Marionette.Router 对象和一个 Marionette.Controller 对象。
Backbone 已经有一个路由器了!
是的,Backbone 已经有一个路由器对象。那么 Marionette.Router 对象有什么不同之处呢?嗯,新的路由器增加了将你的路由器简化为仅包含应用程序路由的小文件的能力,而不会包含一旦路由匹配就会响应和采取行动的方法。这些方法属于控制器——Marionette 为 Backbone 添加的另一个新概念。
让我们构建一个 Marionette.Router 对象和一个 Marionette.Controller 对象,以便更好地理解它们:
var BookStoreController = Backbone.Marionette.Controller.extend({displayBooks : function (){
console.log("I will display books...");
}
});
var BookStoreRouter = Backbone.Marionette.AppRouter.extend({
controller : BookStoreController,
appRoutes: {
"": "displayBooks"
}
});
在前面的代码片段中,我们创建了 BookStoreController 对象,它只是一个包含将匹配路由器中定义的方法名称的函数的 JavaScript 对象。在这种情况下,空的路由器将调用 displayBooks 方法或控制器。这种关注点的分离将使我们能够拥有一个更干净的代码库,因为路由器只知道关于路由的信息。我们通过将路由器的控制器属性设置为 BookStoreController 来声明哪个控制器将处理路由。代码片段的其余部分只是路由的声明。
使用控制器并不强制要求有一个路由器。Marionette 控制器可以在不需要路由器的情况下实例化。您可能不会通过 URL 的变化来处理您网站的交互,而是通过事件。在这种情况下,控制器仍然具有价值,因为它可以成为您视图的容器。
建议根据您应用程序的目的将路由器和控制器分成小的部分,而不是一个包含所有路由和函数的巨大单一路由器文件。
虽然这两部分是应用程序基础的一部分,但我们仍然需要在其中使它们工作。但是,我们还需要做更多的事情才能实现一个功能性的应用程序。让我们一步一步来。让我们首先检查我们是否能在浏览器控制台中看到消息日志。
要做到这一点,我们需要将所有代码放在一起,并添加缺失的部分以使其工作。
到目前为止,我们只定义了应用程序、控制器和路由器。但我们应该在何处实例化它们?Backbone.Marionette.Application 对象提供了在启动我们的应用程序时运行初始化方法的可能。
是的,您读得正确!如果您想保持初始化器的逻辑分离,您可以添加任意多的方法。
在这个初始化方法内部,我们将实例化路由器和控制器,并且为了好玩,添加另一个日志消息以查看执行顺序。
使用以下代码来完成此操作:
BookStoreApp.addInitializer(function () {
var bookStoreController = new BookStoreController({
var bookStoreRouter = new BookStoreRouter({controller:controller});
console.log('Message from the addInitializer Method');
..});
})
应用程序的另一个有用功能是触发 initialize:before、initialize:after 和 start 函数的事件。这些函数的名称相当描述性。正如其名称所暗示的,initialize:before 函数将在初始化之前执行,initialize:after 函数将在初始化之后执行,而 start 函数负责启动应用程序,然后启动初始化器。
在我们的应用程序中,我们将使用 initialize:after。这个函数对我们很有帮助,因为我们最不想做的是在实例化路由器后启动 Backbone.history。
BookStoreApp.on('initialize:after', function () {
if (Backbone.history) {
Backbone.history.start();
}
console.log('Mesagge from initialize:after method');
});
完成我们的应用程序的基础设施或基础结构的最后一步是调用以下函数:
BookStoreApp.start();
现在,让我们将所有代码片段组合如下:
var BookStoreApp = new Backbone.Marionette.Application();
var BookStoreController = Backbone.Marionette.Controller.extend({
displayBooks : function (){
console.log("I will display books...");
}
});
var BookStoreRouter = Backbone.Marionette.AppRouter.extend({
controller : BookStoreController,
appRoutes: {
"": "displayBooks"
}
});
BookStoreApp.addInitializer(function () {
var controller = new BookStoreController();
var router = new BookStoreRouter({controller:controller});
console.log("hello from the addInitializer.");
});
BookStoreApp.on('initialize:after', function () {
if (Backbone.history) {
Backbone.history.start();}
console.log("hello from the initialize:after.");
});
BookStoreApp.start();
小贴士
下载示例代码
您可以从您在www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
现在,您可以继续在浏览器中打开Index.html文件,并在控制台查看结果。
摘要
在本章中,我们学习了应用、控制器和路由功能的应用,以及如何使它们协同工作以获得一个简单的应用程序骨架,这将是我们的书店应用程序的基础。
在下一章中,我们将熟悉 Marionette 为 Backbone 开发添加的不同视图。
第三章. 木偶视图类型及其用法
在上一章中,我们学习了帮助我们为应用程序提供结构的组件;然而,这些组件都没有与 DOM 交互。这个责任属于 Backbone 开发中的视图;然而,在视图内部,DOM 的交互和操作可能会迅速变得复杂。为了拥有更干净、更有意义的对象来操作,DOM Marionette 引入了一套强大的视图。以下是对官方文档中提供的每个视图的描述,官方文档位于github.com/marionettejs/backbone.marionette:
-
Marionette.ItemView:这是一个渲染单个模型的视图 -
Marionette.CollectionView:这是一个遍历集合并渲染每个模型的单个ItemView实例的视图 -
Marionette.CompositeView:这是一个用于渲染叶分支/组合模型层次结构的集合视图和项目视图 -
Marionette.Layout:这是一个渲染布局并创建区域管理器来管理其内部的区域的视图 -
Marionette.View:这是其他 Marionette 视图从中扩展的基础视图类型(不打算直接使用)
在本章中,我们将了解每个视图背后的意图以及如何开始使用它们。
Marionette.View 和 Marionette.ItemView
Marionette.View 扩展了 Backbone.View,这一点很重要,因为我们在创建视图方面已经掌握的所有知识,在处理 Marionette 的新视图集时都将非常有用。
每个视图都旨在提供特定的开箱即用功能,这样你就可以花更少的时间关注使事物工作所需的粘合代码,更多的时间关注与你的应用程序需求相关的事情。这让你可以集中所有注意力在应用程序的具体逻辑上。
我们将从描述 Marionette 的 Marionette.View 部分开始,因为所有其他视图都从此扩展;我们这样做的原因是,这个视图提供了一些非常有用的功能。但重要的是要注意,这个视图并不打算直接使用。作为所有其他视图从中继承的基础视图,它是一个包含我们刚刚提到的粘合代码的绝佳位置。
该功能的良好示例是 close 方法,它将负责从 DOM 中移除 .el。此方法还将负责调用解绑所有事件,从而避免称为僵尸视图的问题。这是一个如果你在常规 Backbone 视图中不小心这样做可能会遇到的问题,其中先前关闭的实例化会触发事件。这些事件现在绑定到了视图使用的 HTML 元素上。现在视图已重新渲染,在视图重新创建期间,新的事件监听器被附加到这些 HTML 元素上。
从 Marionette.View 的文档中,我们确切地知道 close 方法的作用。
-
如果提供了,它会在视图中调用一个
onBeforeClose事件 -
如果提供了,它会在视图中调用一个
onClose事件 -
它解绑了所有自定义视图事件
-
它解绑了所有 DOM 事件
-
它从 DOM 中移除
this.el -
它解绑了所有
listenTo事件
Marionette.View 对象的官方文档链接是 github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.view.md。
重要的一点是,第三点,解绑所有自定义视图事件,将解绑使用 modelEvents 哈希创建的事件、在事件哈希上创建的事件以及通过 this.listenTo 创建的事件。
由于 close 方法已经提供并实现,你不需要执行之前列出的解绑和移除任务。虽然大多数情况下这已经足够,但有时你的某个视图可能需要你执行额外的工作才能正确关闭它;在这种情况下,将同时触发两个事件来关闭视图。
如其名所示,onBeforeClose 事件将在 close 方法之前触发。它将调用同名的函数 onBeforeClose,我们可以在该点添加需要执行的代码。
function : onBeforeClose () {
// code to be run before closing the view
}
第二个事件将是 onClose,它将在 close 方法之后触发,这样视图的 .el 就不再存在,并且所有解绑任务都已执行。
function : onClose () {
// code to be run after closing the view
}
Marionette 背后的一个核心思想是减少你在使用 Backbone 构建应用程序时需要编写的样板代码。一个很好的例子是每个 Backbone 视图中都必须实现的 render 方法,那里的代码在每一个视图中几乎都是相同的。使用 underscore 的 _.template 函数加载模板,然后将转换为 JSON 的模型传递给模板。
以下是一个在 Backbone 中渲染视图所需的重复代码示例:
render : function () {
var template = $( '#mytemplate' ).html();
var templateFunction = _.template( template );
var modelToJSON = this.model.toJSON();
var result = templateFunction(modelToJSON);
var myElement = $( '#MyElement' );
myElement.html( result );
}
由于 Marionette 不再需要定义 render 函数,就像 close 方法一样,前面的代码将在幕后为你调用。为了渲染一个视图,我们只需要声明它,并设置模板属性。
var SampleView = Backbone.Marionette.ItemView.extend({
template : '#sample-template'
});
接下来,我们只需创建一个 Backbone 模型,并将其传递给ItemView构造函数。
var SampleModel = Backbone.Model.extend({
defaults : {
value1 : "A random Value",
value2 : "Another Random Value"
}
})
var sampleModel = new SampleModel();
var sampleView = new SampleView({model:sampleModel);
然后剩下的唯一事情就是调用render函数。
sampleView.render();
注意
如果你想看到它运行,请通过以下 JSFiddle 查看之前的代码:
需要注意的是,我们只需一行代码来指定模板,Marionette 就会通过使用指定的模板渲染我们的视图来完成剩余的工作。注意,在这个例子中,我们使用了ItemView构造函数;我们不应该直接使用Marionette.View,因为它本身没有很多功能。它只是作为其他视图的基础。
因此,以下将使用ItemView演示Marionette.View提供的部分功能,因为这个视图通过扩展继承了所有这些功能。
正如我们在前面的例子中看到的,ItemView在用模板渲染单个模型时工作得很好,但渲染模型集合怎么办?
如果你只需要渲染,例如,书籍或分类列表,你仍然可以使用ItemView。为了完成这个任务,分配给ItemView的模板必须知道如何处理 DOM 的创建,以正确显示该列表项。
让我们渲染一本书籍列表。
Backbone 模型将有两个属性:书籍名称和书籍 ID。我们只想创建一个使用书籍名称作为显示值的链接列表;书籍的 ID 将用于创建查看特定书籍的链接。
首先,让我们为这个例子创建书籍 Backbone 模型及其集合:
var BookModel = Backbone.Model.extend({
defaults : {
id : "1",
name : "First",
}
});
var BookCollection = Backbone.Collection.extend({
model : BookModel
});
现在,让我们实例化集合并向其中添加三个模型:
var bookModel = new BookModel();
var bookModel2 = new BookModel({id:"2",name:"second"});
var bookModel3 = new BookModel({id:"3",name:"third"});
var bookCollection = new BookCollection();
bookCollection.add(bookModel);
bookCollection.add(bookModel2);
bookCollection.add(bookModel3);
在我们的 HTML 中,让我们创建一个用于此视图的模板;模板应如下所示:
<script id="books-template" type="text/html">
<ul>
<% _.each(items, function(item){ %>
<li><a href="book/'+<%= item.id %> +"><%= item.name %> </li>
<% }); %>
</ul>
</script>
现在,我们可以使用以下代码片段来渲染书籍列表:
var BookListView = Marionette.ItemView.extend({
template: "#books-template"
});
var view = new BookListView ({
collection: bookCollection
});
view.Render();
注意
如果你想看到它的实际效果,请访问 JSFiddle 中的工作代码,链接为jsfiddle.net/rayweb_on/8QAgQ/。
之前的代码将生成一个包含特定书籍链接的无序列表。再次强调,我们再次获得了编写少量代码的好处,因为我们不需要指定Render函数,这可能会产生误导,因为ItemView完全能够渲染模型或集合。是否使用CollectionView或ItemView将取决于我们想要实现的目标。如果我们需要一组具有自身功能的独立视图,CollectionView是正确的选择,正如我们在回顾它时将看到的。但如果我们只需要渲染集合的值,ItemView将是完美的选择。
在视图中处理事件
为了跟踪模型事件或集合事件,我们必须在常规 Backbone 视图中编写以下代码片段:
this.listenTo(this.model, "change:title", this.titleChanged);
this.listenTo(this.collection, "add", this.collectionChanged);
要启动这些事件,我们使用以下处理函数:
titleChanged: function(model, value){alert("changed");},
collectionChanged: function(model, value){alert("added");},
这在 Marionette 中仍然有效,但我们可以通过以下配置哈希来声明这些事件,以实现相同的功能:
modelEvents: {
"change:title": "titleChanged"
},
collectionEvents: {
"add": "collectionChanged"
},
这将给出完全相同的结果,但配置哈希非常方便,因为我们可以在我们的模型或集合中继续添加事件,代码更简洁,更容易理解。
modelEvents 和 collectionEvents 不是我们在每个 Marionette 视图中可用的唯一配置哈希集;UI 配置哈希也是可用的。可能存在这样的情况,即您的视图中的一个 DOM 元素将被多次使用以读取其值,并且使用 jQuery 来做这件事在性能方面可能不是最优的。此外,我们将在几个地方重复 jQuery 引用,这会使我们的代码更冗余。
在一个 Backbone 视图中,我们可以定义一组事件,一旦在 DOM 中执行操作就会触发;例如,我们传递一个函数来处理按钮点击事件。
events : {
"click #button2" : "updateValue"
},
这将在我们点击 button2 时调用 updateValue 函数。这没问题,但如果我们想调用不在视图内的方法怎么办?
为了实现这一点,Marionette 提供了 triggers 功能,它将触发可以在视图外部监听的事件。要声明一个 trigger,我们可以使用与 events 对象相同的语法,如下所示:
triggers : { "click #button1": "trigger:alert"},
然后,我们可以使用以下代码在别处监听该事件:
sampleView.on("trigger:alert", function(args){
alert(args.model.get("value2"));
});
在前面的代码中,我们使用了模型来警告并显示属性 value2 的值。
函数接收到的 args 参数将包含您可以使用的对象:
-
触发事件的视图
-
该视图的 Backbone 模型或集合
UI 和模板
当与视图一起工作时,您可能需要在视图的多个地方通过 jQuery 引用一个特定的 HTML 元素。这意味着您将在初始化和视图的几个其他方法中引用一个按钮。为了避免在每个这些方法中重复 jQuery 选择器,您可以在哈希中映射该 UI 元素,以便保留选择器。如果您需要更改它,更改将在一个地方完成。
要创建 UI 元素的映射,我们需要添加以下声明:
ui: {
quantity: "#quantity"
saveButton : "#Save"
},
要使用这些映射 UI 元素,我们只需在配置中给出的名称下,在任意函数中引用它们。
validateQuantity: function() {
if (this.ui.quantity.val() > 0 {
this.ui.saveButton.addClass('active');
}
}
有时候您需要向视图传递不同的模板。在 Marionette 中,我们移除模板声明,而是添加一个名为 getTemplate 的函数。
以下代码片段将说明该函数的使用:
getTemplate: function(){
if (this.model.get("foo"){
return "#sample-template";
}else {
return "#a-different-template";
}
},
在这种情况下,我们检查属性 foo 的存在性;如果不存在,我们使用不同的模板,这就足够了。您不需要指定 render 函数,因为它将以与在先前的示例中声明模板变量相同的方式工作。
如果你想要了解更多关于我们迄今为止讨论的所有概念,请参考 JSFiddle 链接jsfiddle.net/rayweb_on/NaHQS/。
如果你发现自己需要在渲染值时进行涉及复杂过程的计算,你可以使用包含在名为templateHelpers的对象中的templeteHelpers函数。让我们看看一个更好的例子来展示它的使用。
假设我们需要显示书籍的值,但提供了一个需要计算的折扣,可以使用以下代码:
var PriceView = Backbone.Marionette.ItemView.extend({
template: "#price-template",
templateHelpers: {
calculatePrice: function(){
// logic to calculate the price goes here
return price;
}
}
});
如前述代码所示,我们声明了一个包含可以从中调用的函数的对象literal。
<script id="my-template" type="text/html">
Take this book with you for just : <%= calculatePrice () %>
</script>
Marionette.CollectionView
在一个视图中渲染类似书籍的列表是可能的,但我们希望能够与每个项目进行交互。这个解决方案将是使用循环逐个创建视图。但是 Marionette 通过引入CollectionView的概念以非常优雅的方式解决了这个问题,该概念将为我们要显示的集合中的每个元素渲染一个子视图。
一个很好的实践例子可能是按类别列出书籍并创建一个集合视图。这非常简单。
首先,你需要定义每个项目应该如何显示;这意味着每个项目将如何在视图中转换。
对于我们的类别示例,我们希望每个项目都是一个列表<li>元素,并且是集合的一部分;<ul>列表将包含每个类别视图。
我们首先声明ItemView如下:
var CategoryView = Backbone.Marionette.ItemView.extend({
tagName : 'li',
template: "#categoryTemplate",
});
然后我们声明CollectionView,它指定了要使用的视图项。
var CategoriesView = Backbone.Marionette.CollectionView.extend({
tagName : 'ul',
className : 'unstyled',
itemView: CategoryView
});
一个值得注意的好事是,即使我们使用 Marionette 视图,我们仍然能够使用 Backbone 视图提供的标准属性,例如tagName和ClassName。
最后,我们创建一个集合,并通过传递集合作为参数来实例化CollectionView。
var categoriesView = new CategoriesView({collection:categories);
categoriesView.render();
就这样。简单吧?
使用这个视图的优势在于它将为每个项目渲染一个视图,并且它可以有很多功能;我们可以在作为容器的CollectionView中控制所有这些视图。
你可以在jsfiddle.net/rayweb_on/7usdJ/中看到它的实际应用。
Marionette.CompositeView
Marionette.Composite视图不仅提供了渲染模型或集合模型的可能性,还提供了同时渲染模型和集合的可能性。这就是为什么这个视图非常适合我们的 BookStore 网站。我们将向购物车添加单个项目,在这种情况下是书籍,并将这些书籍存储在集合中。但是,我们需要计算订单的小计,显示计算出的税费和订单总额;所有这些属性都将是我们将要显示的totals模型的一部分,我们将与订购的书籍一起显示。
但存在一个问题。当没有添加项目时,我们应该在顺序区域显示什么?好吧,在 CompositeView 和 CollectionView 中,我们可以设置一个 emptyView 属性,这个属性将是一个在集合中没有模型时要显示的视图。一旦我们添加了一个模型,我们就可以渲染项目以及 totals 模型。
可能在这个时候,你可能认为你失去了对渲染功能的控制,并且会有一些需要你进行修改 HTML 的场景。好吧,在这种情况下,你应该使用 onRender() 函数,这是一个非常有用的方法,它将允许你在 render 方法被调用后立即操作 DOM。
最后,我们希望设置一个带有一些标题的模板。这些标题不是 ItemView 的一部分,那么我们如何显示它?
让我们看看代码片段的一部分,解释每个部分是如何解决我们的需求的。
var OrderListView = Backbone.Marionette.CompositeView.extend({
tagName: "table",
template: "#orderGrid",
itemView: CartApp.OrderItemView,
emptyView: CartApp.EmptyOrderView,
className: "table table-hover table-condensed",
appendHtml: function (collectionView, itemView) {
collectionView.$("tbody").append(itemView.el);
},
到目前为止,我们已经定义了视图并设置了模板;Itemview 和 EmptyView 属性将被用来渲染我们的视图。
onBeforeRender 是一个函数,正如其名所示,它将在 render 方法之前被调用;这个函数将允许我们计算将在 total 模型中显示的总数。
onBeforeRender: function () {
var subtotal = this.collection.getTotal();
var tax = subtotal * .08;
var total = subtotal + tax;
this.model.set({ subtotal: subtotal });
this.model.set({ tax: tax });
this.model.set({ total: total });
},
onRender 方法在这里用来检查集合中是否有模型(即用户没有将书籍添加到购物车)。如果没有,我们不应该显示视图的标题和页脚区域。
onRender: function () {
if (this.collection.length > 0) {
this.$('thead').removeClass('hide');
this.$('tfoot').removeClass('hide');
}
},
如我们所见,Marionette 提供了非常出色的函数,可以移除大量的样板代码,并让我们完全控制渲染的内容。
使用 Marionette.Layout 构建我们应用程序的布局
我们需要最后审查的最终视图是 Marionette.Layout 视图。这个视图是 Itemview 和 Region 的组合;我们还没有审查 Marionette.Region 组件,但到目前为止,只需要说它是一个负责在其 el 上渲染视图的组件。
因此,布局作为一个 ItemView 工作,因为它需要一个模板来自我渲染。这个模板可以是你的初始 HTML,通过逻辑区域划分,例如包含显示网站导航部分的导航区域,应该显示在页脚区域的页脚视图,等等。你可以先渲染布局,然后在每个区域上正确渲染视图。
让我们创建 Marionette.Layout 视图。
varCatalogLayout = Backbone.Marionette.Layout.extend({
template: "#CatalogLayout",
regions: {
categories : '#categories',
products : '#products',
order : '#order',
book: '#book'
}
});
在你复制的 第二章 的 HTML 中,你会找到对应区域的 <div> 标签。
在这个视图中,我们指定了视图将使用的脚本/模板来渲染。这个指定的模板被添加到初始 HTML 中,并且其中包含了 <div> 标签,这些标签将作为区域。每个区域都有一个与将要显示的视图相匹配的名称,我们使用一个对象 literal 来定义区域。
Layout 视图继承了所有其他视图的功能,因此如果您想监听事件,可以像在其他任何视图中一样进行操作。
要渲染这个初始布局,我们只需实例化它并将其渲染成任何其他视图。
var catalogLayout = new CatalogLayout();
catalogLaout.render();
您仍然可以通过调用 addRegion 和 removeRegion 方法在运行时向您的布局添加和删除区域。
layout.addRegion("footer", "#footer");
layout.removeRegion("footer ");
要添加多个区域,Layout 视图提供了一个 addRegions 方法,该方法接收一个包含要添加的区域的对象 literal。
layout.addRegions({
favoriteBooks: "#favoritebooks",
bestRated: "#best"
});
这个视图上的 Close 函数的行为将略有不同,因为它将调用所有区域的 close 方法。然后,这些区域将调用它们包含的视图上的 close 方法,确保所有包含的视图都正确关闭。
一个开始您应用程序的好方法是定义一个 Body 区域;这个区域将包含所有逻辑区域的应用程序 Layout。也许您需要在这些区域中的一个显示子布局,这是完全可以的。嵌套布局没有限制;根据您应用程序的需求使用它们。
扩展 Marionette 视图
在使用 Backbone 和 Marionette 以及几乎任何语言时,一个常见的需求是尽可能多地重用代码。如果您想让所有视图以某种方式表现,您可以通过扩展您的 Marionette 视图来实现。在下面的示例中,我们将通过扩展 Marionette.ItemView 来为所有项目视图添加一个 log 方法。
var HandyView = Backbone.Marionette.ItemView.extend({
initialize:function(){
Backbone.Marionette.ItemView.prototype.initialize.apply(this,arguments);
},
logMessage : function (message){
console.log(message);
}
});
现在,您只需开始使用您的 HandyView,以便获得 logMessage 函数的好处。
var BookView = HandyView.extend({
alertMessage : function () {
alert(message);
}
});
var bookView = new BookView();
bookView.logMessage("Hi");
bookView.alertMessage("Bye");
这里的想法是让您知道,您可以像扩展 Backbone 视图一样扩展 Marionette 视图,并利用继承的好处。
摘要
在本章中,我们学习了 Marionette 提供的所有类型的视图,何时使用它们,如何有效地利用其便捷的方法来更好地管理 DOM 创建和交互,以及最后如何扩展它们。
在下一章中,我们将学习如何借助 Marionette 的 Regions、RegionManager 和 BabySitter 对象来管理一组视图。
第四章。管理视图
如我们在第三章中学习的那样,Marionette 视图类型及其使用,Marionette.js 视图为我们提供了许多功能,以极少的代码编写量渲染数据。在本章中,我们将探讨 Marionette 中的 Region 是什么,以及 RegionManager 和 BabySitter 对象是什么。所有这些都是为了帮助我们以更简单的方式管理视图。
我们还将了解在应用程序中渲染模板时的一个实用对象:Renderer 对象。之后,我们将简要总结前四章所学的内容。
本章我们将涵盖以下主题:
-
Marionette.Region -
Marionette.RegionManager -
Marionette.BabySitter -
Marionette.Renderer -
使用
Marionette.TemplateCache提高应用程序的性能
所有这些都非常有用的对象,将帮助我们轻松管理 Marionette 视图,同时考虑到性能和重用。
理解 Marionette.Region 对象
在构建应用程序时,我们需要将屏幕划分为小的、逻辑的部分,如页眉、页脚、导航和内容区域。这些是大多数应用程序中常见的部分。通常,您的导航选项可能会根据用户而变化。页眉也可能根据您的用户配置文件而不同,当然,您的内容区域将忙于显示不同的视图——需要渲染以执行操作并关闭以显示新视图的视图,可能还有一些结果或应用程序中的下一个逻辑步骤。这就是为什么我们应该将应用程序的页脚或内容部分视为应用程序内的区域,在那里我们将交换不同的视图。
以下代码演示了创建 Marionette.Region 对象的一种方法:
var FooterRegion = new Backbone.Marionette.Region({
el: "#footer"
});
要定义一个区域,我们只需指定一个 DOM 元素,该元素将作为应用程序逻辑部分中视图的容器。在这种情况下,#footer 是一个 DIV 元素,但可以是任何 HTML 元素,只要在其中添加视图可以生成有效的 HTML。
区域背后的想法是将其用作应用程序中视图的容器,一次一个。当我们调用区域的 .show 方法时,它将负责调用指定视图的 render 函数。当我们调用区域的 .close 方法时,它将调用当前视图的关闭方法并将其从 DOM 中移除。
以下是需要使用区域来渲染视图的代码:
// definition of a view to be shown in the region
var footerView = new FooterView();
//the Region will show the footerview in its DOM element
//in this case it will render the footer view inside the #footer element in the DOM
FooterRegion.show(footerView);
// the footerView is now rendered
//Finally we can call close on the region and the footer view will be removed from the #footer element
FooterRegion.close();
如前述代码片段所示,区域的 show 方法将接受要渲染的视图实例作为参数。
在一开始,渲染视图然后交换为新视图似乎很简单。我们只需调用close和render,对吧?那么为什么我们需要一个区域对象来为我们做这件事呢?这是因为区域为我们做这些事情,而且更多,我们不需要担心当前显示在其中的视图是哪一个。可以这样想:你有内容区域,你将在其中显示一个视图。我们只需通过调用.show方法来替换这个视图,区域将负责移除第一个视图。所以我们不需要调用它的close方法,因为这是区域.show方法功能的一部分。这意味着如果区域中已经显示了一个视图,通过调用show并传递一个新的视图来调用,将调用现有视图的close方法,从而确保其.el事件绑定被正确移除。
让我们以一个巫师为例。我们分为四个步骤,在每一步中,我们展示一个视图,我们将在这里填写一些数据。在这些视图中的每一个,我们都有链接,这些链接将引导我们进入下一步或回到前一步,以防我们想要修改数据。这些链接将修改 URL,这将由路由器负责调用适当的步骤。
对于这个例子,控制器内部的代码如下所示:
// each view step syncs with the server at the time to initialize and close in order to preserve the data
stepOne : function () {
var stepOneView = new StepOneView();
content.region.show(stepOneView);
},
stepTwo : function () {
var stepTwoView = new StepTwoView();
content.region.show(stepTwoView);
}
stepThree : function () {
ar stepThreeView = new StepThreeView();
content.region.show(stepThreeView);
}
finalStep : function () {
var finalStepView = new FinalStepView();
content.region.show(finalStepView);
}
从前面的代码中,我们可以看到使用区域渲染视图的好处。如果用户点击第二个步骤并决定返回,我们不需要检查stepTwoView方法的实例是否在内存中,并关闭它以渲染stepOneView方法,因为这将由区域处理。
将会有一些情况,我们无法跟踪区域中存在哪个视图,在大多数这些情况下,我们并不关心。我们只需要在这个区域上渲染一个新的视图,而无需担心之前的视图是否被正确移除。
另一种声明区域的方法是将它直接附加到 Marionette 应用程序上,如下所示:
BookStoreApp = new Backbone.Marionette.Application();
BookStoreApp.addRegions({
contentRegion: "#mainContent",
});
在这种情况下,我们使用了应用程序对象的.addRegions方法,它期望一个包含区域名称和要使用的 DOM 元素的字面量对象。
要使用这些新的区域,我们只需通过在对象字面量中给出的名称来调用它们,如下所示:
BookStoreApp.contentRegion.show(stepOneView);
在第三章中,我们定义了一个布局。"Marionette 视图类型及其使用",Marionette 的布局视图充当区域或容器。布局将渲染一个包含 HTML 骨架的模板。在这个骨架内部,我们将放置作为区域的 DIV 元素或元素,一旦这个布局被渲染,我们就可以使用它的区域来显示视图。
因此,让我们按照以下方式回顾代码:
CatalogLayout = Backbone.Marionette.Layout.extend({
template: "#CatalogLayout",
regions: {
categoriesRegion : '#categories',
productsRegion : '#products',
orderRegion : '#order',
bookRegion: '#book'
}
});
在布局声明中,我们定义了一个模板和区域对象字面量,为区域命名,并将它们与 DOM 元素匹配。
对于这个BookStoreApp,我们将创建mainRegion。这个区域的责任是渲染应用程序的布局视图。布局视图将包含初始 HTML 文件和应用程序的逻辑区域,这些区域将显示适当的视图。以下代码示例说明了创建应用程序对象、布局视图以及在布局区域中渲染视图:
BookStoreApp = new Backbone.Marionette.Application();
BookStoreApp.addRegions({
mainRegion: "#mainContent",
});
CatalogLayout = Backbone.Marionette.Layout.extend({
template: "#CatalogLayout",
regions: {
categoriesRegion : '#categories',
productsRegion : '#products',
orderRegion : '#order',
bookRegion: '#book'
}
});
var catalogLayput = new CatalogLayout();
BookStoreApp.mainRegion.show(catalogLayput );
catalogLayput.categoriesRegion.show(new CategoriesView());
catalogLayput.productsRegion.show(new ProductsView());
在布局和区域的帮助下,我们可以创建屏幕的逻辑分割,这将允许我们在每个区域上渲染视图。为这些区域使用有意义的名称将肯定有所帮助,因为我们只需要将正确的视图传递给区域,并停止担心渲染和清理所需的粘合代码。
在渲染视图时,区域将引发以下两个事件,这将帮助我们执行对 DOM 的额外操作:
-
"show"/onShow: 当视图渲染并显示时,此事件在视图实例上被调用 -
"show"/onShow: 当视图渲染并显示时,此事件在区域实例上被调用
最后,在关闭视图时,以下方法将被触发,可以用来执行一些最终任务,例如用友好的消息通知用户:
"close"/onClose: 当视图被关闭时,此方法被调用
您可以使用.on方法声明像往常一样订阅这些事件,如下所示:
BookStoreApp.mainRegion.on("show", function(view){
// extra functionality needed to be added once the view is rendered
});
以下代码示例说明了如何订阅close方法:
BookStoreApp.mainRegion.on("close", function(view){
// code to notify that the view as been removed
});
使用 Marionette.RegionManager 对象
使用区域可以帮助以非常优雅的方式管理视图。但对于某些应用程序来说,这可能还不够,这些应用程序在其生命周期中可能需要添加和删除数十个区域。为了完成这项管理,我们可以利用 Marionette 的RegionManager对象,它将作为区域的容器。
将区域放在这个容器中可以帮助我们完成几乎与使用Backbone.Collection对象相同的行为,借助 underscore 方法如each、map、invoke、contains和toArray。
以下语法可以帮助我们声明Marionette.RegionManager:
var regionManager = new Marionette.RegionManager();
以下语法演示了如何将区域添加到regionManager。addRegion方法接受两个参数。第一个将是区域的 ID 或别名,第二个将是用于的 DOM 元素。
regionManager.addRegion("math", "#math");
如果我们想添加多个区域,我们需要使用addRegions方法,并将带有名称和 ID 的对象字面量作为参数传递给此方法。
regionManager.addRegions({
art: "#art",
music: "#music",
science: "#science"
});
我们可以使用removeRegion方法从regionManager中删除区域,并关闭区域,这将调用包含视图的close,从而从应用程序中删除此视图。
regionManager.removeRegion("math");
要移除所有区域,我们可以使用以下代码中所示的 removeRegions 方法:
regionManager.removeRegions();
我们可以继续向这个容器添加更多区域。但现在让我们创建一个 RegionManager 对象,用于一个包含子类别区域的类别;每个区域将显示一个具有相同子类别书籍的 CollectionView 对象。因此,我们在具有相同名称的区域中显示一个 CollectionView 对象。
这个想法是用户希望查看他最喜欢的类别的书籍,并且还希望向屏幕上添加更多要渲染的子类别。例如,在历史类别中,我们可以有一个选项来查看现代历史、罗马、世界历史、第二次世界大战等更多子类别的书籍。
var historyManager = new Marionette.RegionManager();
historyManager.addRegions({
wwIIRegion: "#wwII",
romeRegion: "#rome",
modernRegion: "#modern",
unitedStatesHistoryRegion : "#us"
});
然后在每个区域上渲染适当的视图,如下所示:
historyManager.modernRegion.show(new BooksView({collection : modern}));
historyManager.wwIIRegion.show(new BooksView({collection :wwIIBooks}));
如果在某个时刻,用户切换到另一个类别并且不再想看到历史类别,调用 removeRegions 将级联到视图级别,从而以安全的方式移除所有视图。
使用 Backbone.BabySitter 对象
他们提供的值使得两个对象从 Marionette 库中脱颖而出,并且可以通过将它们包含在您的脚本中或使用 Marionette 的混合版本来单独使用。一个是 Backbone.Wreqr.EventAggregator 对象,另一个是 Backbone.BabySitter 对象,这是我们即将讨论的对象。
这个对象帮助我们跟踪视图并管理它们。这些视图可以包含在另一个视图或另一个需要跟踪这些视图的对象中。
BabySitter 对象可以用来包含视图而不是区域。RegionManager 的责任是在您的应用程序中包含和执行区域内的操作。BabySitter 对象有相同的责任,但在这个情况下,它帮助我们管理相关的视图。
以下代码是一个 BabySitter 对象实例化的示例:
container = new Backbone.ChildViewContainer();
您可以开始向这个容器添加和移除视图,因为它是一个常规的 Backbone.Collection 对象;您可以通过使用添加和移除函数来完成此操作。
container.add(someView);
container.add(anotherView);
container.remove(someView);
容器提供了许多有用的方法,例如长度,它将返回容器跟踪的视图数量。
var numberofViews =container.length
正如我们之前提到的,容器公开了几个 underscore 集合函数的功能,例如 each、map、find、select、filter、all、some 和 toArray。
为了演示我们可以在容器内部调用每个视图的方法,我们只需要编写以下代码:
container.each(function(view){
view.changeColor();
});
前面函数的责任是迭代容器中的所有视图,并在该视图中调用一个函数,在这种情况下,是每个视图的 changeColor 函数。
我们创建了一个 JSFiddle 来演示 BabySitter 对象的使用。您可以在 jsfiddle.net/rayweb_on/fxzAs/ 找到它。
注意
jsfiddle.net是一个非常有用的网站,用于共享代码片段,促进开发者之间的协作。
利用 Marionette.Renderer 对象
正如我们在第三章中看到的,“Marionette 视图类型及其用途”,Marionette 出色地完成了删除渲染视图所需重复代码的工作。但在幕后,Marionette 所做的是将此任务委托给Renderer对象。其责任是加载和编译模板到.template函数,并传递用于正确渲染视图模型的数据。这意味着 Marionette 中的每个视图对象都使用Renderer对象。此对象还可以通过仅传递一个带有用于此目的的数据的模板来帮助将 HTML 渲染到 DOM 中。
假设由于某种原因我们需要将 HTML 附加到我们的视图中,但我们不想调用我们正在处理的视图的渲染。在这种情况下,我们可以使用Marionette.Renderer对象。
callRenderer : function () {
var template = "#sample-template2";
var data = {foo: "I was appended with Marionette Renderer"};
var html = Backbone.Marionette.Renderer.render(template, data);
this.$el.append(html);
}
在前面的函数中,我们声明了一个在 DOM 中的模板,然后创建了一个包含虚拟数据的对象字面量,然后将这两个传递给Renderer对象,它返回一个 HTML,我们最终将其附加到视图的$el上。
要查看此示例的运行效果,您可以访问jsfiddle.net/rayweb_on/BeMfz/ JSFiddle 链接。
但也许更好的用途是渲染将作为区域容器的 DOM 元素。
如果我们需要渲染子类别,我们需要确保 DIVs(<div id="rome"><div>)可用,以便附加区域,然后调用show来渲染这些容器(DIV 元素)内的视图。
使用 TemplateCache 提高应用程序的性能
在每个应用程序中,性能都很重要。这就是为什么我们的模板在缓存中可用将肯定提高未来调用中渲染过程的速度。
Marionette 有一个名为TemplateCache的对象,该对象由Renderer对象使用。这意味着所有模板都存储在这个TemplateCache对象中,要开始使用它,我们只需调用get方法。内部,此方法将确认是否已有模板,然后返回它;否则,它将从 DOM 中加载模板,并返回模板以便我们可以使用,同时也会保留其引用。因此,在后续调用中,我们将通过以下代码获取模板的缓存版本:
var template = Backbone.Marionette.TemplateCache.get("#my-template");
要从我们的缓存中删除一个或多个模板,我们需要调用clear函数,如下所示:
Backbone.Marionette.TemplateCache.clear("#my-template")
如果我们需要删除多个模板,我们可以传递要删除的模板列表,或者简单地调用不带参数的clear()函数来删除所有缓存的模板,如下所示:
Backbone.Marionette.TemplateCache.clear("#my-template", "#this0-template")
或者,我们可以使用以下代码来完成此操作:
Backbone.Marionette.TemplateCache.clear()
默认情况下,Marionette 的TemplateCache与 underscore 模板一起工作,但为了覆盖这一点,我们需要提供我们自己的compileTemplate函数定义。
为了保持一致性,我们将覆盖这个函数以返回 handlebars 模板。
Backbone.Marionette.TemplateCache.prototype.compileTemplate = function(rawTemplate) {
return Handlebars.compile(rawTemplate);
}
注意
处理 Handlebars 是一个非常流行的模板引擎,通常用作 underscore 模板的替代品。你可以在其网站上了解更多信息,handlebarsjs.com/。
正如我们所见,利用TemplateCache非常简单,它提供的优势无疑是其最大的卖点。
摘要
在本章中,我们学习了 Marionette 提供的一些不同对象来管理视图,例如Region和BabySitter对象。这种管理确实是必要的,但实现它需要大量的粘合代码。因此,在构建应用程序时将其排除在外是一个很好的理由来开始使用这些对象。
在下一章中,我们将学习如何将我们的应用程序模块化成小的子应用程序模块,以便将网站的不同功能隔离开来,但仍然可以协同工作。
第五章。分而治之——模块化一切
在第四章中详细解释了如何实现Marionette.js中的区域来管理视图后,现在是时候了解如何处理复杂的 JavaScript 项目,并学习如何创建一个在子应用中可扩展且应需要最小努力来扩展的框架。
以下列表包含我们将在本章中涵盖的主要主题,以及我们在使用Marionette.js构建模块化和可扩展的单页应用时应考虑的主题:
-
分而治之
-
模块
-
子应用
-
内存管理
应用分而治之原则
复杂性是软件的敌人,构建复杂的 JavaScript 应用很容易失控。有多种处理复杂性的方法,但最有效的方法是使用分而治之的原则。
通过其模块定义,Marionette.js允许我们将代码拆分成更小、责任更单一的块。如果我们不将代码拆分成更小的部分,我们将减慢开发速度,并使我们的应用难以维护。结构化代码的最简单起点是Marionette.Application。应用的主要职责是启动和停止子应用,并在必要时调解跨子应用通信。以下图像显示了我们可以从应用对象开始,如何将我们的解决方案模块化到子应用和模块中:

对于我们正在构建的单页应用(SPA)示例,我们可能从一开始就不需要很多子应用。但真正重要的是要知道如何使用这个强大的功能,它有助于将应用拆分成更小、责任单一的单元。子应用模块是我们应用中的独立部分,它们可以包括路由器、控制器、模型、布局和视图。
所有模块都可以按需加载,因此它们不需要从一开始就创建。例如,我们可以在子应用路由匹配特定模式时启动它们。
模块化单页应用
设计单页应用程序的基础架构并非易事。SPAs 与经常有完整页面重新加载的传统 Web 应用程序相反。它们是运行在一个页面上的动态页面应用程序,通常需要花费一些时间来设计基础。由于我们在客户端存储应用程序状态,因此它们更像桌面应用程序,但管理它很快就会成为一个问题。正如我们从分而治之原则中学到的,一个问题可以被分成几个部分,这样每个部分就可以独立处理。话虽如此,让我们探索如何实现一个应用程序,该应用程序将根据需要加载具有单个职责的子应用程序,每个子应用程序都有停止和启动模块的能力。
从模块开始
| "风格、和谐、优雅和良好节奏之美取决于简单。" | ||
|---|---|---|
| -- 柏拉图 |
通过理解分而治之的概念,我们应该同意代码模块化非常重要。模块是小型、简单且封装良好的包,具有单一的关注点以及定义良好的函数;它们可以与其他模块结合使用,以创建整个系统。在Marionette.js中,一个模块提供高级功能,并管理提供实现细节的对象。
让我们定义一个没有功能的模块,以便继续从书店的例子中进行示例,我们将创建包含购物车和订单历史子应用程序的模块:
var MyApp = new Backbone.Marionette.Application();
var myModule = MyApp.module("MyModule");
Marionette.js的模块在app.start()调用之后加载和定义,并且它们是完全实例化的对象。正如你所看到的,Marionette 的模块悬挂在我们的应用程序上。现在让我们定义一个真实的模块定义:
Books.module('HistoryApp', {
startWithParent: false,
define:
function (HistoryApp, App, Backbone, Marionette,$, _) {
}
});
以下是对前面代码片段的解释:
-
Books: 这是主要的应用程序对象。 -
HistoryApp: 这是命名模块。 -
startWithParent: 如果我们希望手动启动模块而不是让应用程序启动它,则此值应为false。我们必须告诉模块定义不要与父对象一起启动,这正是我们的场景,因为我们不希望从开始就启动所有子应用程序。这个概念将在本章的与子应用程序一起工作部分详细解释。
函数参数的解释如下:
-
App: 这是管理模块生命周期的应用程序中心对象 -
Backbone: 这是Backbone库的引用 -
Marionette: 这是Backbone.Marionette库的引用 -
$: 这是 DOM 库的引用,在这种情况下是 jQuery。 -
_: 这是下划线的引用
除了解释过的参数外,你还可以向这个函数定义传递自定义参数。现在我们有一个非常简单的模块,准备封装一些所需的功能。
将模块拆分为多个文件
有时候,一个模块太长,不适合放在单个文件中,我们希望将定义分散到多个文件中。但是,子应用模块通常包含控制器、路由器和视图等,所以我们不想将它们全部放在一个文件中。Marionette.js模块使这一点变得非常简单,所以让我们看看。
以下是一个控制器文件的示例代码:
Books.module('HistoryApp', function (HistoryApp, App) {
HistoryApp.Controller = Marionette.Controller.extend({
});
});
以下是一个路由器文件的示例代码:
Books.module('HistoryApp', {
startWithParent: false,
define:
function (HistoryApp, App, Backbone, Marionette, $, _) {
var Router = Backbone.Router.extend({
});
}
});
我们创建了两个文件,一个用于控制器,另一个用于路由器,这两个文件都包含在同一个模块 HistoryApp 中,但位于不同的文件中。
实现初始化器和终结器
模块具有类似于应用对象的初始化器和终结器。初始化器在模块启动时运行,终结器在模块停止时运行。
让我们在现有的模块中添加一个初始化器和终结器:
Books.module('HistoryApp', function (HistoryApp, App) {
'use strict';
HistoryApp.Controller = Marionette.Controller.extend({
});
HistoryApp.addInitializer(function (args) {
HistoryApp.controller = new HistoryApp.Controller();
});
HistoryApp.addFinalizer(function () {
if (HistoryApp.controller) {
HistoryApp.controller.close();
delete HistoryApp.controller;
}
});
});
这个例子展示了我们如何在模块内部创建定义。在这种情况下,我们添加了一个控制器,实际上并没有创建任何对象——只是定义——然后我们让初始化器在模块加载时创建对象并设置它们。
与子应用一起工作
我们这本书的示例应用是一个可以包含几个较小应用的单个应用,例如购物车和订单历史。每个应用都是独立的,但由同一个应用管理,并在必要时能够与其他模块交互。下图中描述了由中央应用管理的两个子应用的概念。

每个子应用通常与 SPA 中的一个屏幕相关联。它们负责使用控制器来执行屏幕更改所需的工作,控制器负责启动和停止模块以及处理它们的通信。它们还管理布局,操纵区域以显示或隐藏视图。查看与图表相关的代码。
现在我们来探索如何定义两个子应用,每个子应用都位于不同的文件中,正如我们在上一节中学到的。
以下是我们第一个应用:
Books.module('HistoryApp', function (HistoryApp, App) {
'use strict';
HistoryApp.Controller = Marionette.Controller.extend({
});
});
我们的第二个应用如下:
Books.module('CartApp', function (CartApp, App) {
'use strict';
CartApp.Controller = Marionette.Controller.extend({
});
});
这些应用由中央应用(App)管理,该应用作为参数传递。这两个模块都包含一个控制器定义作为示例。
下面的代码片段演示了主应用能够启动和停止子应用:
Books = (function (Backbone, Marionette) {
'use strict';
var App = new Marionette.Application();
App.on('initialize:after', function () {
if (Backbone.history) {
Backbone.history.start();
}
});
App.startSubApp = function (appName, args) {
var currentApp = App.module(appName);
if (App.currentApp === currentApp) { return; }
if (App.currentApp) {
App.currentApp.stop();
}
App.currentApp = currentApp;
currentApp.start(args);
};
return App;
})(Backbone, Backbone.Marionette);
如我们所见,主应用定义在一个自调用函数中。当我们创建它时,它会自动/立即运行,请注意,调用该函数会返回主要的App对象。
函数startSubApp提供了启动和停止模块的能力。这个函数可能在用户点击按钮打开历史记录或用户直接导航到这个特定路由时被调用。下一步是了解如何调用这个函数。
使用路由过滤器
我们已经了解了如何将应用划分为子应用;然而,我们仍需要决定何时以及如何通知主应用我们需要启动特定的子应用。为了实现这一点,每个模块都应该与一个特定的路由器关联,该路由器需要从一开始就处于活动状态。这与当路由匹配时可以懒加载的模块不同。Marionette.js的创建者通过我们之前提到的BBCloneMail示例应用完美地解决了这个场景。为此,他包含了一个名为routefilter.js的库。与任何其他库一样,这个库通过在我们的项目中添加路径引用来安装。
路由过滤器可以在github.com/boazsender/backbone.routefilter找到。
通常,当我们使用由子应用组成的 SPA 时,同一时间只有一个子应用处于活动状态,我们的示例应用也不例外。这一点很重要,以便理解接下来的代码。
以下代码是针对cart路由器的:
Books.module('CartApp', {
startWithParent: false,
define: function (CartApp, App, Backbone, Marionette, $, _) {
'use strict';
var Router = Backbone.Router.extend({
routes: {
"(:category)(/:id)": "init"
},
before: function () {
App.startSubApp('CartApp');
},
init: function (category,id) {
//call cart app controller function
}
});
App.addInitializer(function () {
var router = new Router();
});
}
});
如我们之前提到的,每个子应用通常都关联着一个路由器。这个路由器将成为该应用的入口点,并负责懒加载它。
让我们来解释一下代码的各个部分。在这里,before是一个使用routefilter.js的魔法定义的函数。这个函数在映射特定路由的任何函数之前执行。这意味着路由器将知道我们正在尝试访问特定的子应用,并通过调用我们之前访问的函数来启动它,该函数位于主应用中(App.startSubApp('CartApp'))。我们已熟悉的其它部分包括模块初始化器和路由定义。
那么,如果我们现在想启动历史应用会怎样?很简单,只需创建一个与该子应用关联的路由器,定义该路由器,我们就完成了。
以下代码将这个概念付诸实践:
Books.module('HistoryApp', {
startWithParent: false,
define:
function (HistoryApp, App, Backbone, Marionette, $, _) {
'use strict';
var Router = Backbone.Router.extend({
routes: {
"history/orders": "showHistory",
},
before: function () {
App.startSubApp('HistoryApp');
},
showHistory: function () {
// call history app controller
}
});
App.addInitializer(function () {
var router = new Router();
});
}
});
内存考虑事项
单页应用中的一个主要挑战是消除内存泄漏。主要问题是我们从未进行过完整的页面重新加载以刷新内存。因此,当放置新的子应用以模拟页面加载时,应用需要处理关闭子应用,从而解除所有与其相关的事件和对象。
但是,如果我们没有正确清理引用,我们仍然可以通过僵尸进程搞乱内存。因此,就像主应用一样,所有子应用都应该关闭旧视图,这就是 Marionette 的 Region 发挥作用的地方。这特别确保了在对象被销毁或我们在区域中切换视图时解除所有事件。
在子应用程序的情况下,有多种清理内存的方法。为了说明这一点,让我们回顾一下“与子应用程序一起工作”部分的一些代码。此函数旨在根据需要停止和启动子应用程序。我们使用这项技术以确保同时只有一个子应用程序运行;一旦子应用程序停止,所有其对象和事件都将被销毁。
App.startSubApp = function (appName, args) {
var currentApp = App.module(appName);
if (App.currentApp === currentApp) { return; }
if (App.currentApp) {
App.currentApp.stop();
}
App.currentApp = currentApp;
currentApp.start(args);
};
在我们的例子中,如果应用程序停止了,路由器提供了调用此函数以在需要时再次启动子应用程序的功能。下面的代码来自本章的“使用路由过滤器”部分。
before: function () {
App.startSubApp('HistoryApp')
},
作为一个重要的注意事项,我们需要培养纪律性,记住每次我们创建对象时,都应该编写适当的代码来删除它们,始终利用Marionette.js的能力。
摘要
到目前为止,我们在创建软件时遇到的主要问题是复杂性。Backbone.js提供了一个易于开始的模型视图结构起点,但它主要提供低级模式。在更复杂的应用程序的情况下,我们可以利用一些其他框架在Backbone.js之上提供缺失的部分。对于你系统中的每个部分,找到解决问题的方法,并将各部分的解决方案组合起来以获得原始问题的解决方案。在实现你的模块时,始终追求可读性和可维护性,并尝试封装行为,而不仅仅是无理由的状态代码。
模块解决了封装的大规模需求,而控制器、视图、路由器和区域则处理问题的更详细方面。
分而治之是一个多年来一直被使用的原则,当处理大型和复杂系统结构时,这是最有用的概念之一。继续掌握我们所学到的所有最佳实践,并尝试将它们作为你应用程序的有机组成部分。下一步是学习使用Marionette.js进行消息传递。
第六章. 消息传递
上一章提出了一种架构,允许将整个应用程序划分为子应用程序和模块。子应用程序只是应用程序的独立部分,它们的功能完全独立。
设计模块和子应用程序的目标是创建一个既集成又松散耦合的整个系统,这正是非常著名的技术出现的地方:消息传递。消息传递的概念,就像分而治之的方法一样,已经存在很长时间了,开发者每天都在使用这些类型的工具和模式。这个模式试图提供一个组件通过消息相互交流的方式,从而允许模块在公共消息总线上订阅和发布事件。
本章将涵盖以下主题:
-
理解事件聚合器
-
使用
Marionette.js的事件聚合器 -
使用事件聚合器扩展应用程序
-
开始使用
Commands -
设置
RequestResponse对象
理解事件聚合器
根据Martin Fowler的说法,事件聚合器执行以下操作:
“将多个对象的事件通道到一个单一对象中,以简化客户端的注册。”
模块化、可扩展和复杂 JavaScript 应用程序中最有用的模式之一是事件聚合。事件聚合器功能位于事件容器中,允许这些事件的发布者和订阅者有一个通信渠道;然而,同时,它还允许它们独立工作,无需它们之间的代码引用,因此它们可以更新或删除而不会影响其他组件。话虽如此,请注意这种解耦在模块化应用程序中的有用性,因为可以添加新的子应用程序和模块,只需利用当前的架构。在我们的复合应用程序设计中,事件聚合器是实现Marionette.js对象之间通信的一种强大方式,我们将看到如何在当前代码中集成它。
以下是对事件聚合器的图形解释:

使用 Marionette.js 的事件聚合器
事件聚合器模式的 Marionette 实现已经从 Marionette 核心构建中分离出来,现在可以在名为backbone.wreqr.js的独立可分发文件中找到。这个实现扩展自backbone.events对象。以下是如何实例化事件聚合器的一个示例:
var vent = new Backbone.Wreqr.EventAggregator();
你可以开始添加监听器,它们将对触发的事件做出反应:
vent.on("do something", function(){
console.log("im logging a message");
});
现在,你有一个监听器,它将等待一个事件的触发。
让我们触发do something方法:
vent.trigger("do something");
这就是所有需要记录消息的内容。好吧,但我们在应用级别如何做到这一点呢?Marionette. An application对象附带了一个Backbone.Wreqr.EventAggregator的实例。因此,通过实例化 Marionette 应用程序对象,您可以在不需要实例化EventAggregator对象的情况下开始注册事件监听器。
var myApp = new Backbone.Marionette.Application();
以下是如何在应用级别注册事件监听器的示例:
myApp.vent.on("helloWorld", function(){
alert("Hello World");
});
事件发布者现在可以在应用程序的任何地方使用以下代码触发事件:
myApp.vent.trigger("helloWorld");
如您所见,我们不需要请求应用程序执行某些工作。在这种情况下,为了显示一个警告,我们应该告诉应用程序对象,当工作将要执行时我们需要被通知MyApp.vent.trigger("helloWorld "),这将显示消息。
使用事件聚合器扩展应用程序
为了使这个模式更容易理解,我们可以使用购物车应用的隐喻。用户从书籍视图中选择要购买的商品。当新商品添加到订单视图中时,需要通知订单视图以显示它并计算总价。
例如,我们有多种方法来完成这个功能,最明显的方法是在书籍视图中引用订单视图,这样我们就可以调用方法或触发事件。但这样,我们将有一个高度耦合的设计,您不能删除订单视图而不影响书籍视图。因此,现在是时候将事件聚合器引入我们的应用程序并解决这个问题了。
我们需要一个中央对象来管理事件及其订阅者。在这个例子中,我们将使用控制器。有了这个控制器和事件聚合器,视图之间将解耦。这意味着书籍视图不会包含订单视图的引用,并且可以修改而不会出现设计问题。
以下是为添加控制器编写的代码:
var cartApp.Controller = App.Controller.extend({
initialize: function (options) {
var self = this;
App.vent.on("itemAdded", function (model) {
self.addProduct(model);
});
},
addProduct: function (model) {
//call orders view
}
});
当控制器初始化时,我们注册了添加项目的监听器。它期望从发布者事件接收参数,然后调用本地函数。下一步是创建触发事件的视图。
以下是为添加视图编写的代码:
CartApp.BookItemView = Backbone.Marionette.ItemView.extend({
template: "#catalogRow",
tagName: "tr",
events: {
'click .btn-primary': 'addItem',
},
addItem: function () {
if (this.$('input').val() > 0) {
this.model.set({ qty: this.$('input').).).val() });
App.vent.trigger("itemAdded", this.model);
}
},
});
这包含一个声明了事件的视图;当执行addItem函数时,此事件将由视图中的按钮调用。它还触发了App.vent.trigger("itemAdded", this.model)事件;此事件将由中央对象,即控制器,处理并调用订单视图。很简单,对吧?这样,我们这里没有订单视图的引用,允许两个视图独立发展。
以下是对我们刚才解释的代码的图形说明。如你所见,我们有一个中心对象,即控制器;它包含监听器,在图书视图中的按钮被点击后,将事件提升以刷新订单视图。你也可以根据你的业务流程更新多个模块。

这种设计还允许控制器拥有多个视图或模块,它们监听事件并相应地做出反应。事件聚合器是一个强大的模式,它能够在模块之间发送消息,使应用程序彼此之间更加解耦。
开始使用命令
当使用纯 Backbone 构建应用程序时,你将只有四个组件:模型、集合、视图和路由器。到目前为止,我们已经回顾了 Marionette 添加的一些对象,例如控制器和应用程序,当然还有不同类型的视图。这些对象中的每一个都旨在减少样板代码并简化将应用程序结构化的过程,以在代码中实现相关的分离,因为并非所有内容都属于视图或路由器。我们现在知道控制器是协调我们的视图的完美地方,但那些不属于视图的代码片段怎么办?显然,将这些代码放入路由器或模型中是没有意义的,因为它们旨在跨所有应用程序使用;对于这些场景,Marionette 有 Commands 组件。
为了实例化它,我们只需要以下这一行:
var commands = new Backbone.Wreqr.Commands();
如你所见,它也是 Wreqr 对象的一部分,所以你可以单独使用它。现在,我们需要设置处理程序,一旦你通过 execute 关键字调用它们,它们就会执行动作。
commands.setHandler("logMessage", function(){
console.log("Im logging an important message");
});
setHandler 函数接受命令名称和它将执行的函数。
以下代码行展示了使用命令名称作为 execute 函数参数来执行命令的示例:
commands.execute("logMessage");
要设置命令并执行它,你只需要做这些。了解你可以像在事件聚合器中一样将这些命令传递参数是很好的。
在以下示例中,我们将传递要记录的消息:
var commands = new Backbone.Wreqr.Commands();
commands.setHandler("logMessage", function(message){
console.log(message);
});
commands.execute("logMessage","I am the message to be logged ");
如你所见,函数接收执行调用传递的消息。当然,这个参数可以是你要传递给处理程序的你需要的任何对象。
现在,让我们在我们的 BookStore 应用程序中使用一个命令,但我们不会实例化 Wrerq 组件,因为 Marionette 应用程序对象已经有一个实例。所以,你可以将命令的处理程序设置到应用程序对象中。
以下代码演示了如何将处理程序设置到应用程序对象中:
var App = new Marionette.Application();
App.commands.setHandler("deleteLocalStorage", function(){
// code todelete the local storage of the application
});
App.execute("deleteLocalStorage");
注意,你可以调用 App.command.execute 或只是 App.execute 加上命令名称,结果将是相同的。
上一个代码中创建的处理程序是用来删除浏览器本地存储中存储的值,以从网站之前的访问中删除旧条目。这段代码不属于任何视图或控制器,因为那些对象的责任与这段代码所做的不一样。我们认为清理浏览器本地存储以防止代码中的旧和无效条目是很有用的,并且为此有一个命令是非常方便的。
我们可以从应用程序的任何部分执行它,但代码放置得很好,以便保持您的关注点分离。我们确信您会发现使用 Backbone 和 Marionette 时使用命令的场景是有意义的。
最后,如果您想删除处理程序,可以使用以下代码行:
App.commands.removeHandler("deleteLocalStorage");
要一次性删除所有已注册的处理程序,使用指令 App.commands.removeAllHandlers()。
设置 RequestResponse 对象
最后,Wreqr 对象的最后一部分是 RequestResponse,在我们看来,这绝对是 Backbone 开发中的一个很好的补充。我们刚刚看到,我们可以如何通过事件的帮助使不同的组件协同工作以相互通信。我们还了解到,并非所有代码都属于视图或控制器的路由器,对于这些情况,Marionette 命令无疑是保持我们关注点分离的一个很好的选择。类似于 Commands,RequestResponse 对象可以帮助我们在应用程序中分割更多的代码责任。
从概念上讲,RequestResponse 对象与事件和 Commands 的工作方式相同,其中一个对象发出调用,另一个对象响应它。与 Commands 的区别在于,在这种情况下,会返回一个响应给调用者。
要设置 RequestResponse 对象,我们需要以下代码行:
var reqres = new Backbone.Wreqr.RequestResponse();
处理程序的设置与命令处理程序类似,正如我们可以在以下代码片段中看到的那样:
reqres.setHandler("getUserName", function(userId){
//code to get the user name goes here
return Username; ///this will be the response
});
为了获取那个响应值,我们需要按照以下方式发出请求:
var username = reqres.request("getUserName", userId);
在上一个示例中,我们请求了一个用户名和处理程序,名为 getUserName 的处理程序仅仅是一个函数,它将为我们返回该值。此外,请注意,您可以为此请求传递参数。
我们认为 RequestResponse 对象对于分离关注点、从服务器获取值、过滤这些值并再次执行数据操作任务非常有用;这些不是迄今为止审查的 Backbone 或 Marionette 的其他组件的责任。将 RequestRepsonse 视为一个服务层,它将在一个地方调用服务器并返回一个集合或模型。而不是在视图级别执行此操作,您的视图应该显示传递给它们的 数据。但它们将承担太多责任,因为它们还负责从服务器检索这些数据,如果您的 API 发生变化怎么办?您将需要更改所有已进行调用的视图或控制器中的服务器调用。
使用RequestResponse对象将使您能够在同一位置执行与服务器的同步,并从不同的地方调用它,始终获得相同的返回值。但最重要的是,它允许您解耦应用程序,并保持其他组件的责任和职责简短且有意义。
让我们看看这个例子,但同样,我们将使用 Marionette 应用程序对象中的默认Wreqr实例:
App.reqres.setHandler("GetBooksByCategory", function(category){
//code to fetch the books by category goes here.
return collection;
});
在控制器的方法内部,我们可以通过执行请求并将集合传递给视图来调用处理器,如下面的代码片段所示:
var BooksController = Marionette.Controller.extend({
initialize: function(options){
this.region = options.region;
},
showBooksinCategory: function(category){
var books = App.request("GetBooksByCategory ", category);
this.region.show(new CategoryView({collection:books}));
}
});
这种方法的优点在于控制器充当了视图和RequestResponse对象之间的调解者,而视图负责获取要删除的数据,因为控制器将集合传递给它。
摘要
在本章中,我们学习了如何借助Wreqr对象解耦我们的应用程序,同时在不同的子组件如事件聚合器、命令和RequestRespone之间分配责任。
在下一章中,我们将学习如何使用Require.js帮助我们在单个文件中使这些组件工作,并保持我们的文件结构有序。
第七章。改变和成长
在上一章中,我们探讨了可以组合起来产生一个完全集成但松散耦合的系统的各种函数。在本章中,我们将介绍一些对 Marionette 非常有价值的外部组件,随着我们的进展,你将发现如何更改框架的一些默认功能,并将 Marionette.js 与外部库结合使用,以使你的应用表现更佳。以下是我们将涵盖的主题列表:
-
使用 异步模块定义 (AMD)
-
使用
Require.js库 -
配置
Require.js -
使用文本插件加载模板
使用 AMD
使用 AMD,API 将帮助我们按需加载脚本,指定模块依赖关系,并减少脚本定义顺序问题。在 第五章 “分而治之——全面模块化”中,我们讨论了构建大型应用如何容易失控。需要考虑多个方面,但管理脚本模块是一个常见的场景。我们需要确保所有脚本按正确顺序加载、合并,并减少对服务器的请求次数。这似乎很简单,但随着应用的扩展,跟踪它变得非常复杂。
为了说明场景,在我们实现以下 AMD 解决方案之前,我们将使用 Index.html 文件中的脚本部分的一部分:
<script src="img/jquery.js"></script>
<script src="img/underscore.js"></script>
<script src="img/backbone.js"></script>
<script src="img/backbone.marionette.js"></script>
<script src="img/backbone.routefilter.js"></script>
<script src="img/backbone.localStorage.js"></script>
<script src="img/bootstrap.min.js"></script>
<script src="img/Books.js"></script>
<script src="img/BaseController.js"></script>
<!-- Cart -->
<script src="img/CartApp.js"></script>
<script src="img/CartRouter.js"></script>
<script src="img/Catalog.js"></script>
<script src="img/BookModels.js"></script>
<script src="img/Books.Data.js"></script>
<script src="img/main.js"></script>
此列表仅包含少量用于证明概念的文件,我们已经开始积累大量的脚本。随着我们添加更多的模块、服务、模型、视图等,它将开始变得不那么全面,并且真正难以维护。例如,在某个时刻,我们可能会失去对不再使用的文件的跟踪。在先前的代码中,所有脚本在页面加载时都会被下载,即使当前视图没有使用它们。我们的代码是模块化的,但仍然不容易编写可以即时加载、作为依赖注入并与其他模块共享的封装代码。让我们回顾一下我们如何解决这个问题。
使用 Require.js 库
James Burke 介绍了 Require.js 库,它有一个庞大的社区。作者在脚本加载方面是专家,也是 AMD 规范的贡献者。本书假设你已了解 AMD 的基础知识,因此在跳转到我们应用的实现之前,它将为你提供使用 Require.js 时所需的配置和样板代码的基础知识。要获取 Require.js 的最新构建版本,请访问项目网站,requirejs.org/docs/download.html。
配置 Require.js
要开始使用 Require.js,我们将创建一个名为 main.js 的文件。当然,你可以给这个文件一个更合适的名字,使其符合你的命名约定和业务领域。我们将在 main.js 文件中编写以下代码:
require.config({
baseUrl: 'src',
paths: {
jquery: 'libs/jquery',
underscore: 'libs/underscore',
backbone: 'libs/backbone',
marionette: 'libs/backbone.marionette',
},
shim: {
underscore: {
exports: '_'
},
backbone: {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
},
marionette : {
deps : ['jquery', 'underscore', 'backbone'],
exports : 'Marionette'
}
}
});
require(['jquery','underscore','backbone','marionette'], function($,_,Backbone,Marionette) {
console.log('Hello world from the main file! ');
});
让我们将 Index.html 文件中的所有脚本引用替换为下一个脚本引用,如下所示:
<script data-main="main" src="img/require.js"></script>
在这个脚本引用中,我们将包含所有所需配置的文件名(在我们的例子中是 main.js 文件)传递给 data-main 属性。请注意,传递的是文件名(main)而不是其扩展名(.js),因为 Require.js 假设它只与 JavaScript 文件一起工作;因此,不需要扩展名。源(src)应指向 Require.js 文件所在的路径。
现在,我们准备进行一个小测试,以查看我们是否走上了正确的道路。打开浏览器并在控制台,当你加载 Index.html 文件时,你应该会看到日志消息。
现在,让我们回顾 main.js 文件内容的每个部分,以更好地理解正在发生的事情。
在前面的代码片段中,我们将我们将要使用的所有库放在了 require.config 函数的 paths 部分下。在左侧,我们分配了库的别名,在右侧,我们指明了文件的路径——这个路径相对于分配的 baseUrl 值,在这种情况下,是 src 文件夹。
这个函数的第二个属性称为 shim。shim 的主要用途是针对不支持 AMD 的库,但你仍然需要管理它们的依赖项。一个完美的例子是 Underscore.js。在这种情况下,Underscore.js 被导出为 _,并且它不依赖于另一个库来加载。我们有一个不同的场景与 Backbone.js 有关,它需要 Underscore 才能正确工作。我们必须指定 Underscore.js 作为依赖项,因为 Backbone.js 在加载之前可能会尝试使用它。
require 函数放置在文件末尾,如下所示:
require(['jquery','underscore','backbone','marionette'], function($,_,Backbone,Marionette) {
console.log('Hello world from the main file!);
});
前面的代码将成为我们应用程序的起点。这是一个函数定义,它将导出的值作为参数。在这个阶段,我们只是在记录一条消息,但现在让我们做一些更有用的事情。
定义我们的应用程序模块
现在已经使用 Require.js 配置了核心依赖项,一旦它们被加载并准备就绪,我们就可以定义我们的 Marionette 应用程序并设置区域初始化器、命令和请求/响应处理器。这是因为我们需要一个名为 app.js 的单个文件内部,目的是将所有与 Marionette 应用程序对象相关的登录细节都放在这个文件中。在下面的代码中,我们的应用程序被定义并准备好作为一个 AMD 模块工作。以下是我们 app.js 文件的内容:
define(['marionette'], function(Marionette){
var Books = new Marionette.Application();
Books.addRegions({
main: '#main',
modal: ModalRegion
});
Books.on('initialize:after', function () {
if (Backbone.history) {
Backbone.history.start();
}
});
Books.startSubApp = function (appName, args) {
var currentApp = App.module(appName);
if (App.currentApp === currentApp) { return; }
if (App.currentApp) {
App.currentApp.stop();
}
App.currentApp = currentApp;
currentApp.start(args);
};
return Books;
});
我们刚刚定义的书籍应用程序将在启动应用程序时在 main.js 文件中使用。
当我们添加新文件时,我们需要知道它的位置以及它的别名。我们通过访问main.js文件定义中的paths部分来指定这一点。在此更改后,您的paths部分应如下所示:
paths: {
jquery: 'libs/jquery',
underscore: 'libs/underscore',
backbone: 'libs/backbone',
marionette: 'libs/backbone.marionette',
app: 'app'
},
现在,我们已准备好使用此文件在main.js文件的require函数中启动我们的 Marionette 应用程序,如下所示:
require(['app'], function(Books) {
Books.start();
});
注意我们如何将书籍的依赖注入以启动 Marionette 应用程序,并使用 Marionette 应用程序对象的start()方法来触发初始化器。
使用 Require.js 编写子应用程序
我们定义的模块是我们的根应用程序,负责启动子应用程序。下一个示例将展示我们如何使用Require.js定义子应用程序。如您所见,我们可以轻松地将前面的代码适配以使用require函数,通过将脚本定义发送到配置文件并将必要的对象注入到模块定义中。以下代码来自CartApp子应用程序:
define(['app'], function(Books){
Books.module('CartApp', function (CartApp, Books, Backbone,Marionette, $, _) {
CartApp.Controller = Marionette.Controller.extend({
initialize: function (options) { },
addProduct: function (model) { },
removeProduct: function(model){ },
});
CartApp.addInitializer(function (args) {
CartApp.controller = new CartApp.Controller({
mainRegion: args.mainRegion,
});
CartApp.controller.show();
});
CartApp.addFinalizer(function () {
if (CartApp.controller) {
CartApp.controller.close();
delete CartApp.controller;
}
});
return Books.CartApp;
});
});
将所有组件模块化
在以下示例中,我们将展示如何编写一个用于加载Require.js的视图模块,但同样的概念适用于所有对象/组件。
在以下代码中,我们定义了一个名为CategoryView.js的视图,并在main.js文件中给它命名为categoryView的别名,这样其他文件就可以使用它。
define(['app'], function(Books){
Books.module('CartApp.CategoryView', function(View, Books, Backbone, Marionette, $, _){
View.CategoryView = Backbone.Marionette.ItemView.extend({
tagName : 'li',
template: '#categoryTemplate',
events : {
'mouseenter .info' : 'showDetails',
'mouseleave .info' : 'hideDetails'
},
showDetails : function() {
this.$( '.info').popover({
title:this.model.get('name'),
content:this.model.get('booksOnCategory')
});
this.$( '.info').popover('show');
},
hideDetails : function() {
this.$( '.info').popover('hide');
},
});
return Books.CartApp.CategoryView;
});
});
前面的示例定义了一个作用域良好的对象。当模块没有任何依赖项且仅是一个集合时,我们通过将对象字面量传递给define()函数。在我们的场景中,我们的模块有依赖项,因此第一个参数应该是一个依赖项名称数组——在这种情况下,app是应用程序的别名——第二个参数应该是一个定义函数。
添加文本插件
到目前为止,我们已使用模板的 ID 定义了视图的模板属性。这个模板位于一个脚本标签内,并且始终存在于 DOM 中。但是,将所有模板放入 SPA 的 HTML 文件中不会扩展,并且会给我们带来与Index.html文件中所有脚本引用相同的维护问题。解决这个问题的方法就是使用文本插件。
您可以从Require.js页面下载文本插件。以下为下载链接:
requirejs.org/docs/download.html#text。
与任何其他脚本文件一样,我们需要在main.js文件中给它一个别名以及其路径,以便开始使用它。
文本插件的责任是从服务器获取模板并将其传递给我们的视图,这样我们就不需要在 HTML 文件中包含它。
在以下代码中,我们使用!text/path语法传递了模板的相对路径,并且创建视图的函数接收模板的导出名称作为参数;在这种情况下,CategoryTemplate。
define(['app', '!text/templates/CategoryTemplate.html'], function(Books, CategoryTemplate){
Books.module('CartApp.CategoryView', function(View, Books, Backbone, Marionette, $, _){
View.CategoryView = Backbone.Marionette.ItemView.extend({
tagName : 'li',
template: CategoryTemplate,
events : {'
'mouseenter .info' : 'showDetails',
'mouseleave .info' : 'hideDetails'
},
showDetails : function() {
this.$( '.info').popover({
title:this.model.get('name'),
content:this.model.get('booksOnCategory')
});
this.$( '.info').popover('show');
},
hideDetails : function() {
this.$( '.info').popover('hide');
},
});
return Books.CartApp.CategoryView;
});
});
当构建大型应用程序时,这种方法更易于维护,但也许你希望为了性能优势,将初始模板保留在 HTML 文件中,其余的模板放在正确的文件结构中。
文件结构化
有许多不同的选项可以定义文件布局,这可能会根据项目的大小和类型而定义。最终,目标应该是创建一个易于理解、实施和维护的文件夹结构。
在以下示例中,我们将源文件分类到常见的文件夹中,例如模型和集合,以及为应用程序组件(视图和控制台)指定的特定文件夹。
我们代码所需的静态依赖项,如 CSS、图像和 JavaScript 库,应放在不同的目录下。这可以防止意外修改库代码,并让我们更好地理解实际的业务领域。
以下图片展示了我们将文件放置的基础结构:

话虽如此,让我们深入了解我们应用程序的一些细节。以下图片显示了您可能如何布局应用程序结构:

在前面的图片中,我们展示了我们的书店应用程序的结构。这种结构在这个特定情况下是有意义的。但好事是,我们创建了小而有意义的文件,它们可以以更简单、更优雅的方式相互交互,而不是包含不同组件逻辑的大文件。
在 Marionette 中使用 handlebars 作为模板引擎
Backbone 的一个卖点是与其他库配合良好,这一点对于 Marionette 也是适用的。如果我们想使用不同的模板引擎,我们可以轻松做到。
对于这个特定的例子,我们将使用 handlebars,我们可以从handlebarsjs.com/下载。
下载了Handlebars.js文件后,我们可以通过以下行将其添加到Index.html文件中:
<script type="text/javascript" src="img/handlebars.js">
或者,我们可以在main.js文件中指定它的别名和路径。
与 underscore 模板的语法差异在于,handlebars 表达式是{{,然后是一些内容,然后是}},而不是require函数的<%= expression %>。因此,handlebars 模板看起来如下:
<script type="text/html" id="sample-template">
<p>This is an ItemView template using handlebars<p>
{{ value1 }} </br>
{{ value2 }} </br>
</script>
为了在 Marionette 视图中使用此模板,我们必须调用以下语法:
template : Handlebars.compile($('#sample-template').html());
前面的行将从 DOM 中获取模板并将其编译成一个函数,该函数稍后将由 Marionette 用于渲染视图。以下将是ItemView的完整代码:
var SampleView = Backbone.Marionette.ItemView.extend({
template : Handlebars.compile($('#sample-template').html())
});
要查看此功能的工作情况,请访问jsfiddle.net/rayweb_on/gXemX/的 JSFiddle 示例。
就这样!开始使用不同的模板引擎不需要进行全局更改。如果我们想的话,我们也可以同时使用这两个引擎,因为模板的定义是在视图层面。
摘要
在本章中,我们了解到为了管理我们应用日益增加的复杂性,我们必须将其分解成单独的文件。这个文件数量的增加导致另一个问题,而Require.js以优雅的方式解决了这个问题。Backbone.js的开发明显受益于Marionette.js的使用,以及其他库,例如Require.js和Handlebars.js等。这无疑会使我们的开发环境更加稳固,同时,对变化也更加灵活。





浙公网安备 33010602011771号