离线下载
PDF版 ePub版

樊潇洁 · 更新于 2017-12-11 08:00:55

XHR和依赖性注入

在硬编码的数据集中有三款手机的数据,建立一个应用程序足够了!让我们使用Angular内建的服务之一,$http从服务器上取得更大的数据集我们将使用Angular的依赖性注入(DI)来为PhoneListCtrl控制器提供服务。

  • 现在有一个20个电话的列表,从服务器载入。

把工作空间重置到第五步

git checkout -f step-5

刷新你的浏览器或在线检查这一步:Step 5 Live Demo

下面列出了第四步和第五步之间的最重要的区别。你可以在GitHub里看到完整的差异。

数据

在你的项目中,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服务向你的Web服务器发出HTTP请求,取回app/phones/phones.json文件中的数据。$http是几个用Web应用中来处理常见的操作的内建Angular服务之一。Angular在你需要的地方为你注入了这些服务。

Angular的DI子系统负责管理这些服务。依赖性注入有用助于你的web应用既结构完好(例如,分离表现层、数据和控制三者)以及松弛的耦合(不能由组件自身解决的组件之间的依赖性问题,由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向你的Web服务器发出一个HTTP GET请求,要求phones/phones.json(该url相对于我们的index.html文件)。服务器在json文件中提供该数据,以响应该请求。(响应可能是由后端服务器动态生成的。但是在浏览器和我们的应用看来,它们没什么不同。为了简单起见,我们在本教程中使用了一个json文件。)

$http服务返回了一个promise对象?,带有success方法。我们调用这个方法以处理异步响应,并假定该作用域的手机数据由该控制器控制,作为一个模块,称为phones。注意Angular侦测了该json响应,并为我们解析了它。

要想在Angular中使用一个服务,你只要声明你所需要的依赖性的名字,作为控制器的构造函数的参数,如下所示:

phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...}

在构造控制器时,Angular的依赖性注入器会把这些服务注入到你的控制器中。这些依赖性控制器还负责创建该服务可能需要的任何传递依赖性(一个服务通常会依赖于其它服务)。

注意,参数的名称非常重要,因为注入器会用这些名称去查阅依赖性。

$前缀名称约定

你可以创建你自己的服务,而且实际上我们将在第十一步做这个。作为一个命名约定,Angular的内建服务,作用域方法以及一些别的Angular API在命名前面使用一个$前缀。

Angular提供的服务的命名空间有$前缀。要想避免冲突,最好避免把你的服务和模块命名成带有$前缀。

如果你检查一个作用域,你可能还会注意到一些属性以$$开头。这些属性被视为是私有属性,不能访问或者修改。

在极简化上的一个注记

因为Angular从参数的名称调用控制器的依赖性到控制器构造器的函数,如果你打算为PhoneListCtrl控制器缩小JavaScript代码,所有的函数参数都会被压缩,而且依赖性注入器将不能正确的识别服务。

我们可以克服这个问题,通过用依赖性的名称注释这个函数,作为字符串提供,它不会被压缩。提供这种注入注释有两种方法:

  • 在控制器函数中创建一个$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) {...}]);

从此刻开始,我们将在本教程中使用内联方法。考虑到这一点,让我们把注释加到PhoneListCtrl上:

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服务,我们可以用在单元测试中。我们通过调用一个称为$httpBackend服务上的方法,为服务器请求配置了“假的”响应。

describe('PhoneCat controllers', function() {

  describe('PhoneListCtrl', function(){
    var scope, ctrl, $httpBackend;

    // 在每次测试之前载入我们的应用模块定义
    beforeEach(module('phonecatApp'));

    // 注入器会忽略前面和后面的下划线(例如_$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});
    }));

注意:因为我们在测试环境中载入了Jasmine以及angular-mocks.js,我们得到了两个辅助方法moduleinject,用来访问和配置注入器。

我们在测试环境中创建控制器,如下所示:

  • 我们使用inject辅助方法,向Jasmine的beforeEach函数注入$rootScope$controller$httpBackend服务的实例,这些实例来自于一个注入器,在每一个测试内部都会被重新创建这个注入器。这保证了每次测试都从一个众所周知的起点开始,每次测试与其它测试相互独立。
  • 通过调用$rootScope.$new()来为我们的控制器创建一个新的作用域。
  • 调用了已注入的$controller函数,以参数的形式传入PhoneListCtrl控制器的名称和创建范围。

因为我们的代码现在使用$http服务以取回我们的控制器中的手机列表数据,在我们创建PhoneListCtrl子作用域之前,我们需要告诉测试套件等待一个后面的请求,来自控制器。我们可以这样做:

  • 请求把$httpBackend服务注入到我们的beforeEach函数中。这是一个在产品环境中的服务的模拟版本,可以响应各种XHR和JSONP请求。该服务的模拟版本允许你编写测试,不需要处理原生的API和与它相关的全局状态——本来这两者都会使测试变成一个噩梦。

  • 使用$httpBackend.expectGET方法规定$httpBackend服务等待之后的HTTP请求,并告诉它如何响应它。注意,直到我们调用$httpBackend.flush方法,才会返回响应。

现在我们作了断言以核实在响应到达之前,作用域上不存在手机模块:

    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服务返回的promise对象由规范的应答来处理。可以在模拟$httpBackend文档中了解为什么必须“清空HTTP请求”的完整解释。

  • 我们制作了断言,核实作用域上已经有手机模块了。

最后,我们核实已经正确设置了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响应。在$http回调中使用以下的代码:
$scope.phones = data.splice(0, 5);

总结

现在你已经知道了使用Angular服务是多么容易(幸亏Angular的依赖性注入),前往第六步 模板连接和图像,在那里你将添加一些手机的缩略图以及一些链接。