【问题标题】:How to structure an Angular service so it can handle asynchronous calls?如何构建 Angular 服务以便它可以处理异步调用?
【发布时间】:2015-08-03 06:05:36
【问题描述】:

在我的 Angular 应用程序中,我有两个控制器都需要访问相同的数据。

为此,我创建了一个服务,负责保存和提供对该数据的访问:

angular.module("SomeModule").factory( "SomeService", function( $http ) {

    var svc = {};
    var data = {};

    // on initialization, load data from the server
    $http.get( "somefile.json" )
        .success( function( data ) {
            svc.data = data;
        } );

    svc.getItem = function( id ) {
        // find the specified item within svc.data, and return its value
    };

    return svc;

} );

...我已经将该服务注入到两个控制器中的每一个中:

angular.module("SomeModule").controller( "SomeController", function( $routeParams, SomeService ) {

    var ctrl = this;

    ctrl.item = null; // set an initial value

    // load the item that was requested in the URL
    ctrl.item = SomeService.getItem( $routeParams.id );

} );

几乎有效 - 但它有一个很大的缺陷。如果SomeControllerSomeService 完成加载somefile.json 之前调用SomeService.getItem(),那么SomeService 将没有任何数据要返回。

实际上,如果我多次加载应用程序,一些加载会起作用(ieSomeService首先完成加载somefile.json,然后控制器将根据需要显示数据),而其他加载则不会(ieSomeController 将在数据实际加载之前尝试从 SomeService 检索数据,一切都会崩溃并烧毁)。

显然,我需要找到一些方法来推迟getItem() 的执行,直到SomeService 实际上准备好处理这些调用。但我不确定最好的方法。

我能想到一些相当麻烦的解决方案,比如在SomeService 中建立我自己的呼叫队列,并连接一堆复杂的回调。但是必须有一个更优雅的解决方案。

怀疑Angular's $q service 在这里可能有用。但是,我对 Promise 很陌生,我不确定我应该如何在这里使用 $q(甚至我是否在正确地吠叫)。

你能把我推向正确的方向吗?我会非常感激的。

【问题讨论】:

  • 只需让getItem 返回一个承诺 - 并将数据的承诺存储在svc.data 中,而不是“准备就绪的数据”。
  • 正如@Bergi 所说,您必须返回一个承诺。你在我的回答中有一些例子,如果你想了解这方面的详细信息,请检查第二个重点
  • 正如@Bergi 建议的那样,我在我的答案中调整了这个例子以适应你的情况。

标签: angularjs asynchronous promise angularjs-service angular-promise


【解决方案1】:

你可以有 2 个不错的选择

  1. 使用回调

  2. 使用 $q 返回承诺

使用回调:

svc.getItem = function( id,callback ) {
      $http.get( "somefile.json" )
        .success( function( data ) {
            svc.data = data;
            callback(svc.data)
        } );
    };

在控制器中

SomeService.getItem( $routeParams.id,function(data){
 ctrl.item =  data
 } );

使用承诺:

  svc.getItem = function( id) {

   var deferred = $q.defer();

      $http.get( "somefile.json" )
        .success( function( data ) {
            svc.data = data;
            deferred.resolve(svc.data);
        } )
        .error(function (error) {
        deferred.reject(error);
         });

      return deferred.promise;
     ;
    };

在控制器中

SomeService.getItem( $routeParams.id).then(function (data) {
     ctrl.item =  data
},
function (error) {
    //do something with error
});

【讨论】:

  • $http 已经返回了一个承诺。通过使用 $q,您可以完成两次工作。
【解决方案2】:

我们是这样做的,我们使用 $q 来推迟异步调用将为您提供的任何服务,然后我将响应中的数据部分并解决它,它将所需的数据发送到控制器(没有状态,标头...)。

我在我的服务中使用了 try catch 语句,以使错误处理远离控制器。

angular.module("JobsService", [])
    .factory("JobsService", ['$q', '$http', '$log', function ($q, $http, $log) {

        var serviceName = "JobsService";
        var url = "http://localhost:8080/path/to/resource/";

        var service = {};

        service.getAll = function () {

            var deferred = $q.defer();

            try {
                $http.get(url + "/jobs")
                        .success(function (response, status) {
                            $log.debug("GET response in " + serviceName + " returned with status " + status);
                            deferred.resolve(response);
                        })
                        .error(function (error, status) {
                            deferred.reject(error + " : " + status);
                        });
            } catch (err) {
                $log.error(err);
                deferred.reject();
            }

            return deferred.promise;
        };

        return service;
    }]);

然后在控制器中

JobsService.getAll()
         .then(function (response) {

             $scope.jobs = response;
             // records are stored in $scope.jobs
         }, function (response) {
             $scope.jobs = undefined;
         })
             .finally(function () {
              // will always run
         });

【讨论】:

  • 不要使用deferred antipattern
  • 但所有酷孩子都会这样做。实际上你是对的,我这样做只是因为它有助于让我的控制器更整洁
  • 所有酷孩子都使用承诺。延迟的反模式是关于你工厂里的乱七八糟的,控制器应该和你写的一样整洁。
【解决方案3】:

这是我在自己的项目中的做法。

您的服务

