使用 Backbone.js 和 Marionette.js 创建单页应用的入门指南






4.83/5 (28投票s)
讨论 backbone.js 和 marionette.js 框架,以及如何使用它们创建单页 JavaScript 应用程序。
引言
在本文中,我们将讨论 backbone.js 和 marionette.js 框架,并了解如何使用它们来创建单页 JavaScript 应用程序。
背景
很久以前(几乎是十年前),大多数软件应用程序都是作为独立应用程序构建的。这些应用程序面向单个用户并在其操作系统上运行。然后出现了跨多个用户共享数据和将数据存储在中央位置的需求。这种需求催生了分布式应用程序和 Web 应用程序。分布式应用程序作为独立应用程序在用户计算机上运行,为用户提供丰富的用户界面(类似桌面的体验),同时在后台,这些应用程序将数据发送到服务器。而 Web 应用程序则沙箱化在 Web 浏览器中,使用 HTML 和 HTTP 让用户对存储在远程服务器上的数据执行操作。
这两个应用程序的主要区别在于,分布式应用程序提供了交互式用户体验,而 Web 应用程序的功能非常有限(由于技术限制)。分布式应用程序的缺点是很难发布和确保所有用户都获得更新的应用程序。Web 应用程序没有这些问题,因为一旦应用程序在服务器上更新,所有用户都会获得更新的应用程序。
这两种方法都有优缺点,需要做些什么来获得两全其美。这就是基于浏览器插件的应用程序(如 Flash 应用程序和 Silverlight 应用程序)的出现。这些技术填补了 HTML 无法实现的所有功能的空白。它们提供了在浏览器中运行的富互联网应用程序的可能性。唯一的缺点是用户需要安装浏览器插件才能使这些应用程序正常工作。
然后,浏览器变得越来越强大,HTML 也变得越来越成熟。仅使用基于浏览器的客户端技术就可以创建富互联网应用程序。这促使开发人员使用 HTML 和 JavaScript 编写客户端代码来创建富互联网应用程序。不再需要 Flash 和 Silverlight 等插件。但是,由于 HTML 和 JavaScript 最初并非用于编写功能齐全的 Web 应用程序,因此这些应用程序的 HTML 和 JavaScript 代码混合在一起。这导致了“意大利面条式代码”,这些客户端 HTML/JavaScript 应用程序(单页应用程序或 SPA)变成了维护噩梦。
那么,为什么要在导致代码质量差的情况下编写单页应用呢?创建单页应用程序的主要原因是它们允许我们为用户提供更接近原生/桌面/设备的应用程序体验。因此,需要以结构化的方式创建 SPA,这就催生了能够为单页应用程序提供一定结构的 JavaScript 框架和库的需求。
目前,有相当多的开源 JavaScript 框架可帮助我们解决“意大利面条式代码”的问题。这些框架允许我们以结构化的方式设计我们的应用程序。在本文中,我们将讨论 backbone.js,它无可争议是开发单页应用程序的最佳框架之一。在本文中,我们将了解 backbone.js 框架和 Marionette.js 框架的基础知识,并尝试实现一个简单的应用程序来实际演示这些概念。
Using the Code
什么是 Backbone.js 和 Marionette.js
Backbone.js 是一个轻量级框架,可让我们以结构化的方式创建单页应用程序。它基于模型-视图-控制器(MVC)模式。它最适合使用 RESTful 服务来持久化数据来创建单页应用程序。
Marionette.js 是一个构建在 backbone.js 之上的复合应用程序库,它允许我们构建大型 JavaScript 单页应用程序。Backbone.js 不强制使用严格的 MVC 模式,但 Backbone Marionette 强制使用严格的 MVC 模式。Marionette 还提供了 View
类,我们可以从中创建和扩展简单的视图、集合视图和复合视图。它还为我们提供了创建控制器类和跨应用程序的事件机制的可能性。
关注点分离和 MVC
良好的应用程序架构最重要的方面之一是关注点分离 (SoC)。Backbone Marionette 框架最棒的地方在于它能够使用 MVC 模式提供这种关注点分离。Model 将代表实现解决方案所需的业务对象。View 是用户可见的部分。用户可以简单地使用视图来消费数据,或者对数据执行操作,即 CRUD 操作。Controller 负责提供视图和 Model 之间的交互机制。Controller 的职责是响应用户输入,并通过更新 Model 和 View 来协调应用程序。Model 和 View 保持松耦合,即 Model 对 View 一无所知,而 View 拥有一个 Model 对象(关联)来提取信息并显示给用户。
模型
什么是 Model?在典型的 MVC 或 MVVM 模式中,Model 代表业务实体。Model 代表业务实体,并具备执行某些业务逻辑和业务规则验证的可能性。Model 还公开事件,以便使用这些 Model 的任何一方(通常是 View)都可以订阅它们,并在 Model 的状态发生更改时获得通知。Model 是 Backbone 应用程序中最重要的部分。如我们所讨论的,它们代表了我们的应用程序将使用的实体。通常是通过查看 RESTful API 来创建这些 Model。我们基于 REST API 接口创建 Model。所以,假设我们有一个提供图书实体 CRUD 操作的 RESTful 服务。该实体的 JSON 负载是
{
"ID": 3,
"BookName": "Test Book 3"
}
注意:出于演示目的,我将使用 WebAPI 来创建 RESTful 服务。该服务的源代码已附加在示例代码中,并且可以在这里找到 WebAPI 的文档
因此,对于上述负载,我们需要创建如下的 Backbone Model:
var Book = Backbone.Model.extend({
defaults: {
ID: null,
BookName: null
},
urlRoot: 'https://:51377/api/Books'
});
这里 Book
是 Model 的名称。它包含两个属性:ID
和 BookName
。这些属性是通过查看 JSON 负载创建的。通常,JSON 负载中的每个属性在 Model
类中都有一个对应的属性。urlRoot
指定了用于对此 Model 执行 CRUD 操作(在服务器上)的 URL。
现在的问题是:我们如何创建这个 Model?
var book = new Book({BookName: "Backbone Book 1"});
我们不传递任何 ID
属性的原因是,ID
属性是服务器端的自动生成列。现在我们来看看如何对这个 Model 执行 CRUD 操作。
Create
要在服务器上创建新实体,我们需要填充 Model 中非标识字段(在本例中是 ID
以外的字段),然后调用 Model 上的 Save
方法。
var book = new Book({BookName: "Backbone Book 1"});
book.save({}, {
success: function (model, respose, options) {
console.log("The model has been saved to the server");
},
error: function (model, xhr, options) {
console.log("Something went wrong while saving the model");
}
});
读取
要读取单个图书实体,我们需要创建带有已填充标识属性的图书实体,即我们要读取图书的 ID。然后我们需要在 model
对象上调用 fetch
方法。
var book1 = new Book({ id: 10 });
book1.fetch({
success: function (bookResponse) {
console.log("Found the book: " + bookResponse.get("BookName"));
}
});
更新
现在,假设我们要更新先前 fetch 调用中检索到的图书的名称。我们只需要设置需要更新的属性,然后再次调用 save
方法。
// Let us update this retrieved book now
bookResponse.set("BookName", bookResponse.get("BookName") + "_updated");
bookResponse.save({}, {
success: function (model, respose, options) {
console.log("The model has been updated to the server");
},
error: function (model, xhr, options) {
console.log("Something went wrong while updating the model");
}
});
删除
现在要删除一个 Model
,我们只需要调用 model
对象的 destroy
方法。
// Let us delete the model with id 13
var book2 = new Book({ id: 13 });
book2.destroy({
success: function (model, respose, options) {
console.log("The model has deleted the server");
},
error: function (model, xhr, options) {
console.log("Something went wrong while deleting the model");
}
});
集合
因此,我们已经看到了如何创建 Model 并对 Model 执行 CRUD 操作。但是,如果我想从服务器获取多个 Model,即 Model 的集合呢?这时 Backbone collections 就会派上用场。我们可以创建一个 Backbone Collection,它将处理 Model 的集合。所以,如果我们需要在我们的应用程序中创建一个 BooksCollection
对象
var BooksCollection = Backbone.Collection.extend({
model: Book,
url: 'https://:51377/api/Books'
});
url
属性指向将用于检索图书列表的服务器位置。所以,如果我们想获取图书列表并将其填充到 BooksCollection
对象中,我们可以这样做:
//Let us try to fetch a collection of books
var books = new BooksCollection();
books.fetch({
success: function(books) {
console.log(books.length + " books found");
}
});
注意:现在我们知道什么是 Backbone Model 以及如何使用这些 Model 执行 CRUD 操作。在此处要提及的重要一点是,Model 除了数据和 CRUD 操作之外,还提供了围绕该 Model 的许多其他功能。我们可以使用 Model 执行验证、转换以及许多其他操作。本文不涵盖执行这些操作。
视图
有了 Model,我们就能够为我们的应用程序实现一些结构,因为我们所有的 JSON 解析、JavaScript 对象、验证以及执行 CRUD 操作的 AJAX 调用现在都被抽象成 Model 的形式。但是,如果我们仍然使用嵌入在 HTML 中的 JavaScript 来显示 Model 数据并在用户操作时操作 DOM 元素,我们可能会遇到很多糟糕的零散代码。如何解决这个问题?
Backbone 通过提供 Views 来帮助我们解决这个问题。Backbone 为我们提供了创建 View 类的可能性。View 类不了解 HTML 和 CSS,即实际的 UI 部分。View 类更像是连接 HTML 模板和 Model 对象的粘合剂。此外,它还提供了处理从 Model 触发的事件并更新 UI、处理 UI 事件并对其进行响应(对 Model 执行某些操作)的机制。所以,从某种意义上说,我们可以说 View 只是观察者,它们监听 Model 和 UI 事件,这使得它们成为处理所有事件并对其采取行动的理想场所。
Marionette 更进一步,为我们提供了创建多种类型视图的可能性,即 ItemView
(显示单个 Model)、CollectionView
(显示 Model 的集合)和 CompositeView
(使用自定义模板显示 Model 的集合)。
所以,假设我们想创建一个非常小的单页应用程序来对 REST API 提供的 book
实体执行 CRUD 操作。我们需要一个视图来显示图书列表。一个添加新图书的视图,一个更新图书的视图,以及从列表视图中删除图书的可能性。所以,让我们从创建一个简单的视图来显示单个图书实体开始。
<script type="text/template" id="bookView">
< td> < % =BookName % > < /td>
< td> < input type="submit" id="btnEditBook" value="edit"/> < /td>
< td> < input type="submit" id="btnDeleteBook" value="delete"/> < /td>
< /script>
这是一个模板,它将从 Model 中渲染图书名称,以及一个编辑图书名称的按钮和一个删除图书名称的按钮。由于它被包装在 script
模板标签内,浏览器不会显示它们,但 Backbone Marionette 视图类可以使用它们来渲染 Model 数据。所以,让我们看看显示单个图书条目的视图类是什么样的:
var BookView = Backbone.Marionette.ItemView.extend({
tagName: 'tr',
template: "#bookView",
initialize: function() {
this.listenTo(this.model, "change", this.render);
},
events: {
'click #btnDeleteBook': "deleteBook",
'click #btnEditBook': "editBook"
},
deleteBook: function() {
// do something to delete this book
},
editBook: function() {
// do something to edit this book
}
});
这个类指定了它将使用的模板、渲染模板时将创建的 HTML tagName
,以及处理 UI 事件的函数。现在,如果我们想创建这个视图并在屏幕上渲染模板:
// Lets create a book
var book = new Book ({BookName: "Test Book"});
// Lets create the view and pass the book as model
var bookView = new BookView({model: book});
同理,如果我们想创建一个显示图书列表的视图,我们需要创建 CollectionView
。
var BooksCollectionView = Backbone.Marionette.CollectionView.extend({
itemView: BookView,
tagName: 'table',
});
它只是指定了渲染时应使用的 HTML tagName
以及用于渲染集合中各个项目的视图。要创建这个集合视图:
// Create a collection of books
var books = new BooksCollection();
// Do something to fetch or fill this collection
// create the collection view to render
var booksView = new BooksCollectionView({ collection: books });
要创建新图书,我们需要另一个视图,所以这个视图的模板将是这样的:
<script type="text/template" id="addBookView">
<input type="textbox" id="txtBookName"/>
<input type="submit" id="btnAddBook" value="Add Book"/>
</script>
以及渲染此视图的视图类将是这样的:
var AddBookView = Backbone.Marionette.ItemView.extend({
template: "#addBookView",
events: {
'click #btnAddBook': "addBook"
},
addBook: function () {
// Handle the book addition here
}
});
现在我们已经准备好显示图书列表、显示一本书、添加一本书的视图了。此外,UI 还包含能够编辑和删除图书的元素。现在,要理解这些视图将如何渲染,让我们先看看控制器。
控制器
Controller 类为视图和 Model 之间的交互提供了粘合代码。所以我创建了一个简单的控制器类,它将对 Model 执行 CRUD 操作。Controller 还将负责创建视图并渲染它们。稍后,我们将将其与 UI 事件挂钩。
var BooksController = Backbone.Marionette.Controller.extend({
ShowBooksList: function (options) {
this.collection = new BooksCollection();
var self = this;
this.collection.fetch({
success: function (books) {
var booksView = new BooksCollectionView({ collection: self.collection });
options.region.show(booksView);
}
});
},
ShowAddBookView: function (options) {
var addBookView = new AddBookView();
options.region.show(addBookView);
},
AddBook: function (book) {
var BookToSave = book;
var self = this;
BookToSave.save({}, {
success: function (model, respose, options) {
console.log("The model has been saved to the server");
self.collection.push(model);
},
error: function (model, xhr, options) {
console.log("Something went wrong while saving the model");
}
});
},
DeleteBook: function (book) {
var BookToDelete = book;
var self = this;
BookToDelete.id = BookToDelete.get("ID");
BookToDelete.destroy({
success: function (model, respose, options) {
console.log("The model has deleted the server");
self.collection.remove(model);
},
error: function (model, xhr, options) {
console.log("Something went wrong while deleting the model");
}
});
},
UpdateBook: function (book) {
var BookToUpdate = book;
var self = this;
BookToUpdate.id = BookToUpdate.get("ID");
BookToUpdate.save({}, {
success: function (model, respose, options) {
console.log("The model has been updated to the server");
self.collection.push(model, { merge: true });
},
error: function (model, xhr, options) {
console.log("Something went wrong while updating the model");
}
});
}
});
Controller 的 ShowBooksList
函数将从服务器获取图书,创建 BooksCollectionView
,然后使用 show
方法(我稍后会解释)在屏幕上渲染它。ShowAddBookView
仅创建 AddBookView
并在屏幕上渲染它。其他函数,如 AddBook
、DeleteBook
和 UpdateBook
,用于对 book
实体执行 CRUD 操作。
所以,现在我们有了代表业务实体并知道如何对它们执行 CRUD 操作的模型类。我们有指定 HTML/CSS 部分的视图模板,以及使用模板渲染 Model 的视图类。这些视图还充当观察者,监听 Model 更改以更新视图,并监听 UI 事件以操作 Model。最后,我们有了提供视图和 Model 之间交互的控制器类。现在让我们看看这一切是如何融入整体的,以及如何创建完整的应用程序。
Marionette 应用程序模型
Marionette 为我们提供了创建中央 Application
对象 的可能性。所以,让我们先创建 Marionette Application
对象。
var sampleApp = new Backbone.Marionette.Application();
然后我们需要指定我们应用程序的主要区域。我们的应用程序将只有两个区域,例如,一个用于添加新图书,另一个用于显示图书列表。
sampleApp.addRegions({
listRegion: "#listRegion",
addRegion: "#addRegion"
});
这些区域只是 main.html 中的 div
。现在,让我们创建 controller
对象,并将这些区域传递给 respective 方法,以便 controller
在从服务器获取数据后渲染它们。
// Lets show the list of books
var booksController = new BooksController();
booksController.ShowBooksList({ region: sampleApp.listRegion });
// Lets show the region to add the books
booksController.ShowAddBookView({ region: sampleApp.addRegion });
完成此操作后,我们将在屏幕上看到图书列表。
现在唯一剩下的就是将 UI 事件从 View
类连接到 Controller
类的函数。所以,让我们连接视图事件并调用 controller
函数。完成后,view
类将如下所示:
var BookView = Backbone.Marionette.ItemView.extend({
tagName: 'tr',
template: "#bookView",
initialize: function() {
this.listenTo(this.model, "change", this.render);
},
events: {
'click #btnDeleteBook': "deleteBook",
'click #btnEditBook': "editBook"
},
deleteBook: function() {
if(confirm("Are you sure you want to delete this book")) {
sampleApp.trigger("bookDelete", this.model);
}
},
editBook: function() {
var newName = prompt("Enter new name for the book");
if(newName) {
this.model.set("BookName", newName);
sampleApp.trigger("bookEdit", this.model);
}
}
});
var BooksCollectionView = Backbone.Marionette.CollectionView.extend({
itemView: BookView,
tagName: 'table',
});
var AddBookView = Backbone.Marionette.ItemView.extend({
template: "#addBookView",
events: {
'click #btnAddBook': "addBook"
},
addBook: function () {
// Let us extract the value from the textbox now
var bookName = $('#txtBookName').val();
var book = new Book({ BookName: bookName });
sampleApp.trigger("bookAdd", book);
}
});
model
类将如下所示:
var Book = Backbone.Model.extend({
defaults: {
ID: "",
BookName: ""
},
urlRoot: 'https://:51377/api/Books'
});
var BooksCollection = Backbone.Collection.extend({
model: Book,
url: 'https://:51377/api/Books'
});
最后,负责协调的 controller
类是这样的:
var BooksController = Backbone.Marionette.Controller.extend({
initialize: function (options) {
var self = this;
// Hook up the add book event
sampleApp.on("bookAdd", function (book) {
self.AddBook(book);
});
// Hook up the delete book event
sampleApp.on("bookDelete", function (book) {
self.DeleteBook(book);
});
// Hook up the edit book event
sampleApp.on("bookEdit", function (book) {
self.UpdateBook(book);
});
},
ShowBooksList: function (options) {
this.collection = new BooksCollection();
var self = this;
this.collection.fetch({
success: function (books) {
var booksView = new BooksCollectionView({ collection: self.collection });
options.region.show(booksView);
}
});
},
ShowAddBookView: function (options) {
var addBookView = new AddBookView();
options.region.show(addBookView);
},
AddBook: function (book) {
var BookToSave = book;
var self = this;
BookToSave.save({}, {
success: function (model, respose, options) {
console.log("The model has been saved to the server");
self.collection.push(model);
},
error: function (model, xhr, options) {
console.log("Something went wrong while saving the model");
}
});
},
DeleteBook: function (book) {
var BookToDelete = book;
var self = this;
BookToDelete.id = BookToDelete.get("ID");
BookToDelete.destroy({
success: function (model, respose, options) {
console.log("The model has deleted the server");
self.collection.remove(model);
},
error: function (model, xhr, options) {
console.log("Something went wrong while deleting the model");
}
});
},
UpdateBook: function (book) {
var BookToUpdate = book;
var self = this;
BookToUpdate.id = BookToUpdate.get("ID");
BookToUpdate.save({}, {
success: function (model, respose, options) {
console.log("The model has been updated to the server");
self.collection.push(model, { merge: true });
},
error: function (model, xhr, options) {
console.log("Something went wrong while updating the model");
}
});
}
});
现在,我们已经准备好了一个基本的 Backbone 应用程序,它具有对 Book
实体的所有 CRUD 操作。要运行此应用程序,应首先下载并运行 WebAPI 项目,然后运行 Backbone 示例应用程序。如果 WebAPI 在其他端口上运行,则需要在运行应用程序之前更新模型类中的 URL。
看点
在本文中,我介绍了如何使用 Backbone Marionette 框架创建 JavaScript 单页应用程序。本文面向刚开始接触单页应用程序和 Backbone Marionette 的初学者程序员。本文更侧重于 Marionette 框架,因为我个人非常喜欢 marionette,而 Backbone 和 marionette 结合起来是一个完整的单页应用程序开发包。
我建议查看和调试示例应用程序,因为这是我们理解应用程序结构的唯一方法。希望本文对您有所帮助。
历史
- 2013 年 12 月 18 日:首个版本