Processando a resposta $ http em serviço

233

Recentemente, publiquei uma descrição detalhada do problema que estou enfrentando aqui no SO. Como não pude enviar uma $httpsolicitação real , usei o tempo limite para simular o comportamento assíncrono. A ligação de dados do meu modelo para exibição está funcionando corretamente, com a ajuda de @Gloopy

Agora, quando eu uso em $httpvez de $timeout(testado localmente), pude ver que a solicitação assíncrona foi bem-sucedida e dataé preenchida com a resposta json no meu serviço. Mas, minha opinião não está atualizando.

atualizou o Plunkr aqui

bsr
fonte

Respostas:

419

Aqui está um Plunk que faz o que você deseja: http://plnkr.co/edit/TTlbSv?p=preview

A idéia é que você trabalhe diretamente com as promessas e suas funções "then" para manipular e acessar as respostas retornadas de forma assíncrona.

app.factory('myService', function($http) {
  var myService = {
    async: function() {
      // $http returns a promise, which has a then function, which also returns a promise
      var promise = $http.get('test.json').then(function (response) {
        // The then function here is an opportunity to modify the response
        console.log(response);
        // The return value gets picked up by the then in the controller.
        return response.data;
      });
      // Return the promise to the controller
      return promise;
    }
  };
  return myService;
});

app.controller('MainCtrl', function( myService,$scope) {
  // Call the async method and then do stuff with what is returned inside our own then function
  myService.async().then(function(d) {
    $scope.data = d;
  });
});

