之前几章中,我们使用的3个手机数据集都是硬编码的。下面让我们使用Angular自带的一个叫$http
的service
来从远程服务器上获取一个较大的数据集。我们将使用Angular的依赖注入(DI)
为PhoneListCtrl
控制器注入$http
服务。
下面我们把代码切换到step-5:
git checkout -f step-5
刷新浏览器查看效果。也可以点这里在线看效果。
数据
项目文件中的app/phones/phones.json
里是JSON格式的手机信息列表,数据量比之前的要多。文件内容像下面这样:
[
{
"age": 13,
"id": "motorola-defy-with-motoblur",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
"snippet": "Are you ready for everything life throws your way?"
...
},
...
]
控制器
我们将使用Angular的$http
服务(在控制器中)来发起一个HTTP请求,从而从我们的网站应用服务器获取app/phones/phones.json
文件。 Angular自带了一些服务用于完成一些通用的功能,$http
是其中之一。在你需要的时候,Angualr会为你注入它们。
服务通过Angualr的DI 子系统
来管理。依赖注入
功能不仅可以帮你组织好网站应用架构(比如,把控件、表示层、数据和控制分开),还可以让应用变得松耦合(组件之间的依赖不会由组件自身来控制,而是由DI系统统一管理)。
app/js/controllers.js:
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
});
$http
发起了一个HTTP GET请求,从我们服务器上获取phones/phones.json
文件(这个目录是相对于index.html
文件的)。服务器将返回文件里的json内容。(其实服务器完全可以动态的产生数据,比如从数据库读取。对于浏览器来说,都一样,这个例子中我们为了教程简单易懂,所以用了一个json文件来做演示。)
$http
服务通过success
方法返回一个约定的数据对象。我们通过调用这个函数,来异步的处理HTTP返回并且把手机数据加入控制器域中的phones
数据模型。注意这个过程是由Angular在后台自动完成处理和解析的。
在Angular中,使用一个服务很简单,只需把服务的名字作为入参传给控制器的构造器函数。
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...}
通过上述代码,Angular在构造控制器时同时会为你的控制器提供以下这些服务($scope, $http
)。Angular还会自动把你想注入的服务所依赖的服务递归的引用进来。
注意参数的名字很重要,因为注入器将根据它们来寻找对应的服务。
$前缀命名公约
你可以创建自己定制的服务,我们再第十三章中也会教大家去做。Angular有一个服务命名公约,对于Angular自带的服务,我们一般使用 $
作为前缀。当然你也可以自己创建以$
开头的服务,为了防止冲突,我们希望大家最好不要这么做。
另外,在有些Scope中,你会发现有些属性变量是以$$
开头的,这代表这是一个私有属性,应该避免访问和修改它。
最小化JS时的注意事项
由于Angular是根据控制器构造函数的入参来加载控制器所依赖的对象的,所以你一旦对JS代码进行了最小化(注解:应该就是所谓的JS压缩),函数的参数也会被最小化(注解:JS压缩一般会把长变量使用简单字母来替换,从而缩小JS代码),这样一来,Angular就无法正确加载对应的依赖了。
我们可以通过用依赖的名字字符串来注解函数(因为字符串是不会被压缩的),从而解决这个问题。我们有两种方法来实现注解注入:
- 在控制器定义函数上方创建一个
$inject
字符串数组属性,用来存放依赖服务的名字字符串。本例中代码如下:
function PhoneListCtrl($scope, $http) {...}
PhoneListCtrl.$inject = ['$scope', '$http'];
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
- 使用内联注解:为控制器构造函数传入一个数组,而不是一个简单的函数。这个数组包含了服务的名称以及放在最后的函数本身,如下:
function PhoneListCtrl($scope, $http) {...}
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);
上述两种方案对于任何能被Angular注入的函数都适用,所以使用那种方法完全取决于你项目的风格。
当大家使用第二种方案的时候,我们一般把函数的定义直接放在数组最后一个元素内。如下:
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]);
对应本例中的代码:
app/js/controllers.js
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http',
function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
}]);
测试
见test/unit/controllersSpec.js
因为我们开始使用依赖注入并且控制器也有了自己的依赖,所以在测试中构造控制器也变得复杂起来了。我们将使用new
操作符来提供一个包含$http
伪实现的构造器。然而,Angular已经提供了一个$http
mock服务,我们可以直接在单元测试中使用。我们可以通过调用$httpBackend
服务中的函数来设置请求返回,从而达到mock数据的功能。代码如下:
describe('PhoneCat controllers', function() {
describe('PhoneListCtrl', function(){
var scope, ctrl, $httpBackend;
// Load our app module definition before each test.
beforeEach(module('phonecatApp'));
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
// This allows us to inject a service but then attach it to a variable
// with the same name as the service in order to avoid a name conflict.
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('phones/phones.json').
respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
scope = $rootScope.$new();
ctrl = $controller('PhoneListCtrl', {$scope: scope});
}));
注意:因为我们已经在测试环境中加载了Jasmine和angular-mocks.js, 我们可以直接使用
module
和inject
两个帮助函数
来访问和配置注入器。
我们在测试中是这样创建控制器的:
- 我们使用
inject
这个帮助函数来注入$rootScope
、controller
、$httpBackend
这三个服务实例到Jasmine的beforeEach函数中。这些实例在每个单独的测试中都会重新创建。这保证了每个测试的初始条件是一致的,并且互相之间是独立开的。 - 我们为我们的控制器新创建了一个
域
,即$rootScope.$new()
- 我们通过控制器的名字
PhoneListCtrl
来调用被注入测试的$controller
函数,并且以创建好的`域作为参数。
因为我们的控制器代码使用了$http
服务来获取手机信息列表数据,在我们创建控制器PhoneListCtrl
域之前,我们需要告诉对应的测试模块接受来自控制器的请求:
- 通过
beforeEach
函数中注入的$httpBackend
服务,我们可以向它发送请求。httpBackend
服务所模拟的是真实生产环境中的用来处理所有XHR(异步请求)和JSONP请求的服务。httpBackend
服务可以让你轻松的编写请求测试,而不用调用一些本地API和负责的全局上下文参数——这两个东西都会让编写测试变成噩梦。 - 我们使用
$httpBackend.expectGET
来“训练”$httpBackend
服务,告诉它当它收到一个HTTP请求的时候,应该返回什么。注意,你必须调用$httpBackend.flush
来完成HTTP应答。
在该测试用例中,收到HTTP回复之前,我们断言scope域
中不包含phones
数据模型:
it('should create "phones" model with 2 phones fetched from xhr', function() {
expect(scope.phones).toBeUndefined();
$httpBackend.flush();
expect(scope.phones).toEqual([{name: 'Nexus S'},
{name: 'Motorola DROID'}]);
});
- 我们通过调用
$httpBackend.flush()
来清空浏览器中的请求队列。这将促使$http
服务返回对应的回复。点这里可以了解为什么这句代码很重要。 - 我们现在可以断言,scope域中已经包含手机数据模型了。
最后我们再看看orderProp的默认值是否设置正确了:
it('should set the default value of orderProp model', function() {
expect(scope.orderProp).toBe('age');
});
你将可以在Karma的终端输出中看到以下信息:
Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs)
实验
在index.html
的底部,添加:
<pre>{{phones | filter:query | orderBy:orderProp | json}}</pre>
这个绑定用来查看JSON格式的手机信息列表。
在PhoneListCtrl
控制器中,预处理HTTP回复,使得手机列表中只含有5条记录。把下面的代码加到$http
回调函数中:
$scope.phones = data.splice(0, 5);
总结
现在你已经了解了使用Angular的服务是多么的简单(谢谢Angular的依赖注入)。下一章我们将添加为手机添加一些缩略图以及链接。
网友评论