测试主干应用与测试任何其他应用没有什么不同;除了 Backbone.js 已经免费为您利用了许多功能之外,您仍然会根据您的规范来驱动您的代码。所以期望写更少的代码,从而更少的规范。
主干是一个微框架,旨在为网络应用提供足够的结构来允许它们增长。它提供了四个基础抽象:
- 模型:它为应用数据和自定义事件提供了一个键值存储
- 集合:提供丰富的可枚举 API
- 视图:创建界面积木
- 路由器:提供客户端路由的方式
在处理每种类型的抽象时,我们会看到一些常见的测试场景,以及人们在创建这些规范时犯的一些常见错误。
它们是 Backbone.js 真正的主干,它们是我们构建业务逻辑的抽象,它们保存数据并负责执行验证和与远程服务器的同步;它们是主干模型。
虽然我们没有在示例应用中使用主干,但是我们已经有了一些定义良好的模型,包括股票和投资对象。但是在我们深入研究如何将它们重构为主干模型之前,我们首先需要对它们的工作原理有一点了解。
要创建一个新的模型对象,我们首先需要从基础主干模型扩展它。为了使事情变得更有趣,我们将重写整个股票规范,期望股票成为主干模型。
在股票规范中,我们可以写下以下接受标准,虽然与业务无关,但保证这个模型将继承模型的所有功能:
describe("Stock", function() {
var stock;
beforeEach(function() {
stock = new Stock();
});
it("should be a Backbone.Model", function() {
expect(stock).toEqual(jasmine.any(Backbone.Model));
});
});
为了实现这个新模型,我们需要使用股票源文件中的Backbone.Model.extend
函数:
(function (Backbone) {
var Stock = Backbone.Model.extend();
this.Stock = Stock;
})(Backbone);
我们完了!该股现在是一个功能齐全的骨干模式,其所有的细节。
但是现在我们已经完成了,我们能做什么呢?
主干模型的核心是键值存储,应该是用来保存模型的所有数据。您可以通过一个非常简单的 getter 和 setter 函数的 API 来访问它。这里给出了一个代码片段,它是如何为之前声明的模型工作的:
var stock = new Stock();
stock.set('sharePrice', 10)
stock.get('sharePrice') // 10
您也可以将对象传递到 set
功能,以便设置多个参数:
stock.set({
sharePrice: 20,
symbol: 'AOUE'
});
并且仍然能够单独获得每个属性:
stock.get('sharePrice') // 20
stock.get('symbol') // AOUE
您甚至可以在实例化时传递模型的所有属性,而 getter 方法仍然可以单独为每个属性工作:
var stock = new Stock({
sharePrice: 20,
symbol: 'AOUE'
});
现在,您已经理解了模型属性是如何工作的,让我们再来看一个简单的股票规范测试案例(已经移植到 Backbone.js 上):
beforeEach(function() {
stock = new Stock({
sharePrice: 10
});
});
it("should have a share price", function() {
expect(stock.get('sharePrice')).toEqual(10);
});
虽然这个场景以前对测试一段代码很有用,但是现在已经过时了。我们基本上是在测试主干功能,主干库本身已经对它进行了很好的测试,所以您可以安全地删除该规范,并且仍然确信模型属性正在工作(只要您有规范来测试它是主干模型)。
很好!到目前为止,主干帮助我们编写了更少的代码和规范。
虽然你可以为每件事编写规范代码,但是尽量不要重复你自己(通过重写别人的规范),并且编写涵盖你实际编写的一段代码的规范。
回到模型属性,我们将探索主干功能,这确实需要我们进行一些测试,但这是因为我们还将编写一些代码来使其工作。在实例化时设置默认模型值是一个非常简单的功能。
还是以股票为例,让我们添加另一个接受标准,即它的默认股价值应该为零:
describe("Stock", function() {
var stock;
beforeEach(function() {
stock = new Stock({ symbol: 'AOUE' });
});
it("should have a default share price of zero", function() {
expect(stock.get('sharePrice')).toEqual(0);
});
});
我们以前见过标准的主干属性处理。请注意,我们在任何时候都不会在规范上设置股价值,因此这取决于股票代码。
这里有另一个主干功能,通常我们会在构造函数上设置这个功能,但是主干为我们处理这个功能,我们所要做的就是在 Stock 定义中声明一个defaults
对象,为每个模型的属性提供默认值。让我们看看它是如何工作的:
var Stock = Backbone.Model.extend({
defaults: {
'sharePrice': 0
}
});
简单干净。
正如我们在第 3 章、测试前端代码中看到的,可观察模式是一个很好的解决方案,可以在减少耦合的同时实现两个视图之间的集成。在主干中,它的每一个四个抽象都建立在一个共享的事件基础设施上,使得默认情况下都是可观察的对象。在模型中,可以观察整个模型或单个属性的变化,在它与后端同步时得到通知(我们将在后面看到),甚至可以创建自定义事件。如果你熟悉如何在 jQuery 上监听事件,使用主干函数会感觉像在家里一样。
同样,让我们看一些代码来理解它是如何工作的:
var stock = new Stock();
stock.on('change:sharePrice', function () {
alert('it has changed!);
});
stock.set({ 'sharePrice': 30 })
我们使用的是前面定义的相同的股票模型。首先,我们通过on
函数添加一个观察器,监听股票实例上股价值的变化。您可以看到我们正在传递事件名称(change:sharePrice
)和事件发生后要回调的函数。
之后,我们更改股价属性的值。在幕后,Backbone 会注意到这一变化,并通知所有监听观察者,在您的浏览器上显示警报消息(it
已更改)。
这对于保持接口和模型的同步非常好,我们将在后面看到。但是,我们如何利用这一功能来实现模型本身的任何良好目的呢?
还记得投资对象吗?在将其转换为主干模型之后,我们可以重写它的一个规范来演示一个涉及模型事件的测试用例。
这里有一个很好的涉及roi
属性的候选人:
describe("when its stock share price valorizes", function() {
beforeEach(function() {
stock.set('sharePrice', 40);
});
it("should have a positive return of investment", function() {
expect(investment.get('roi')).toEqual(1);
});
});
roi
属性是所谓的虚拟属性,因为它是两个其他属性之间计算的结果。以前,我们选择像函数一样实现它,我们仍然可以,但是这样做我们将失去在所有模型属性之间有一个同构接口的好处,此外,roi
是一个常规属性,其他人也可以观察它的变化。
概括地说,投资回报是支付的股价与其当前价值之间的比率。我们可以创建一个函数来计算它,比如:
function calculateROI () {
var sharePrice = this.get('sharePrice');
var stockSharePrice = this.get('stock').get('sharePrice');
this.set('roi', (stockSharePrice - sharePrice) / sharePrice);
}
到目前为止,你可能已经知道如何让规范通过。我们要计算投资股票每次股价变动的投资回报。
为了在 Investment 对象中做到这一点,我们需要在创建投资后给stock
属性添加一个观察者:
var Investment = Backbone.Model.extend({
initialize: function () {
this.get('stock').on('change:sharePrice', calculateROI, this);
}
});
搞定了。在定义投资时,我们必须通过另一个配置来指定一个功能。它就像一个构造函数,一旦模型被实例化就被调用。
显然,这个实现并不完整,因为stock
属性本身可能会改变为不同的股票,这需要改变观察器的重新绑定。但我会把它留给你做练习。
本章我们将深入探讨骨干事件,但要获得完整的参考,请务必查看http://backbonejs.org/#Events的官方文档。
一个模型不会是一个没有某种机制来允许它被读取或保存到服务器的模型。正如你已经猜到的,这个实现对你来说将变得简单得多。
默认情况下,Backbone 附带了对实现REST
标准的后端服务器的支持,剩下要做的就是为它配置一个端点来发出请求,它会自动获取、更新、创建和删除模型。
目前,唯一依赖后端服务器的应用是带有fetch
功能的 Stock 模型。让我们看看 Backbone.js 在这件事上能提供什么。
下面是原始的规范实现,做了一些调整,使其更简单并与主干兼容:
describe("Stock", function() {
var stock;
beforeEach(function() {
stock = new Stock({ symbol: 'AOUE' });
});
describe("when fetched", function() {
var fakeServer;
beforeEach(function() {
fakeServer = sinon.fakeServer.create();
fakeServer.respondWith('/stocks/AOUE',
'{ "sharePrice": 20.13 }'
);
stock.fetch();
fakeServer.respond();
});
afterEach(function() {
fakeServer.restore();
});
it("should update its share price", function() {
expect(stock.get('sharePrice')).toEqual(20.13);
});
});
});
这是使用 Sinon 的实现。JS 假服务器见第六章、光速单元测试。在之前,我们必须自己实现这个fetch
功能,但是有了主干,我们所要做的就是在 Stock 定义上设置两个配置:
var Stock = Backbone.Model.extend({
idAttribute: 'symbol',
urlRoot: '/stocks'
});
这些属性是:
urlRoot
:这个表示主干执行 AJAX 请求需要进入的根 URLidAttribute
:这个表示在发出 AJAX 请求时,它必须使用模型的哪个属性作为标识
我们又一次完蛋了!规范应该通过了,房间里的每个人都应该高兴!
但是我们写的那个规范似乎显示了它的年龄,它比我们写的代码测试的要多得多。虽然这不是一件坏事,但我们可以通过相信主干正在正确实现其fetch
功能(假设我们提供了正确的参数)并重写规范来使事情变得更简单,现在我们已经熟悉了主干 API:
describe("Stock", function() {
var stock;
beforeEach(function() {
stock = new Stock({ symbol: 'AOUE' });
});
it("should allow fetching its information", function() {
expect(stock.idAttribute).toEqual('symbol');
expect(stock.urlRoot).toEqual('/stocks');
});
});
我们已经用两个断言替换了整个when fetched
描述函数的单个规范,同时仍然保证了提取函数的工作。
再一次,更少的代码和更少的规范。但是请注意,我们可以离开旧的规范实现。它确实提供了一点信心,因为你可以(理论上)错误地编写实现和规范,比如在两个文件上都错误地将 urlroot 类型为 urlRoot。
这是简单性和安全性之间的权衡。何时使用每种方法由你决定。
主干同步还有更多,比如支持本地存储或者 XML 请务必查看在http://backbonejs.org/#Sync提供的完整文档。
骨干集合基本上是有超能力的阵列:
- 它们带有一系列内置的可枚举函数,如 map、sort 和 select
- 它们支持各种事件,例如添加、删除,甚至对它包含的单个模型进行更改
- 它们支持从远程服务器读取数据
期望在您的主干应用中与模型结合使用它。
让我们看一个小代码片段来感受一下它是如何工作的。这里我们实例化一个新的集合,传递一个带有初始数据项的数组来启动它:
var collection = new Backbone.Collection([
{ id: 1, name 'first' }
]);
默认情况下,它将为该数组中的每个对象创建一个新的Backbone.Model
,但是可以指定您自己的自定义模型对象。
接下来,我们将展示如何向集合中添加新模型:
collection.add(new Backbone.Model({ id: 2, name: 'second' }));
最后,我们可以通过其id
属性来检索添加的模型:
var model = collection.get(1)
model.get('name') // first
它们还附带事件支持,因此可以监听集合中的更改:
collection.on('add', function (newModel) { })
以及更多的功能。
在我们挖掘一些特性以及如何测试主干集合之前,我们必须首先学习如何声明一个,为此我们需要一个例子。
我们将创建一个新的股票集合,它将需要一个新的源代码和规范文件,分别写在src/StockCollection.js
和spec/StockCollectionSpec.js
处。
在规范文件中,我们可以首先期望这个新的StockCollection
是一个主干集合,也是一个股票集合:
describe("StockCollection", function() {
var collection;
beforeEach(function() {
collection = new StockCollection();
});
it("should be a Backbone Collection", function() {
expect(collection).toEqual(jasmine.any(Backbone.Collection));
});
it("should be of Stocks", function() {
expect(collection.model).toBe(Stock);
});
});
定义集合包含哪个模型不是必需的,但是通过指定,集合知道如何创建模型的新实例,例如,在进行提取时。
下面是StockCollection
的实现:
(function (Backbone, Stock) {
var StockCollection = Backbone.Collection.extend({
model: Stock
});
this.StockCollection = StockCollection;
})(Backbone, Stock);
将StockCollection
作为基础Backbone.Collection
的延伸,并将其模型设置为库存。
与模型一样,也可以从集合中获取数据,除了我们在单个请求中检索一个或多个模型。
在StockCollection
中,我们希望它有一个fetch
功能来更新它的模型。为了使它更有趣,我们将添加一个要求,即我们必须根据集合中可用的股票向服务器发送我们想要的股票数据。
我们的服务器将期待一个包含股票标识查询字符串的网址,类似于(对于我们的开发服务器):http://0.0.0.0:8000/stocks/?ids[]=AOUE&ids[]=COUY
。
这个规范会稍微复杂一点,所以我们将从它的骨架开始,这样你就可以感觉到我们将如何编写它:
describe("StockCollection", function() {
describe("given a populated collection", function() {
describe("when fetch", function() {
it("should request by the Stocks it contains", function(){});
it("should update its models share price", function(){});
});
});
});
接下来,我们将向您展示如何实现每个片段,从given a populated collection
开始:
describe("given a populated collection", function() {
beforeEach(function() {
model1 = new Stock({ symbol: 'AOUE' });
model2 = new Stock({ symbol: 'COUY' });
collection = new StockCollection([
model1,
model2
]);
});
});
其中它创建了一个包含两个模型的集合。
接下来,我们需要执行fetch
功能。为了做到这一点,我们将使用西农。JS 假服务器:
describe("when fetch", function() {
beforeEach(function() {
fakeServer = sinon.fakeServer.create();
fakeServer.respondWith(JSON.stringify([
{
symbol: 'AOUE',
sharePrice: 20.13
},
{
symbol: 'COUY',
sharePrice: 14
}
]));
collection.fetch();
fakeServer.respond();
});
afterEach(function() {
fakeServer.restore();
});
});
您可以看到,这里我们正在为假服务器配置一个假响应,执行的fetch
功能,告诉假服务器响应所有请求。
最后,我们可以检查我们的收藏是否提出了正确的要求:
it("should have request by the Stocks it contains", function() {
// encoded '/stocks?ids[]=AOUE&ids[]=COUY'
var url = '/stocks?' + $.param({ ids: ['AOUE', 'COUY'] });
expect(fakeServer.requests[0].url).toEqual(url);
});
然后检查模型是否用假响应数据更新:
it("should update its models share price", function() {
expect(model1.get('sharePrice')).toEqual(20.13);
expect(model2.get('sharePrice')).toEqual(14);
});
就是这样。虽然内容广泛,但理解起来相当简单。但实际执行情况如何?为了切换回源文件,我们需要定义一个 URL 属性,供 Backbone.js 在发出请求时使用:
(function (Backbone, Stock) {
var StockCollection = Backbone.Collection.extend({
model: Stock,
url: function () {
return "/stocks" + idsQueryString.call(this);
}
});
function modelIds () {
return this.map(function (model) { return model.id; });
}
function idsQueryString () {
var ids = modelIds.call(this);
if (ids.length === 0) { return ''; }
return '?' + $.param({ ids: ids });
}
this.StockCollection = StockCollection;
})(Backbone, Stock);
就是这里。您可以通过高亮显示的部分看到,该网址是动态的,我们根据收藏中存储的模型的标识来构建它,非常符合我们的要求。
回到如何测试模型同步的解释,我们告诉你规范是如何过时的。这里也可能是这种情况。看看如何简化这个规范,同时又能保证它能工作。
主干收藏还有更多功能,如需了解更多信息,请务必查看位于http://backbonejs.org/#Collection的官方文档。
我们已经在第 3 章、测试前端代码中看到了使用视图模式的一些优势,并且已经在以这种方式创建我们的接口组件。那么《脊梁观》与我们目前所做的有什么不同呢?
它保留了许多我们讨论过的创建可维护的浏览器代码的最佳实践模式,但是使用了一些语法糖和自动化来使我们的生活更容易。
它们是 HTML 和模型之间的粘合代码,主干视图的主要职责是向接口添加行为,同时保持它与模型或集合同步。
正如我们将看到的,Backbone.js 最大的胜利是它如何实现易于处理的 DOM 事件委托,这是一项通常用 jQuery 完成的任务。
与我们到目前为止所看到的非常相似,声明一个新的视图将是一个扩展基础Backbone.View
对象的问题。
为了演示它是如何工作的,我们需要一个例子。我们将创建一个新的视图,它的职责是在屏幕上呈现单个投资。
我们将以允许第 3 章、测试前端代码中简要讨论的InvestmentListView
组件使用的方式创建它。
这是一个新的组件和规范,分别写在src/InvestmentView.js
和spec/InvestmentViewSpec.js
中。
在规范文件中,我们可以编写类似于之前所看到的内容:
describe("InvestmentView", function() {
var view;
beforeEach(function() {
view = new InvestmentView();
});
it("should be a Backbone View", function() {
expect(view).toEqual(jasmine.any(Backbone.View));
});
});
这转化为扩展基础Backbone View
组件的实现:
(function (Backbone) {
var InvestmentView = Backbone.View.extend()
this.InvestmentView = InvestmentView;
})(Backbone);
现在,我们准备探索 Backbone.js 提供的一些新功能。
像第 3 章、测试前端代码中描述的视图模式一样,主干视图也有一个包含对其 DOM 元素引用的属性。
这里的不同之处在于,Backbone.js 默认附带了它,提供了:
view.el
:DOM 元素view.$el
:该元素的 jQuery 对象view.$
:一个限定范围的 jQuery 查找函数(与我们实现的方式相同)
如果您没有在构造函数中提供元素,它会自动为您创建一个元素。当然,它创建的元素不会附加到文档中,要由视图的用户代码来附加它。
以下是使用视图时常见的模式:
-
实例化:
var view = new InvestmentView();
-
调用
render
函数绘制视图的组件(我们将在下一节中看到):view.render()
-
将其元素追加到页面文档中:
$('body').append(view.el);
考虑到我们对InvestmentView
的干净实现,如果您继续并在干净的页面上执行前面的代码,您将得到以下结果:
<body>
<div></div>
</body>
一个空的div
元素;这是主干创建的默认元素。但是我们可以通过InvestmentView
声明中的一些配置参数来改变这一点。
假设我们希望InvestmentView
的 DOM 元素是一个带有investment
CSS 类的列表项(li
)。我们可以使用熟悉的 Jasmine jQuery 匹配器来编写这个规范:
describe("InvestmentView", function() {
var view;
beforeEach(function() {
view = new InvestmentView();
});
it("should be a list item with 'investment' class", function() {
expect(view.$el).toBe('li.investment');
});
});
您可以看到我们没有使用setFixtures
函数,因为我们可以对视图中可用的元素实例运行这个测试。
现在来实施;我们所要做的就是在View
定义中定义两个简单的属性,主干将使用它们来创建视图的元素:
var InvestmentView = Backbone.View.extend({
className: 'investment',
tagName: 'li'
});
通过查看实现,您可能想知道我们是否应该像在主干模型:同步和 AJAX 请求部分那样测试它。在这里,我建议不要这样做,因为这种方法不会给你带来任何好处,因为这个规范更加可靠。
这很好,但是我们如何向 DOM 元素添加内容呢?这取决于我们接下来要看到的render
功能。
记住我们可以在构建视图时传递一个元素,就像我们在第 3 章、测试前端代码中所做的一样:
var view = new InvestmentView({ el: $('body') });
但是通过让视图处理它的呈现,我们可以获得更好的组件化,并且我们还可以获得性能。
现在我们理解了在视图上有一个空元素是一个好主意,我们必须进入如何在这个空画布上绘制的细节。
主干视图已经有了一个可用的 render
函数,但是它是一个虚拟的实现,所以由你来定义它是如何工作的。
回到InvestmentView
示例,让我们添加一个新的接受标准来描述它应该如何呈现。我们将从预期投资回报的百分比值开始。以下是规范实现:
describe("InvestmentView", function() {
var view, investment;
beforeEach(function() {
investment = new Investment();
view = new InvestmentView({ model: investment });
});
describe("when rendering", function() {
beforeEach(function() {
investment.set('roi', 0.1);
view.render();
});
it("should render the return of investment", function() {
expect(view.$el).toContainHtml('10%');
});
});
});
这是一个非常标准的规范,其中的概念我们之前已经见过,实现只是在InvestmentView
声明中定义render
函数的问题:
var InvestmentView = Backbone.View.extend({
className: 'investment',
tagName: 'li',
render: function () {
this.$el.html('<p>'+ formatedRoi.call(this) +'<p>');
return this;
}
});
function formatedRoi () {
return (this.model.get('roi') * 100) + '%';
}
它使用this.$el
属性向视图的元素添加一些 HTML 内容。关于render
功能的实现,有一些的细节需要注意:
-
我们正在使用
jQuery.html
函数,这样我们就可以多次调用render
函数,而不会复制视图的内容。 -
render
函数在完成渲染后返回视图实例。这是允许链式调用的常见模式,例如:$('body').append(new InvestmentView().render().el);
现在回到测试。您可以看到,我们没有测试特定的 HTML 片段,而是只呈现了 10%的文本。您本可以按照预期通过检查完全相同的 HTML 来完成一个更彻底的编写规范,但是这最终会增加测试的复杂性,而且没有什么好处。
我们理解视图是如何渲染的,模型事件是如何工作的,如果我们能把这些东西联系在一起,让视图在每次模型改变时都渲染自己,那不是很好吗?这正是我们能做的!
回到InvestmentView
等级库,我们可以添加一个新的等级库来检查模型更新后视图是否会自动渲染:
describe("when the investment changes", function() {
beforeEach(function() {
spyOn(view, 'render');
investment.trigger('change', investment);
});
it("should update the interface", function() {
expect(view.render).toHaveBeenCalled();
});
});
规范通过触发一个变化事件来工作,就像我们设置一个属性时模型所做的那样,然后期望调用render
函数。
看起来没问题,所以下一步是在InvestmentView
构造函数中添加实现:
var InvestmentView = Backbone.View.extend({
className: 'investment',
tagName: 'li'
initialize: function () {
this.model.on('change', this.render, this);
},
render: function () {
this.$el.html('<p>'+ formatedRoi.call(this) +'<p>');
return this;
}
});
就像其他主干抽象一样,构造函数可以通过在视图定义中添加initialize
函数来实现。
接下来,我们使用模型的事件基础结构将变更事件绑定到视图的render
函数:
this.model.on('change', this.render, this);
这个实现是正确的,但是如果你试图运行规范,它应该会失败,因为关于间谍如何工作的细节。
你看,通过监视视图的渲染函数,我们实际上是用一个spy
函数代替了它的原始实现。由于InvestmentView
将事件绑定在其构造函数上,因此它仍然绑定在原始的render
实现上,而不是我们的spy
上。
为了使这个测试工作,我们需要在实例化视图之前设置render
函数的间谍。在实例化视图之前,我们可以通过在InvestmentView
的原型中设置render
函数来实现这一点:
beforeEach(function() {
spyOn(InvestmentView.prototype, 'render');
investment = new Investment();
view = new InvestmentView({
model: investment
});
});
describe("when the investment changes", function() {
beforeEach(function() {
investment.trigger('change', investment);
});
it("should update the interface", function() {
expect(view.render).toHaveBeenCalled();
});
});
现在规范应该通过了!
在我们可以进入下一节之前,还有一件事很重要,那就是内存泄漏。通过添加该事件侦听器,视图和模型实例将被永远绑定,因此即使在销毁视图实例之后,在模型实例被释放之前,它也永远不会从内存中释放。
为了解决这个问题,Backbone.js 为其所有组件提供了另一个功能,允许监听其他组件事件; listenTo
功能。
因此,与其将事件处理程序添加到模型中:
this.model.on('change', this.render, this);
我们要求视图倾听模型上的事件:
this.listenTo(this.model, 'change', this.render, this);
通过使用listenTo
,视图知道它创建的所有事件处理程序,一旦被销毁,它就可以删除所有事件处理程序。
但是要使它工作,您必须记住,每当您使用完一个视图时,您可以通过调用视图的 remove
函数来显式删除它:
view.remove()
到目前为止,我们在每次模型改变时都有视图渲染和更新,但是一旦视图改变,更新模型呢?
这只是向视图 DOM 元素添加事件处理程序的问题,一旦它们被触发,我们就更新模型。
为了演示这个概念,让我们在InvestmentView
中添加另一个接受标准。我们想添加一个新按钮,一旦点击,就会触发投资模型的破坏。
为了能够点击销毁按钮,首先需要渲染它。让我们把这个规范写如下:
describe("InvestmentView", function() {
var view, investment;
beforeEach(function() {
investment = new Investment();
view = new InvestmentView({
model: investment
});
});
describe("when rendering", function() {
beforeEach(function() {
view.render();
});
describe("when the destroy button is clicked", function() {
beforeEach(function() {
spyOn(investment, 'destroy');
view.$('.destroy-investment').click();
});
it("should destroy the model", function() {
expect(investment.destroy).toHaveBeenCalled();
});
});
});
});
我们在investment.destroy
函数上添加一个间谍,然后我们模拟点击,最后期待destroy
函数被调用。
好的部分来了。通常情况下,您必须手动添加 jQuery 上的点击事件,但是使用主干,您所要做的就是向InvestmentView
定义添加一个 events
对象。
这个events
对象必须包含你想要绑定的 DOM 事件,在一个函数定义或者函数名旁边,主干为你绑定。
以下是InvestmentView
的代码:
var InvestmentView = Backbone.View.extend({
className: 'investment',
tagName: 'li',
events: {
'click .destroy-investment': function () {
this.model.destroy();
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render, this);
},
render: function () {
this.$el.html('<p>'+ formatedRoi.call(this) +'<p>');
return this;
}
});
这里你可以看到我们正在监听点击破坏投资按钮,并传递一个函数处理程序,调用模型的destroy
函数。
为了定义事件,我们需要传递我们想要监听的事件和元素。请记住,这都是视图的 DOM 元素的范围:
'click .destroy-investment'
但是这个定义也可以只接收一个事件类型,以防您想要向视图 DOM 元素本身添加事件。假设我们想让所有点击查看:
events: {
'click': function () {}
}
它还支持传递函数名,让主干在视图实例上查找函数定义,并为您调用它:
var InvestmentView = Backbone.View.extend({
events: {
'click .destroy-investment': 'destroyTheModel'
},
destroyTheModel: function () {
this.model.destroy();
},
});
您可能已经注意到的另一件事是负责调用您的事件处理程序,同时将this
值绑定到视图实例,这是一个您不必再担心的小细节。
随着应用代码向客户端的转移,我们不断看到更多的单页 web 应用。但是它们有一个缺点。由于我们将用户保持在同一个页面上,我们失去了通过网址变化跟踪步骤的自然能力。
Backbone.js 中的路由器是解决这个问题的方法。它们允许在您的 JavaScript 代码中定义路由,将网页的感觉反馈给您丰富的 web 应用。
假设我们希望我们的应用支持 URL 路由来显示不同类型的投资:
/investments/good
:仅显示良好投资的列表/investments/bad
:仅显示不良投资列表/investments/all
:显示所有投资的列表
通常这将在服务器上处理,但是使用 Backbone.js,我们可以定义如何在浏览器上用 JavaScript 处理这些路由。不过有一个问题,要做到这一点,它需要使用散列网址片段(#/investments
)。所以上面所有的网址实际上都是这样的http://0.0.0.0:8000#/investments/good
(在我们的开发服务器上)。
可以让主干使用新的历史应用编程接口,并支持实际的网址,但这个主题不在本书的范围内。
回到我们路由器的实现,我们可以看到所有这些路由都涉及投资。所以我们可以从创建一个新的InvestmentRouter
开始,有它的源文件和规范文件。
在规范文件中,我们可以从期望它是主干路由器开始:
describe("InvestmentsRouter", function() {
var router;
beforeEach(function() {
router = new InvestmentsRouter();
});
it("should be a Backbone Router", function() {
expect(router).toEqual(jasmine.any(Backbone.Router));
});
});
它在代码中被翻译成熟悉的主干组件声明:
(function (Backbone) {
var InvestmentsRouter = Backbone.Router.extend();
this.InvestmentsRouter = InvestmentsRouter;
})(Backbone);
那么我们如何继续实施这些路线呢?
我们可以采取的一种方法是让路由器文件尽可能简单,让它们处理 URL 和触发事件,这样应用就可以处理用户请求。
为了更好地理解它的含义,让我们看看关于'/investments/good'
路线的说明:
describe("InvestmentsRouter", function() {
var router, observer;
beforeEach(function() {
router = new InvestmentsRouter();
observer = jasmine.createSpy();
});
it("should route '/investments/good'", function() {
router.on('route:goodInvestments', observer);
Backbone.history.loadUrl('/investments/good')
expect(observer).toHaveBeenCalled();
});
});
让我们看看这个规范的作用:
-
首先,它使用熟悉的事件基础设施向
route:goodInvestment
事件添加一个观察者:router.on('route:goodInvestments', observer);
-
然后,我们使用主干来模拟一个 URL 请求:
Backbone.history.loadUrl('/investments/good');
-
最后,我们期望我们的观察者被称为:
expect(observer).toHaveBeenCalled();
要在路由器上实现,需要向路由对象添加一个新的条目:
var InvestmentsRouter = Backbone.Router.extend({
routes: {
'investments/good': 'goodInvestments'
}
});
我们通过匹配的模式和名称来定义路线,在本例中分别是investments/good
和goodInvestments
。
这就是测试和编写路由器的方法。
我打赌你对我们如何将它集成到应用中很好奇。下面是一个片段,展示了如何做到这一点:
this.router = new InvestmentsRouter();
this.router.on('route:goodInvestments', function () {
this.applicationView.showGoodInvestments();
}, this);
Backbone.history.start();
最后一行很重要;它告诉 Backbone.js 开始监听网址变化,有效地启用路由器。
有一个常见的错误,骨干初学者试图从路线驱动应用开发。这是 web 应用中的一种常见模式,对于用户所做的每一个动作,都需要一个新的 URL 请求。但现在已经不是这样了。您正在开发一个丰富的网络应用,它更像一个本地应用。
因此,从您的视图中驱动您的代码,您总是可以在以后添加路由。
在这一章中,您已经看到了如何使用主干来做一些繁重的工作,让您能够更加专注于应用代码。在测试用它们开发的应用时,您已经了解了四个主干抽象和一些常见场景。
我向您展示了事件的力量,以及它们如何让不同组件之间的集成变得更加容易,让您能够保持模型和视图的同步。
您还看到了如何利用这些强大的抽象,以及如何编写更少的代码和规范。您还面临着这样的决定:编写更简单的规范,并相信主干实现是正确的。
到目前为止,您应该对使用主干创建新的单页应用感到满意,我希望已经向您展示了如何考虑测试构建在框架之上的应用。在下一章中,我们将看到如何打包和缩小应用代码。