Aqui está uma versão um pouco mais complicada que armazena em cache a solicitação, para que você a faça apenas pela primeira vez ( http://plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview ):

app.factory('myService', function($http) {
  var promise;
  var myService = {
    async: function() {
      if ( !promise ) {
        // $http returns a promise, which has a then function, which also returns a promise
        promise = $http.get('test.json').then(function (response) {
          // The then function here is an opportunity to modify the response
          console.log(response);
          // The return value gets picked up by the then in the controller.
          return response.data;
        });
      }
      // Return the promise to the controller
      return promise;
    }
  };
  return myService;
});

app.controller('MainCtrl', function( myService,$scope) {
  $scope.clearData = function() {
    $scope.data = {};
  };
  $scope.getData = function() {
    // Call the async method and then do stuff with what is returned inside our own then function
    myService.async().then(function(d) {
      $scope.data = d;
    });
  };
});
Pete BD
fonte
13
Ainda existe alguma maneira de chamar os métodos de sucesso e erro no controlador após a interceptação do serviço then?
precisa saber é o seguinte
2
@PeteBD Se eu quiser ligar myService.async()várias vezes a partir de vários controladores, como você organizaria o serviço para que seja o único $http.get()requisito para a primeira solicitação e todas as solicitações subsequentes retornem apenas uma matriz de objetos local definida na primeira chamada para myService.async(). Em outras palavras, desejo evitar várias solicitações desnecessárias ao serviço JSON, quando na verdade só preciso fazer uma.
GFoley83
5
@ GFoley83 - aqui está você: plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview . Se você olhar para o console, verá que a solicitação é feita apenas uma vez.
Pete BD
3
@PeteBD Acho que você também pode usar $scope.data = myService.async()diretamente no controlador.
Julian
2
@ Blowsie- Eu atualizei o Plunks. Aqui está o original (atualizado para 1.2RC3): plnkr.co/edit/3Nwxxk?p=preview Aqui está um serviço em uso: plnkr.co/edit/a993Mn?p=preview
Pete BD
82

Que seja simples. É tão simples quanto

  1. Retorno promiseem seu serviço (não é necessário usá-lo then)
  2. Use thenno seu controlador

Demo. http://plnkr.co/edit/cbdG5p?p=preview

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

app.factory('myService', function($http) {
  return {
    async: function() {
      return $http.get('test.json');  //1. this returns promise
    }
  };
});

app.controller('MainCtrl', function( myService,$scope) {
  myService.async().then(function(d) { //2. so you can use .then()
    $scope.data = d;
  });
});
allenhwkim
fonte
No seu link, é app.factory, e no seu código, é app.service. É suposto app.factoryneste caso.
Re de Captcha
1
app.service também funciona. Além disso - isso para mim parece a solução mais elegante. Estou esquecendo de algo?
usar o seguinte comando
1
Parece que toda vez que tenho um problema angular, o @allenhwkim tem a resposta! (3rd time this week- great ng-map component btw)
Yarin 28/04
Eu só quero saber como colocar o sucesso e erro aqui com status_code
Anuj
58

Por ser assíncrono, $scopeestá obtendo os dados antes da conclusão da chamada ajax.

Você pode usar $qem seu serviço para criar promisee devolvê-lo ao controlador, e o controlador obtém o resultado dentro de uma then()chamada promise.

Em seu serviço,

app.factory('myService', function($http, $q) {
  var deffered = $q.defer();
  var data = [];  
  var myService = {};

  myService.async = function() {
    $http.get('test.json')
    .success(function (d) {
      data = d;
      console.log(d);
      deffered.resolve();
    });
    return deffered.promise;
  };
  myService.data = function() { return data; };

  return myService;
});

Então, no seu controlador:

app.controller('MainCtrl', function( myService,$scope) {
  myService.async().then(function() {
    $scope.data = myService.data();
  });
});
Tosh
fonte
2
+1 Eu gosto deste melhor porque é mais OO que os outros. No entanto, existe algum motivo para você não fazer isso this.async = function() {e this.getData = function() {return data}? Eu espero que você começa o que eu quero dizer
bicicleta
@bicycle Eu queria da mesma maneira, mas não vai funcionar porque a promessa precisa ser resolvida até o fim. Se não o fizer, e tentar acessá-lo como faria normalmente, você receberá um erro de referência ao acessar os dados internos. Espero que faça sentido?
user6123723
Se eu entendi corretamente, é necessário adicionar deffered = $q.defer()dentro do myService.async se eu quiser chamar myService.async () duas ou mais vezes
demas
1
Este exemplo é um antipadrão diferido clássico . Não há necessidade de fabricar uma promessa, $q.deferpois o $httpserviço já retorna uma promessa. A promessa retornada será interrompida se $httpretornar um erro. Além disso, os métodos .successe .errorestão obsoletos e foram removidos do AngularJS 1.6 .
Georgeawg 02/11/19
23

shoshyama tem uma solução, mas você pode simplificar muito se usar o fato de que $ http retorna promessas e essas promessas podem retornar um valor:

app.factory('myService', function($http, $q) {
  myService.async = function() {
    return $http.get('test.json')
    .then(function (response) {
      var data = reponse.data;
      console.log(data);
      return data;
    });
  };

  return myService;
});

app.controller('MainCtrl', function( myService,$scope) {
  $scope.asyncData = myService.async();
  $scope.$watch('asyncData', function(asyncData) {
    if(angular.isDefined(asyncData)) {
      // Do something with the returned data, angular handle promises fine, you don't have to reassign the value to the scope if you just want to use it with angular directives
    }
  });

});

Uma pequena demonstração no coffeescript: http://plunker.no.de/edit/ksnErx?live=preview

Seu desentupidor atualizado com o meu método: http://plnkr.co/edit/mwSZGK?p=preview

Guillaume86
fonte
Vou tentar mais adiante sua abordagem. Mas gosto de capturar o resultado em serviço em vez de retornar. Veja a pergunta relacionada a isso aqui stackoverflow.com/questions/12504747/… . Eu gosto de processar os dados retornados por $ http de maneiras diferentes no controlador. Obrigado novamente por sua ajuda.
bsr
você pode usar promessas em serviços, se você não gostar de $ watch, você pode fazer o compromisso.then (function (data) {service.data = data;}, onErrorCallback); `
Guillaume86
Eu adicionei uma Plunker bifurcada de seu
Guillaume86
1
Alternativamente, você pode usar $ âmbito $ emitem a partir do serviço e US $ âmbito $ ON no ctrl para dizer-lhe controlador que os dados retornou, mas eu realmente não vejo um benefício..
Guillaume86
7

Uma maneira muito melhor eu acho que seria algo como isto:

Serviço:

app.service('FruitsManager',function($q){

    function getAllFruits(){
        var deferred = $q.defer();

        ...

        // somewhere here use: deferred.resolve(awesomeFruits);

        ...

        return deferred.promise;
    }

    return{
        getAllFruits:getAllFruits
    }

});

E no controlador você pode simplesmente usar:

$scope.fruits = FruitsManager.getAllFruits();

Angular colocará automaticamente o resolvido awesomeFruitsno $scope.fruits.

HasanAboShally
fonte
4
deferred.resolve ()? Seja mais preciso, por favor, e onde está a chamada de $ http? Além disso, por que você retorna um objeto em um serviço.
6

Eu tinha o mesmo problema, mas quando estava navegando na internet, entendi que $ http retornava por padrão uma promessa, então eu poderia usá-lo com "then" depois de retornar os "dados". veja o código:

 app.service('myService', function($http) {
       this.getData = function(){
         var myResponseData = $http.get('test.json').then(function (response) {
            console.log(response);.
            return response.data;
          });
         return myResponseData;

       }
});    
 app.controller('MainCtrl', function( myService, $scope) {
      // Call the getData and set the response "data" in your scope.  
      myService.getData.then(function(myReponseData) {
        $scope.data = myReponseData;
      });
 });
JhonQO
fonte
4

Ao vincular a interface do usuário à sua matriz, você deve atualizar a mesma matriz diretamente, definindo o comprimento como 0 e inserindo os dados na matriz.

Em vez disso (que define uma referência de matriz diferente para a dataqual sua interface do usuário não saberá):

 myService.async = function() {
    $http.get('test.json')
    .success(function (d) {
      data = d;
    });
  };

tente isto:

 myService.async = function() {
    $http.get('test.json')
    .success(function (d) {
      data.length = 0;
      for(var i = 0; i < d.length; i++){
        data.push(d[i]);
      }
    });
  };

Aqui está um violino que mostra a diferença entre definir uma nova matriz versus esvaziar e adicionar a uma existente. Não consegui que seu plnkr funcionasse, mas espero que funcione para você!

Gloopy
fonte
isso não funcionou. no log do console, pude ver que d é atualizado corretamente no retorno de chamada bem-sucedido, mas não nos dados. Pode ser que a função já esteja executada.
bsr
Esse método definitivamente deve funcionar, talvez tenha algo a ver com o tipo de dado d que não é um array (no asp.net, você precisaria acessar o dd do array, por exemplo). Veja este plnkr para um exemplo empurrando uma seqüência para a matriz em caso de erro: plnkr.co/edit/7FuwlN?p=preview
Gloopy
1
angular.copy(d, data)também irá funcionar. Quando um destino é fornecido ao método copy (), ele primeiro exclui os elementos do destino e depois copia os novos da fonte.
Mark Rajcok
4

Relacionado a isso, passei por um problema semelhante, mas não com o get ou post feito pelo Angular, mas com uma extensão feita por terceiros (no meu caso, a extensão do Chrome).
O problema que eu enfrentei é que a extensão do Chrome não retornará, then()então não consegui fazê-lo da maneira acima na solução acima, mas o resultado ainda é assíncrono.
Portanto, minha solução é criar um serviço e prosseguir para um retorno de chamada

app.service('cookieInfoService', function() {
    this.getInfo = function(callback) {
        var model = {};
        chrome.cookies.get({url:serverUrl, name:'userId'}, function (response) {
            model.response= response;
            callback(model);
        });
    };
});

Então no meu controlador

app.controller("MyCtrl", function ($scope, cookieInfoService) {
    cookieInfoService.getInfo(function (info) {
        console.log(info);
    });
});

Espero que isso ajude outras pessoas a obter o mesmo problema.

Shadoweb
fonte
4

Eu li http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/ [AngularJS nos permite otimizar nossa lógica de controlador colocando uma promessa diretamente no escopo, em vez de entregar manualmente a solução resolvida valor em um retorno de chamada bem-sucedido.]

tão simples e prático :)

var app = angular.module('myApp', []);
            app.factory('Data', function($http,$q) {
                return {
                    getData : function(){
                        var deferred = $q.defer();
                        var promise = $http.get('./largeLoad').success(function (response) {
                            deferred.resolve(response);
                        });
                        // Return the promise to the controller
                        return deferred.promise; 
                    }
                }
            });
            app.controller('FetchCtrl',function($scope,Data){
                $scope.items = Data.getData();
            });

Espero que esta ajuda

Whisher
fonte
não funciona o valor de retorno de defrred.promisenão é uma função.
Jürgen Paul
@PineappleUndertheSea Por que ele precisa ser uma função? É um objeto de promessa.
Chev
@PineappleUndertheSea você quis usar adiado e não defrred?
Derrick
2
Como PeteBD salientou, esta forma $scope.items = Data.getData(); é obsoleto em Anglular
poshest
2

Eu realmente não gosto do fato de que, devido à maneira "promissora" de fazer as coisas, o consumidor do serviço que usa $ http precisa "saber" sobre como descompactar a resposta.

Eu só quero chamar alguma coisa e divulgar os dados, de maneira semelhante à antiga $scope.items = Data.getData();, que agora está obsoleta .

Tentei por um tempo e não achei uma solução perfeita, mas aqui está minha melhor chance ( Plunker ). Pode ser útil para alguém.

app.factory('myService', function($http) {
  var _data;  // cache data rather than promise
  var myService = {};

  myService.getData = function(obj) { 
    if(!_data) {
      $http.get('test.json').then(function(result){
        _data = result.data;
        console.log(_data);  // prove that it executes once
        angular.extend(obj, _data);
      }); 
    } else {  
      angular.extend(obj, _data);
    }
  };

  return myService;
}); 

Então controlador:

app.controller('MainCtrl', function( myService,$scope) {
  $scope.clearData = function() {
    $scope.data = Object.create(null);
  };
  $scope.getData = function() {
    $scope.clearData();  // also important: need to prepare input to getData as an object
    myService.getData($scope.data); // **important bit** pass in object you want to augment
  };
});

As falhas que eu já consigo identificar são

  • Você precisa passar o objeto ao qual deseja adicionar os dados , o que não é um padrão intuitivo ou comum no Angular.
  • getDatasó pode aceitar o objparâmetro na forma de um objeto (embora também possa aceitar uma matriz), o que não será um problema para muitos aplicativos, mas é uma limitação dolorida
  • Você tem que preparar o objeto de entrada $scope.datacom = {}a torná-lo um objeto (essencialmente o que $scope.clearData()faz acima), ou = []para um array, ou não vai funcionar (já estamos tendo que assumir algo sobre os dados que entram). Eu tentei fazer este passo de preparação getData, mas sem sorte.

No entanto, ele fornece um padrão que remove o clichê do controlador "promete desembrulhar" e pode ser útil nos casos em que você deseja usar determinados dados obtidos a partir de $ http em mais de um local, mantendo-o SECO.

chique
fonte
1

No que diz respeito ao cache da resposta em serviço, aqui está outra versão que parece mais direta do que o que vi até agora:

App.factory('dataStorage', function($http) {
     var dataStorage;//storage for cache

     return (function() {
         // if dataStorage exists returned cached version
        return dataStorage = dataStorage || $http({
      url: 'your.json',
      method: 'GET',
      cache: true
    }).then(function (response) {

              console.log('if storage don\'t exist : ' + response);

              return response;
            });

    })();

});

esse serviço retornará os dados em cache ou $http.get;

 dataStorage.then(function(data) {
     $scope.data = data;
 },function(e){
    console.log('err: ' + e);
 });
maioman
fonte
0

Por favor, tente o código abaixo

Você pode dividir o controlador (PageCtrl) e o serviço (dataService)

'use strict';
(function () {
    angular.module('myApp')
        .controller('pageContl', ['$scope', 'dataService', PageContl])
        .service('dataService', ['$q', '$http', DataService]);
    function DataService($q, $http){
        this.$q = $q;
        this.$http = $http;
        //... blob blob 
    }
    DataService.prototype = {
        getSearchData: function () {
            var deferred = this.$q.defer(); //initiating promise
            this.$http({
                method: 'POST',//GET
                url: 'test.json',
                headers: { 'Content-Type': 'application/json' }
            }).then(function(result) {
                deferred.resolve(result.data);
            },function (error) {
                deferred.reject(error);
            });
            return deferred.promise;
        },
        getABCDATA: function () {

        }
    };
    function PageContl($scope, dataService) {
        this.$scope = $scope;
        this.dataService = dataService; //injecting service Dependency in ctrl
        this.pageData = {}; //or [];
    }
    PageContl.prototype = {
         searchData: function () {
             var self = this; //we can't access 'this' of parent fn from callback or inner function, that's why assigning in temp variable
             this.dataService.getSearchData().then(function (data) {
                 self.searchData = data;
             });
         }
    }
}());

Ratheesh
fonte