Skip to content

Files

Latest commit

1def2f3 · Jan 8, 2022

History

History
405 lines (274 loc) · 13.9 KB

05.md

File metadata and controls

405 lines (274 loc) · 13.9 KB

五、消费外部服务

Angular 内置了与远程 HTTP 服务器通信的支持。 $http 服务通过浏览器的 XMLHttpRequest 对象或通过带有填充符的JSON(JSONP)处理低级 AJAX 请求。 $resource 服务允许您与 RESTful 数据源交互,并提供高级行为,这些行为自然映射到 RESTful 资源。

用 AJAX 请求 JSON 数据

问题

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

溶液

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

    <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 上使用角度种子项目找到完整的示例。

讨论

控制器定义了对$scope$http模块的依赖。使用get方法执行对data/posts.json端点的 HTTP GET 请求。它用successerror方法返回一个 $promise 对象。一旦成功,JSON 数据被分配到$scope.posts以使其在模板中可用。

$http服务支持 HTTP 动词getheadpostputdelete,jsonp。我们将在接下来的章节中研究更多的例子。

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

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

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

消费高质量原料药

问题

您希望使用 RESTful 数据源。

溶液

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

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

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

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

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

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

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

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

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

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

    Post.save(data);

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

    Post.delete({ id: id });

完整的示例代码基于布莱恩·福特的角度表达种子并使用表达框架。

你可以在 GitHub 上找到完整的例子。

讨论

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

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

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

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

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

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

配置

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

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

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

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

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

    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 模板允许您在输入字段中输入搜索词;它将在列表中呈现搜索结果:

    <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配置可以在请求数据的控制器中完成:

    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 上找到完整的例子。

讨论

推特搜索应用编程接口支持 JSON 格式的callback属性,如他们的文档中所述。$resource定义将callback属性设置为JSON_CALLBACK,这是使用 JSONP 时来自 Angular 的约定。它是一个占位符,由 Angular 生成的真实回调函数替换。另外,我们将get方法配置为使用JSONP。现在,在调用 API 时,我们使用q URL 参数来传递输入的searchTerm

延期和承诺

问题

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

溶液

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

    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服务进行简化:

    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 上找到完整的例子。

讨论

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

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

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

    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 模板中通过将其包装到范围上定义的函数中来触发:

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

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

这只是承诺如何简化代码的众多例子之一,但是你可以通过查看 Kris Kowal 的 Q 实现找到更多的例子。

测试服务

问题

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

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

    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 _ 后端嘲讽服务。

    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 上找到完整的例子。

讨论

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

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

看一看 ngMock。$ httpbacknd模块了解更多详情。