是时候给这些网页来点动态特性了——用AngularJS!我们这里为后面要加入的控制器加入了一个測试。
一个应用的代码架构有非常多种。
对于AngularJS应用。我们鼓舞使用模型-视图-控制器(MVC)模式解耦代码和分离关注点。
考虑到这一点。我们用AngularJS来为我们的应用加入一些模型、视图和控制器。
请重置工作文件夹:
git checkout -f step-2
我们的应用如今有了一个包括三部手机的列表。
步骤1和步骤2之间最重要的不同在以下列出。。你能够到GitHub去看完整的区别。
视图和模板
在AngularJS中,一个视图是模型通过HTML**模板**渲染之后的映射。这意味着。不论模型什么时候发生变化,AngularJS会实时更新结合点,随之更新视图。
比方,视图组件被AngularJS用以下这个模板构建出来:
<html ng-app>
<head>
...
<script src="lib/angular/angular.js"></script>
<script src="js/controllers.js"></script>
</head>
<body ng-controller="PhoneListCtrl">
<ul>
<li ng-repeat="phone in phones">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
</body>
</html>
我们刚刚把静态编码的手机列表替换掉了。由于这里我们使用ngRepeat指令和两个用花括号包裹起来的AngularJS表达式——{{phone.name}}
和{{phone.snippet}}
——能达到相同的效果。
模型和控制器
在PhoneListCtrl
控制器里面初始化了数据模型(这里仅仅只是是一个包括了数组的函数。数组中存储的对象是手机数据列表):
app/js/controller.js:
function PhoneListCtrl($scope) {
$scope.phones = [
{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S."},
{"name": "Motorola XOOM? with Wi-Fi",
"snippet": "The Next, Next Generation tablet."},
{"name": "MOTOROLA XOOM?",
"snippet": "The Next, Next Generation tablet."}
];
}
虽然控制器看起来并没有起到什么控制的作用,可是它在这里起到了至关关键的数据。
通过给定我们数据模型的语境,控制器同意我们建立模型和视图之间的数据绑定。
我们是这样把表现层。数据和逻辑部件联系在一起的:
AngularJS的作用域理论很重要:一个作用域能够视作模板、模型和控制器协同工作的粘接器。
AngularJS使用作用域,同一时候还有模板中的信息。数据模型和控制器。这些能够帮助模型和视图分离,可是他们两者确实是同步的!不论什么对于模型的更改都会即时反映在视图上。不论什么在视图上的更改都会被立马体如今模型中。
想要更加深入理解AngularJS的作用域,请參看AngularJS作用域文档。
測试
“AngularJS方式”让开发时代码測试变得十分简单。
让我们来瞅一眼以下这个为控制器新加入的单元測试:
test/unit/controllersSpec.js:
describe(‘PhoneCat controllers‘, function() {
describe(‘PhoneListCtrl‘, function(){
it(‘should create "phones" model with 3 phones‘, function() {
var scope = {},
ctrl = new PhoneListCtrl(scope);
expect(scope.phones.length).toBe(3);
});
});
});
这个測试验证了我们的手机数组里面有三条记录(临时无需弄明确这个測试脚本)。
这个样例显示出为AngularJS的代码创建一个单元測试是多么的easy。
正由于測试在软件开发中是不可缺少的环节,所以我们使得在AngularJS能够轻易地构建測试。来鼓舞开发人员多写它们。
在写測试的时候。AngularJS的开发人员倾向于使用Jasmine行为驱动开发(BBD)框架中的语法。虽然AngularJS没有强迫你使用Jasmine,可是我们在教程里面全部的測试都使用Jasmine编写。
你能够在Jasmine的官方主页或者Jasmine
Wiki上获得相关知识。
基于AngularJS的项目被预先配置为使用JsTestDriver来执行单元測试。你能够像以下这样执行測试:
- 在一个单独的终端上。进入到
angular-phonecat
文件夹而且执行./scripts/test-server.sh
来启动測试(Windows命令行下请输入.\scripts\test-server.bat
来执行脚本,后面脚本命令执行方式类似); - 打开一个新的浏览器窗体,而且转到http://localhost:9876 。
-
选择“Capture this browser in strict mode”。
这个时候,你能够抛开你的窗体无论然后把这事忘了。JsTestDriver会自己把測试跑完而且把结果输出在你的终端里。
-
执行./scripts/test.sh
进行測试
。
你应当看到类似于例如以下的结果:
Chrome: Runner reset.
.
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (2.00 ms)
Chrome 19.0.1084.36 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (2.00 ms)
耶。測试通过了!或者没有... 注意:假设在你执行測试之后发生了错误。关闭浏览器然后回到终端关了脚本,然后在又一次来一边上面的步骤。
练习
-
为index.html
加入还有一个数据绑定。
比如:
<p>Total number of phones: {{phones.length}}</p>
-
创建一个新的数据模型属性,而且把它绑定到模板上。比如:
$scope.hello = "Hello, World!"
更新你的浏览器。确保显示出来“Hello, World!”
-
用一个迭代器创建一个简单的表:
<table>
<tr><th>row number</th></tr>
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i}}</td></tr>
</table>
如今让数据模型表达式的i
添加1:
<table>
<tr><th>row number</th></tr>
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
</table>
-
确定把toBe(3)
改成toBe(4)
之后单元測试失败。然后又一次跑一遍./scripts/test.sh
脚本
迭代器过滤
我们在上一步做了非常多基础性的训练,所以如今我们能够来做一些简单的事情喽。
我们要增加全文检索功能(没错,这个真的非常easy!)。
同一时候。我们也会写一个端到端測试。由于一个好的端到端測试能够帮上非常大忙。它监视着你的应用,而且在发生回归的时候迅速报告。
请重置工作文件夹:
git checkout -f step-3
我们的应用如今有了一个搜索框。
注意到页面上的手机列表随着用户在搜索框中的输入而变化。
步骤2和步骤3之间最重要的不同在以下列出。
你能够在GitHub里看到完整的区别。
控制器
我们对控制器不做不论什么改动。
模板
app/index.html
<div class="container-fluid">
<div class="row-fluid">
<div class="span2">
<!--Sidebar content-->
Search: <input ng-model="query">
</div>
<div class="span10">
<!--Body content-->
<ul class="phones">
<li ng-repeat="phone in phones | filter:query">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
</div>
</div>
</div>
我们如今加入了一个<input>
标签,而且使用AngularJS的$filter函数来处理ngRepeat指令的输入。
这样同意用户输入一个搜索条件,立马就能看到对电话列表的搜索结果。我们来解释一下新的代码:
-
数据绑定: 这是AngularJS的一个核心特性。当页面载入的时候。AngularJS会依据输入框的属性值名字。将其与数据模型中同样名字的变量绑定在一起。以确保两者的同步性。
在这段代码中,用户在输入框中输入的数据名字称作query
,会立马作为列表迭代器(phone
in phones | filter:
query`)其过滤器的输入。当数据模型引起迭代器输入变化的时候,迭代器能够高效得更新DOM将数据模型最新的状态反映出来。
測试
在步骤2,我们学习了编写和执行一个測试的方法。
单元測试用来測试我们用js编写的控制器和其它组件都很方便,可是不能方便的对DOM操作和应用集成进行測试。
对于这些来说,端到端測试是一个更好的选择。
搜索特性是全然通过模板和数据绑定实现的。所以我们的第一个端到端測试就来验证这些特性是否符合我们的预期。
test/e2e/scenarios.js:
describe(‘PhoneCat App‘, function() {
describe(‘Phone list view‘, function() {
beforeEach(function() {
browser().navigateTo(‘../../app/index.html‘);
});
it(‘should filter the phone list as user types into the search box‘, function() {
expect(repeater(‘.phones li‘).count()).toBe(3);
input(‘query‘).enter(‘nexus‘);
expect(repeater(‘.phones li‘).count()).toBe(1);
input(‘query‘).enter(‘motorola‘);
expect(repeater(‘.phones li‘).count()).toBe(2);
});
});
});
虽然这段測试代码的语法看起来和我们之前用Jasmine写的单元測试很像。可是端到端測试使用的是AngularJS端到端測试器提供的接口。
执行一个端到端測试。在浏览器新标签页中打开以下随意一个:
这个測试验证了搜素框和迭代器被正确地集成起来。
你能够发现,在AngularJS里写一个端到端測试多么的简单。虽然这个样例不过一个简单的測试。可是用它来构建不论什么一个复杂、可读的端到端測试都非常easy。
练习
- 在
index.html
模板中加入一个{{query}}
绑定来实时显示query
模型的当前值,然后观察他们是怎样依据输入框中的值而变化。 -
如今我们来看一下我们怎么让query
模型的值出如今HTML的页面标题上。
你也许觉得像以下这样在title
标签上加上一个绑定即可了:
<title>Google Phone Gallery: {{query}}</title>
可是,当你重载页面的时候,你根本没办法得到期望的结果。这是由于query
模型只在body
元素定义的作用域内才有效。
<body ng-controller="PhoneListCtrl">
假设你想让<title>
元素绑定上query
模型,你必须把ngController
声明移动到HTML
元素上,由于它是title
和body
元素的共同祖先。
<html ng-app ng-controller="PhoneListCtrl">
一定要注意把body
元素上的ng-controller
声明给删了。
当绑定两个花括号在title
元素上能够实现我们的目标。可是你也许发现了,页面正载入的时候它们已经显示给用户看了。一个更好的解决方式是使用ngBind或者ngBindTemplate指令。它们在页面载入时对用户是不可见的:
<title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
-
在test/e2e/scenarios.js
的describe
块中增加以下这些端到端測试代码:
it(‘should display the current filter value within an element with id "status"‘,
function() {
expect(element(‘#status‘).text()).toMatch(/Current filter: \s*$/);
input(‘query‘).enter(‘nexus‘);
expect(element(‘#status‘).text()).toMatch(/Current filter: nexus\s*$/);
//alternative version of the last assertion that tests just the value of the binding
using(‘#status‘).expect(binding(‘query‘)).toBe(‘nexus‘);
});
刷新浏览器,端到端測试器会报告測试失败。为了让測试通过。编辑index.html
,加入一个id为“status”
的div
或者p
元素。内容是一个query
绑定。再加上Current
filter:
前缀。
比如:
<div id="status">Current filter: {{query}}</div>
-
在端到端測试里面加一条pause();
语句,又一次跑一遍。你将发现測试器暂停了!这样同意你有机会在測试执行过程中查看你应用的状态。測试应用是实时的!
你能够更换搜索内容来证明。稍有经验你就会知道,这对于在端到端測试中迅速找到问题是多么的关键。
双向绑定
在这一步你会添加一个让用户控制手机列表显示顺序的特性。动态排序能够这样实现。加入一个新的模型属性,把它和迭代器集成起来,然后让数据绑定完毕剩下的事情。
请重置工作文件夹:
git checkout -f step-4
你应该发现除了搜索框之外,你的应用多了一个下来菜单,它能够同意控制电话排列的顺序。
步骤3和步骤4之间最重要的不同在以下列出。你能够在GitHub里看到完整的区别。
模板
app/index.html
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
我们在index.html中做了例如以下更改:
- 首先,我们添加了一个叫做
orderProp
的<select>
标签,这样我们的用户就能够选择我们提供的两种排序方法。
AngularJS在select
元素和orderProp
模型之间创建了一个双向绑定。而后,orderProp
会被用作orderBy
过滤器的输入。
正如我们在步骤3中讨论数据绑定和迭代器的时候所说的一样。不管什么时候数据模型发生了改变(比方用户在下拉菜单中选了不同的顺序),AngularJS的数据绑定会让视图自己主动更新。
没有不论什么笨拙的DOM操作!
控制器
app/js/controllers.js:
function PhoneListCtrl($scope) {
$scope.phones = [
{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S.",
"age": 0},
{"name": "Motorola XOOM? with Wi-Fi",
"snippet": "The Next, Next Generation tablet.",
"age": 1},
{"name": "MOTOROLA XOOM?",
"snippet": "The Next, Next Generation tablet.",
"age": 2}
];
$scope.orderProp = ‘age‘;
}
- 我们改动了
phones
模型—— 手机的数组 ——为每个手机记录其添加了一个age
属性。我们会依据age
属性来对手机进行排序。 -
我们在控制器代码里加了一行让orderProp
的默认值为age
。假设我们不设置默认值。这个模型会在我们的用户在下拉菜单选择一个顺序之前一直处于未初始化状态。
如今我们该好好谈谈双向数据绑定了。注意到当应用在浏览器中载入时,“Newest”在下拉菜单中被选中。
这是由于我们在控制器中把orderProp
设置成了‘age’。所以绑定在从我们模型到用户界面的方向上起作用——即数据从模型到视图的绑定。如今当你在下拉菜单中选择“Alphabetically”,数据模型会被同一时候更新,而且手机列表数组会被又一次排序。这个时候数据绑定从还有一个方向产生了作用——即数据从视图到模型的绑定。