# 五、消费外部服务

Angular 内置了与远程 HTTP 服务器通信的支持。 [$http](http://docs.angularjs.org/api/ng.$http) 服务通过浏览器的 [XMLHttpRequest](http://en.wikipedia.org/wiki/XMLHttpRequest) 对象或通过带有填充符的[JSON](http://en.wikipedia.org/wiki/JSON "JSON")([JSONP](http://en.wikipedia.org/wiki/JSONP))处理低级 AJAX 请求。 [$resource](http://docs.angularjs.org/api/ngResource.$resource) 服务允许您与 RESTful 数据源交互，并提供高级行为，这些行为自然映射到 RESTful 资源。

## 用 AJAX 请求 JSON 数据

### 问题

您希望通过 AJAX 请求获取 JSON 数据并呈现它。

### 溶液

使用`$http`服务实现一个控制器，以获取数据并将其存储在范围内:

```js
    <body ng-app="MyApp">
    <div ng-controller="PostsCtrl">
    <ul ng-repeat="post in posts">
    <li>{{post.title}}</li>
    </ul>
    </div>
    </body>

    var app = angular.module("MyApp", []);

    app.controller("PostsCtrl", function($scope, $http) {
    $http.get('data/posts.json').
    success(function(data, status, headers, config) {
    $scope.posts = data;
    }).
    error(function(data, status, headers, config) {
    // log error
    });
    });

```

您可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe1) 上使用角度种子项目找到完整的示例。

### 讨论

