在这一步,我们将修改我们应用读取数据
我们定义一个自定义服务代表一个RESTFULL客户端,使用客户端我们就能用更容易的方法去请求数据服务。不用低层次的$httpAPI,HTTP方法和URLs
工作空间重置介绍 重置你的工作区间到第十一步
git checkout -f step-11
刷新你的浏览器,或者在线上检出这一步:第十一步例子。 大多数重要的修改列在下面,你可以在 GitHub上看到全部不同。
RESTFULL功能由Angular的 ngResource
模块提供,与Angular框架核心分离分布。 我们使用 Bower安装客户端依赖,这步更新 bower.json
配置来包含新的依赖。
{ "name": "angular-seed", "description": "A starter project for AngularJS", "version": "0.0.0", "homepage": "https://github.com/angular/angular-seed", "license": "MIT", "private": true, "dependencies": { "angular": "~1.3.0", "angular-mocks": "~1.3.0", "bootstrap": "~3.1.1", "angular-route": "~1.3.0", "angular-resource": "~1.3.0" } }
新依赖 "angular-resource": "~1.3.0"
告诉bower安装 angular-resource
组件兼容1.3.x的版本。我们必须寻问bower去下载并安装这个依赖,我们可以这样做:
npm install
警告:如果你最后运行npm install
后,一个新的Angular版本发布,你用bower intall
可以有问题。你需要安装的Angular版本可能会有冲突。如果你遇到,在运行 npm install
之前,你可以简单地删除你的app/bower_components
目录。 注意:如果你已经全局安装了bower,你可能运行 bower insatll
,但是对于这个项目我们已经预配置npm install
给我们运行bower。
我们自定义的资源服务将会定义在 app/js/service.js
中,所有我们必须包含这个文件到布局模板,另外,我们也需要读取 angular-resource.js
文件,他包含了 ngResource模块。 app/index.html.
... <script src="bower_components/angular-resource/angular-resource.js"></script> <script src="js/services.js"></script> ...
我们创建了我们自己的服务提供对服务器上手机数据的访问。 app/js/services.js.
var phonecatServices = angular.module('phonecatServices', ['ngResource']); phonecatServices.factory('Phone', ['$resource', function($resource){ return $resource('phones/:phoneId.json', {}, { query: {method:'GET', params:{phoneId:'phones'}, isArray:true} }); }]);
通过工厂函数,我们使用了模块API去注册一个自定义的服务,我们通过了名字为Phone的服务和工厂函数,工厂函数类似于控制的构造器,他们都通过函数参数定义了要注入的依赖,Phone服务在 $resource
服务上定义了依赖。 $resource服务只用几千代码就让创建 RESTFULL客户端变得容易,这个客户端可以用于我们的程序代替低层的 $http服务。 app/js/app.js.
... angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']). ...
我们需要增加一个 phonecateServices
模块依赖到 phonecatApp
模块必须数组
我们通过分离出低层的$ http
服务,替换名叫Phone的新服务,简化了我们的子控制器( PhoneListCtrl
和 PhoneDetaialCtrl
)。Angular的 $resource
比 $http更容易交互暴露数据像RESTFULL客户端资源,现在也更容易懂得我们控制器中代码做了什么。 app/js/controllers.js.
var phonecatControllers = angular.module('phonecatControllers', []); ... phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) { $scope.phones = Phone.query(); $scope.orderProp = 'age'; }]); phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) { $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { $scope.mainImageUrl = phone.images[0]; }); $scope.setImage = function(imageUrl) { $scope.mainImageUrl = imageUrl; } }]);
注意我们怎么替换PhoeListCtrl
$http.get('phones/phones.json').success(function(data) { $scope.phones = data; });
通过
$scope.phones = Phone.query();
这是一个简单的语句,我们想查询所有的手机。 在上面的代码中,需要注意一个重要的事,当我们执行手机服务中的方法时,我们没有通过任何回调函数。虽然他很像结果同步返回。但那不是所有的情况。返回的future对象是什么?——一个对象,当XHR响应返回时就会填充。因为Angular的数据绑定,我们能使用future,并绑定到我们的模板,这样,当数据到达,视图将会被自动更新。 有些时候,单独依靠future对象和数据绑定不满足我们做我们所需的事,在这些案例中,我们可以增加一个回调处理服务器响应, PhoneDetaialCtrl
通过在回调中设置一个 mainImageUrl
阐明了这个。
因为我们现在使用了 ngResource模块,所以必须更新我们的Karma配置,使用 angular-resource
我们的测试才会通过。 test/karma.conf.js:
files : [ 'app/bower_components/angular/angular.js', 'app/bower_components/angular-route/angular-route.js', 'app/bower_components/angular-resource/angular-resource.js', 'app/bower_components/angular-mocks/angular-mocks.js', 'app/js/**/*.js', 'test/unit/**/*.js' ],
我们修改了我们的测试验证我们新服务像期盼的一样发起了HTTP请求,并处理了他们。测试也检查了我们的控制器与服务是交互正确的。 $resource服务的参数响应对象,对象包含更新与删除资源的方法,如果我们使用标准的 toEqual
匹配器,我们的测试将会失败,因为测试值将不会如期盼的一样响应。为了解决这个问题,我们使用新定义的 toEqualData
Jasmine匹配器,当 toEqualData
匹配两个对象,他只会比对帐号的属性,而忽略方法。 test/unit/controllersSpec.js:
describe('PhoneCat controllers', function() { beforeEach(function(){ this.addMatchers({ toEqualData: function(expected) { return angular.equals(this.actual, expected); } }); }); beforeEach(module('phonecatApp')); beforeEach(module('phonecatServices')); describe('PhoneListCtrl', function(){ var scope, ctrl, $httpBackend; 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}); })); it('should create "phones" model with 2 phones fetched from xhr', function() { expect(scope.phones).toEqualData([]); $httpBackend.flush(); expect(scope.phones).toEqualData( [{name: 'Nexus S'}, {name: 'Motorola DROID'}]); }); it('should set the default value of orderProp model', function() { expect(scope.orderProp).toBe('age'); }); }); describe('PhoneDetailCtrl', function(){ var scope, $httpBackend, ctrl, xyzPhoneData = function() { return { name: 'phone xyz', images: ['image/url1.png', 'image/url2.png'] } }; beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData()); $routeParams.phoneId = 'xyz'; scope = $rootScope.$new(); ctrl = $controller('PhoneDetailCtrl', {$scope: scope}); })); it('should fetch phone detail', function() { expect(scope.phone).toEqualData({}); $httpBackend.flush(); expect(scope.phone).toEqualData(xyzPhoneData()); }); }); });
你会在Karma标签页下面的输出。
Chrome 22.0: Executed 5 of 5 SUCCESS (0.038 secs / 0.01 secs)
现在我们已经学习怎样构建一个像RESTFULL客户端的自定义服务,在 第十二步我们准备学习怎样使用动画提升我们的应用。