【一起学AngularJS】第七章、XHRs和依赖注入

之前几章中,我们使用的3个手机数据集都是硬编码的。下面让我们使用Angular自带的一个叫$httpservice来从远程服务器上获取一个较大的数据集。我们将使用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就无法正确加载对应的依赖了。
我们可以通过用依赖的名字字符串来注解函数(因为字符串是不会被压缩的),从而解决这个问题。我们有两种方法来实现注解注入:

  1. 在控制器定义函数上方创建一个$inject字符串数组属性,用来存放依赖服务的名字字符串。本例中代码如下:
function PhoneListCtrl($scope, $http) {...}
PhoneListCtrl.$inject = ['$scope', '$http'];
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
  1. 使用内联注解:为控制器构造函数传入一个数组,而不是一个简单的函数。这个数组包含了服务的名称以及放在最后的函数本身,如下:
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已经提供了一个$httpmock服务,我们可以直接在单元测试中使用。我们可以通过调用$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, 我们可以直接使用moduleinject两个帮助函数来访问和配置注入器。

我们在测试中是这样创建控制器的:

  • 我们使用inject这个帮助函数来注入$rootScopecontroller$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的依赖注入)。下一章我们将添加为手机添加一些缩略图以及链接。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容