控制器定义了对`$scope`和`$http`模块的依赖。使用`get`方法执行对`data/posts.json`端点的 HTTP GET 请求。它用`success`和`error`方法返回一个 [$promise](http://docs.angularjs.org/api/ng.$q) 对象。一旦成功，JSON 数据被分配到`$scope.posts`以使其在模板中可用。

`$http`服务支持 HTTP 动词`get`、`head`、`post`、`put`、`delete,`和`jsonp`。我们将在接下来的章节中研究更多的例子。

`$http`服务自动添加某些 HTTP 头，例如`X-Requested-With: XMLHttpRequest`。但是您也可以使用`$http.defaults`功能自行设置自定义的 HTTP 头:

```js
    $http.defaults.headers.common["X-Custom-Header"] = "Angular.js"

```

直到现在，`$http`服务看起来并不是特别特别。但是如果你查看[文档](http://docs.angularjs.org/api/ng.$http)，你会发现很多不错的特性，包括，例如，请求/响应转换，自动为你反序列化 JSON，响应缓存，处理全局错误处理的响应拦截器，身份验证或其他预处理任务，当然还有承诺支持。我们将在后面的章节中研究`$q`服务，Angular 的承诺/延期服务。

## 消费高质量原料药

### 问题

您希望使用 RESTful 数据源。

### 溶液

使用 Angular 的高级`$resource`服务。请注意，Angular `ngResource`模块需要单独加载，因为它不包含在基本 angular.js 文件中:

```js
    <script src="angular-resource.js">

```

现在让我们从将应用程序模块和我们的`Post`模型定义为 Angular 服务开始:

```js
    var app = angular.module('myApp', ['ngResource']);

    app.factory("Post", function($resource) {
    return $resource("/api/posts/:id");
    });

```

现在，我们可以使用我们的服务来检索控制器内部的帖子列表(例如，HTTP GET/API/post):

```js
    app.controller("PostIndexCtrl", function($scope, Post) {
    Post.query(function(data) {
    $scope.posts = data;
    });
    });

```

或由`id`指定的具体帖子(如 HTTP GET/API/post/1):

```js
    app.controller("PostShowCtrl", function($scope, Post) {
    Post.get({ id: 1 }, function(data) {
    $scope.post = data;
    });
    });

```

我们可以使用 save 创建一个新帖子(例如，HTTP POST/API/POST):

```js
    Post.save(data);

```

并且我们可以通过`id`删除特定的帖子(例如，DELETE/API/post/1):

```js
    Post.delete({ id: id });

```

完整的示例代码基于布莱恩·福特的[角度表达种子](https://github.com/btford/angular-express-seed)并使用[表达](http://expressjs.com/)框架。

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe2) 上找到完整的例子。

### 讨论

遵循一些约定会大大简化我们的代码。我们只通过传递 URL 模式来定义`$resource`。这给了我们一些很好的方法，包括`query`、`get`、`save`、`remove,`和`delete`来使用我们的资源。在上面的例子中，我们实现了几个控制器来覆盖典型的用例。`get`和`query`方法需要三个参数，请求参数、成功回调和错误回调。`save`方法需要四个参数，请求参数、开机自检数据、成功回调和错误回调。

`$resource`服务目前不支持承诺，因此与`$http`服务有明显不同的接口。但是我们不用等太久，因为 1.1 开发分公司已经开始引入`$resource`服务承诺支持！

`$resource`查询或获取函数返回的对象是提供`$save`、`$remove,`和`$delete`方法的`$resource`实例。这允许您轻松获取资源并更新它，如下例所示:

```js
    var post = Post.get({ id: 1 }, function() {
    post.title = "My new title";
    post.$save();
    });

```

需要注意的是`get`调用会立即返回一个空引用——在我们的例子中是`post`变量。一旦从服务器返回数据，就填充现有的引用；然后，我们可以方便地更改我们的文章标题和使用`$save`方法。

请注意，有一个空引用意味着我们的帖子不会在模板中呈现。但是，一旦数据被返回，视图会自动重新呈现，显示新数据。

#### 配置

如果你对帖子的回应不是一个数组，而是一个更复杂的 JSON 呢？这通常会导致以下错误:

```js
    TypeError: Object #<Resource> has no method 'push'

```

Angular 似乎希望您的服务返回一个 JSON 数组。看看下面的 JSON 例子，它在 JSON 对象中包装了一个`posts`数组:

```js
    {
    "posts": [
    {
    "id" : 1,
    "title" : "title 1"
    },
    {
    "id": 2,
    "title" : "title 2"
    }
    ]
    }

```

在这种情况下，您必须相应地更改`$resource`的定义:

```js
    app.factory("Post", function($resource) {
    return $resource("/api/posts/:id", {}, {
    query: { method: "GET", isArray: false }
    });
    });

    app.controller("PostIndexCtrl", function($scope, Post) {
    Post.query(function(data) {
    $scope.posts = data.posts;
    });
    });

```

我们仅通过将`isArray`属性设置为`false`来将`query`动作的配置更改为不期待数组。然后，在我们的控制器中，我们可以直接访问`data.posts`。

将您的模型和`$resource`用法封装在 Angular 服务模块中，并将其注入到您的控制器中，这通常是一个很好的做法。这样，您可以轻松地在不同的控制器中重用同一个模型，并更轻松地测试它。

## 使用 JSONP 应用程序接口

### 问题

您希望调用一个 JSONP 应用编程接口。

### 溶液

使用`$resource`服务，并将其配置为使用 JSONP。作为一个例子，我们将在这里使用推特搜索应用编程接口。

HTML 模板允许您在输入字段中输入搜索词；它将在列表中呈现搜索结果:

```js
    <body ng-app="MyApp">
    <div ng-controller="MyCtrl">
    <input type="text" ng-model="searchTerm" placeholder="Search term">
    <button ng-click="search()">Search</button>
    <ul ng-repeat="tweet in searchResult.results">
    <li>{{tweet.text}}</li>
    </ul>
    </div>
    </body>

```

`$resource`配置可以在请求数据的控制器中完成:

```js
    var app = angular.module("MyApp", ["ngResource"]);

    function MyCtrl($scope, $resource) {
    var TwitterAPI = $resource("http://search.twitter.com/search.json",
    { callback: "JSON_CALLBACK" },
    { get: { method: "JSONP" }});

    $scope.search = function() {
    $scope.searchResult = TwitterAPI.get({ q: $scope.searchTerm });
    };
    }

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe3) 上找到完整的例子。

### 讨论

推特搜索应用编程接口支持 JSON 格式的`callback`属性，如他们的[文档](https://dev.twitter.com/docs/api/1/get/search)中所述。`$resource`定义将`callback`属性设置为`JSON_CALLBACK`，这是使用 [JSONP](http://en.wikipedia.org/wiki/JSONP) 时来自 Angular 的约定。它是一个占位符，由 Angular 生成的真实回调函数替换。另外，我们将`get`方法配置为使用`JSONP`。现在，在调用 API 时，我们使用`q` URL 参数来传递输入的`searchTerm`。

## 延期和承诺

### 问题

您希望同步多个异步函数，避免 JavaScript 回调地狱。

### 溶液

举个例子，我们想按顺序调用三个服务，并组合它们的结果。让我们从嵌套方法开始:

```js
    tmp = [];

    $http.get("/app/data/first.json").success(function(data) {
    tmp.push(data);
    $http.get("/app/data/second.json").success(function(data) {
    tmp.push(data);
    $http.get("/app/data/third.json").success(function(data) {
    tmp.push(data);
    $scope.combinedNestedResult = tmp.join(", ");
    });
    });
    });

```

我们调用`get`函数三次来检索三个 JSON 文档，每个文档都有一个字符串数组。我们甚至还没有开始添加错误处理，但是已经通过使用嵌套回调，代码变得混乱，并且可以使用`$q`服务进行简化:

```js
    var first = $http.get("/app/data/first.json"),
    second = $http.get("/app/data/second.json"),
    third = $http.get("/app/data/third.json");

    $q.all([first, second, third]).then(function(result) {
    var tmp = [];
    angular.forEach(result, function(response) {
    tmp.push(response.data);
    });
    return tmp;
    }).then(function(tmpResult) {
    $scope.combinedResult = tmpResult.join(", ");
    });

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe4) 上找到完整的例子。

### 讨论

`all`函数将多个承诺组合成一个承诺，非常优雅地解决了我们的问题。

让我们仔细看看`then`法。这是相当做作的，但应该会让你知道如何使用`then`来顺序调用函数和传递数据。既然`all`函数返回了一个承诺，我们可以在上面再次调用`then`。通过返回`tmp`变量，它将作为一个`tmpResult`参数传递给当时的函数。

在完成这个配方之前，让我们快速讨论一个例子，在这个例子中，我们必须创建自己的延迟对象:

```js
    function deferredTimer(success) {
    var deferred = $q.defer();

    $timeout(function() {
    if (success) {
    deferred.resolve({ message: "This is great!" });
    } else {
    deferred.reject({ message: "Really bad" });
    }
    }, 1000);

    return deferred.promise;
    }

```

使用`defer`方法，我们创建一个延迟实例。作为异步操作的一个例子，我们将使用`$timeout`服务，该服务将根据布尔成功参数来解决或拒绝我们的操作。该函数将立即返回`promise`，因此不会在我们的 HTML 模板中呈现任何结果。一秒钟后，计时器将执行并返回我们的成功或失败响应。

这个`deferredTimer`可以在我们的 HTML 模板中通过将其包装到范围上定义的函数中来触发:

```js
    $scope.startDeferredTimer = function(success) {
    deferredTimer(success).then(
    function(data) {
    $scope.deferredTimerResult = "Successfully finished: " +
    data.message;
    },
    function(data) {
    $scope.deferredTimerResult = "Failed: " + data.message;
    }
    );
    };

```

我们的`startDeferredTimer`函数将获得一个`success`参数，并将其传递给`deferredTimer`。`then`函数期望一个成功和一个错误回调作为参数，我们用它来设置一个范围变量`deferredTimerResult`来显示我们的结果。

这只是承诺如何简化代码的众多例子之一，但是你可以通过查看 [Kris Kowal 的 Q 实现](https://github.com/kriskowal/q)找到更多的例子。

## 测试服务

### 问题

您希望使用 JSONP 应用编程接口对您的控制器和服务进行单元测试。

让我们再次看看我们希望测试的示例:

```js
    var app = angular.module("MyApp", ["ngResource"]);

    app.factory("TwitterAPI", function($resource) {
    return $resource("http://search.twitter.com/search.json",
    { callback: "JSON_CALLBACK" },
    { get: { method: "JSONP" }});
    });

    app.controller("MyCtrl", function($scope, TwitterAPI) {
    $scope.search = function() {
    $scope.searchResult = TwitterAPI.get({ q: $scope.searchTerm });
    };
    });

```

请注意，它与之前的配方略有不同，因为`TwitterAPI`被拉出控制器，现在驻留在自己的服务中。

### 溶液

使用 angular-seed 项目和$ http _ 后端嘲讽服务。

```js
    describe('MyCtrl', function(){
    var scope, ctrl, httpBackend;

    beforeEach(module("MyApp"));

    beforeEach(
    inject(
    function($controller, $rootScope, TwitterAPI, $httpBackend) {
    httpBackend = $httpBackend;
    scope = $rootScope.$new();
    ctrl = $controller("MyCtrl", {
    $scope: scope, TwitterAPI: TwitterAPI });

    var mockData = { key: "test" };
    var url = "http://search.twitter.com/search.json?" +
    "callback=JSON_CALLBACK&q=angularjs";
    httpBackend.whenJSONP(url).respond(mockData);
    }
    )
    );

    it('should set searchResult on successful search', function() {
    scope.searchTerm = "angularjs";
    scope.search();
    httpBackend.flush();

    expect(scope.searchResult.key).toBe("test");
    });

    });

```

你可以在 [GitHub](https://github.com/fdietz/recipes-with-angular-js-examples/tree/master/chapter5/recipe5) 上找到完整的例子。

### 讨论

由于现在服务和控制器之间有了明确的分离，我们可以简单地将`Twitter API`注入到我们的`beforeEach`功能中。

用`$httpBackend`嘲讽是`beforeEach`的最后一步。当 JSONP 请求发生时，我们用`mockData`来响应。`search()`触发后，我们`flush()`返回`httpBackend`以便返回我们的`mockData`。

看一看 [ngMock。$ httpbacknd](http://docs.angularjs.org/api/ngMock.$httpBackend)模块了解更多详情。