angular.module("SomeModule").factory( "SomeService", function( $http ) {

    var svc = {};
    svc.data = {};

    // on initialization, load data from the server
    svc.getData = function(){
        return $http.get( "somefile.json" );
    };

    return svc;

} );

您的控制者

angular.module("SomeModule").controller( "SomeController", function( $routeParams, SomeService ) {

    ctrl.items = null; // set an initial value

    // load the item that was requested in the URL
    SomeService.getData().success(function(data){
        ctrl.items = data;
    }).error(function(response){
        console.err("damn");
    });

} );

重点:承诺

以我的拙见,处理异步调用的责任在于控制器。我总是尽可能返回一个 $http 承诺。

 svc.getData = function(){
    return $http.get( "somefile.json" );
 };

您可以在服务中添加一些逻辑,但您始终必须返回承诺。 (要知道:承诺上的 .success() 返回承诺)

控制器将有逻辑知道如何根据您的异步调用的响应来表现。他必须知道在成功和错误的情况下如何表现。

如果您有更多问题,请随时提问。希望对你有所帮助。

【讨论】:

  • 也许您可以展示 OP 问题的解决方案,而不是您如何解决自己的项目?
  • @Bergi 完全正确,我现在就这样做
  • 事实上.success() 不会返回新的而是原始的承诺。而且它根本不应该在这里使用。
  • @Bergi 哦?也许我当时正在做的一个错误。我觉得我可以将资源的“服务”处理包装在响应的 .success() 中,然后将其返回给控制器。然后控制器自己处理响应。我错了吗 ?如果是这样,我会编辑这个
  • 是的,将承诺返回给控制器是正确的。但是.success(function(data) { svc.data = data; }) 很没用
【解决方案4】:

我建议更好地利用 AngularJS 的路由功能,它允许您解决依赖关系以及 $http 服务缓存,并相应地构建您的应用程序。

因此,我认为您需要完全摆脱您的服务。

从下面的例子开始,taken straight from the Angular documentation:

phonecatApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html',
        controller: 'PhoneListCtrl'
      }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html',
        controller: 'PhoneDetailCtrl'
      }).
      otherwise({
        redirectTo: '/phones'
      });
  }]);

所以 PhoneListCtrl 和 PhoneDetailCtrl 都需要来自 somefile.json 的数据。我会像这样将这些数据注入每个控制器:

(function(){
        angular.module('phonecatApp').controller('PhoneListCtrl', ['somefileJsonData', function(somefileJsonData){
            this.someFileJsonData = someFileJsonData;
        }]);
})();

PhoneDetailCtrl 的想法相同。

然后像这样更新你的路由:

phonecatApp.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html',
        controller: 'PhoneListCtrl',
        resolve:{ somefileJsonData: ['$http',function($http){
            return $http.get("somefile.json", { cache: true });
        }] }
      }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html',
        controller: 'PhoneDetailCtrl',
        //same resolve
      }).
      otherwise({
        redirectTo: '/phones'
      });
  }]);

这样,您就可以让 Angular 在路由过程中解决这种依赖关系。

cache 设置为 true 也会缓存它,因此您不会两次执行相同的 Get 请求,并且 Angular 只会在解决依赖关系时显示您的视图。

因此,在您的应用中,SomeController 与作为路由过程的一部分的视图配对,使用 resolve 来解析 item,并将其注入控制器。

【讨论】:

  • 在我看来你说的是真的,但服务也旨在管理你的模块中的对象的共享集合。
  • 嗯。我喜欢这种精神,但实际上注入到我的控制器中的是一个复杂对象,其属性名为data(它包含加载的 JSON)、statusconfigstatusText。显然我可以将控制器调整为 this.someFileJsonData = someFileJsonData.data 而不是 this.someFileJsonData = someFileJsonData,但这感觉很混乱 - 它将控制器与 someFileJsonData 的实现耦合在一起。有没有办法清理路由定义中的resolve,以便它只注入加载的 JSON 数据?
  • 另外,您链接到的 Angular 文档页面在任何地方都没有提到 resolve
  • @greenie2600 如果需要,您也可以在解析中使用您的服务而不是 $http (但它必须返回一个承诺!)。根据您的用例,JMK 解决方案是最好的选择。
  • 更多关于 resolve here 的信息,是的,你可能需要访问正在传入的对象的属性,但我发现为更整洁的代码付出的代价很小
【解决方案5】:

试试这个代码

angular.module("SomeModule").factory("SomeService", function ($http) {
    var svc = {};

    svc.getList = function () {
       return $http.get("somefile.json");
    };

    svc.getItem = function (id) {
        svc.getList().then(function (response) {
            // find the specified item within response, and return its value
        });
    };
    return svc;
});

【讨论】:

  • 您的 deferred 初始化在错误的范围内(当然还有 you shouldn't use deferreds
  • @Bergi,我去掉了$q的用法,但是我认为不需要将promise返回给控制器,因为控制器中item的值会自动更新(2种数据绑定方式)
  • 但是任何地方都没有建立数据绑定?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-29
  • 1970-01-01
  • 2018-12-16
  • 1970-01-01
  • 1970-01-01
  • 2011-08-18
相关资源
最近更新 更多