Przetwarzanie odpowiedzi $ http w usłudze

233

Niedawno opublikowałem szczegółowy opis problemu, przed którym stoję tutaj, w SO. Ponieważ nie mogłem wysłać faktycznego $httpżądania, wykorzystałem limit czasu do symulacji zachowania asynchronicznego. Powiązanie danych z mojego modelu do wyświetlenia działa poprawnie, przy pomocy @Gloopy

Teraz, gdy używam $httpzamiast $timeout(testowane lokalnie), widziałem, że żądanie asynchroniczne powiodło się i datajest wypełnione odpowiedzią json w mojej usłudze. Ale mój widok się nie aktualizuje.

zaktualizowano Plunkr tutaj

bsr
źródło

Odpowiedzi:

419

Oto Plunk, który robi, co chcesz: http://plnkr.co/edit/TTlbSv?p=preview

Chodzi o to, że pracujesz bezpośrednio z obietnicami i ich funkcjami „wtedy”, aby manipulować i uzyskiwać dostęp do asynchronicznie zwróconych odpowiedzi.

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;
  });
});

Oto nieco bardziej skomplikowana wersja, która buforuje żądanie, więc możesz je wykonać tylko za pierwszym razem ( 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
źródło
13
Czy jest jakiś sposób na wywołanie metod powodzenia i błędów w kontrolerze po przechwyceniu usługi then?
andyczerwonka
2
@PeteBD Jeśli chcę dzwonić myService.async()wielokrotnie z różnych kontrolerów, w jaki sposób zorganizujesz usługę tak, aby działała tylko $http.get()dla pierwszego żądania, a wszystkie kolejne żądania tylko zwracają lokalną tablicę obiektów, która jest ustawiana przy pierwszym wywołaniu do myService.async(). Innymi słowy, chcę uniknąć wielu niepotrzebnych żądań do usługi JSON, kiedy tak naprawdę muszę tylko jedno.
GFoley83
5
@ GFoley83 - proszę bardzo: plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview . Jeśli spojrzysz na konsolę, zobaczysz, że żądanie zostało wykonane tylko raz.
Pete BD
3
@PeteBD Myślę, że możesz także używać $scope.data = myService.async()bezpośrednio w kontrolerze.
Julian
2
@ Blowsie- Zaktualizowałem Plunks. Oto oryginał (zaktualizowany do wersji 1.2RC3): plnkr.co/edit/3Nwxxk?p=preview Oto jeden za pomocą usługi: plnkr.co/edit/a993Mn?p=preview
Pete BD
82

Niech to będzie proste. To jest tak proste jak

  1. Zwróć promisew usłudze (nie musisz korzystać thenz usługi)
  2. Użyj thenw swoim kontrolerze

Próbny. 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
źródło
W twoim linku jest app.factory, a w twoim kodzie jest app.service. Tak powinno być app.factoryw tym przypadku.
Re Captcha
1
działa również aplikacja. usługa. Poza tym - dla mnie wygląda to na najbardziej eleganckie rozwiązanie. Czy coś brakuje?
user1679130,
1
Wydaje się, że za każdym razem, gdy mam problem kątowy, @allenhwkim ma odpowiedź! (Trzeci raz w tym tygodniu - świetny komponent mapy ng btw)
Yarin
Chcę tylko wiedzieć, jak umieścić tutaj sukces i błąd z kodem statusu
Anuj,
58

Ponieważ jest asynchroniczny, $scopepobiera dane przed zakończeniem wywołania ajax.

Możesz użyć $qw swojej usłudze, aby utworzyć promisei zwrócić go kontrolerowi, a kontroler uzyska wynik w ramach then()połączenia przeciwko promise.

W twoich usługach

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;
});

Następnie w kontrolerze:

app.controller('MainCtrl', function( myService,$scope) {
  myService.async().then(function() {
    $scope.data = myService.data();
  });
});
Bzdury
źródło
2
+1 lubię ten najbardziej, ponieważ jest bardziej OO niż inne. Jednak czy jest jakiś powód, dla którego nie rób tego this.async = function() {i this.getData = function() {return data}? Mam nadzieję, że rozumiesz, o co mi chodzi
rower
@ bicykl Chciałem to w ten sam sposób, ale to nie zadziała, ponieważ obietnica musi zostać rozwiązana do końca. Jeśli tego nie zrobisz i spróbujesz uzyskać do niego dostęp w zwykły sposób, otrzymasz błąd odniesienia podczas uzyskiwania dostępu do danych wewnętrznych. Mam nadzieję, że to ma sens?
user6123723,
Jeśli dobrze rozumiem, konieczne jest dodanie deffered = $q.defer()wewnątrz myService.async, jeśli chcę wywołać myService.async () dwa lub więcej razy
demas
1
Ten przykład jest klasycznym odroczonym anty-wzorem . Nie ma potrzeby tworzenia obietnicy, $q.deferponieważ $httpusługa już zwraca obietnicę. Zwrócona obietnica zawiesi się, jeśli $httpzwróci błąd. Ponadto metody .successi .errorsą przestarzałe i zostały usunięte z AngularJS 1.6 .
georgeawg
23

tosh shimayama ma rozwiązanie, ale możesz dużo uprościć, jeśli wykorzystasz fakt, że $ http zwraca obietnice i że obietnice mogą zwrócić wartość:

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
    }
  });

});

Mała demonstracja w coffeescript: http://plunker.no.de/edit/ksnErx?live=preview

Twój plunker zaktualizowany moją metodą: http://plnkr.co/edit/mwSZGK?p=preview

Guillaume86
źródło
Spróbuję dalej w twoim podejściu. Ale lubię uchwycić wynik w służbie zamiast wracać. Zobacz pytanie związane z tym tutaj stackoverflow.com/questions/12504747/... . Lubię przetwarzać dane zwracane przez $ http na różne sposoby w kontrolerze. jeszcze raz dziękuję za pomoc.
bsr
możesz korzystać z obietnic w usługach, jeśli nie podoba ci się $ watch, możesz zrobić „promise.then (function (data) {service.data = data;}, onErrorCallback);`
Guillaume86
Dodałem plunker rozwidlony od twojego
Guillaume86
1
alternatywnie możesz użyć $ scope. $ emit z usługi i $ scope. $ on na ctrl, aby poinformować kontrolera, że ​​dane zostały zwrócone, ale tak naprawdę nie widzę korzyści
Guillaume86
7

Myślę, że o wiele lepszym sposobem byłoby coś takiego:

Usługa:

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

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

        ...

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

        ...

        return deferred.promise;
    }

    return{
        getAllFruits:getAllFruits
    }

});

A w kontrolerze możesz po prostu użyć:

$scope.fruits = FruitsManager.getAllFruits();

Angular automatycznie umieści rozdzielczość awesomeFruitsw $scope.fruits.

HasanAboShally
źródło
4
deferred.resolve ()? Bądź bardziej precyzyjny i gdzie jest połączenie $ http? Również dlaczego zwracasz obiekt w usłudze?
6

Miałem ten sam problem, ale kiedy surfowałem po Internecie, zrozumiałem, że $ http domyślnie zwraca obietnicę, a potem mogę użyć jej z „wtedy” po zwróceniu „danych”. spójrz na kod:

 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
źródło
4

W przypadku powiązania interfejsu użytkownika z tablicą należy upewnić się, że zaktualizowano tę samą tablicę bezpośrednio, ustawiając długość na 0 i przekazując dane do tablicy.

Zamiast tego (który ustawia inne odwołanie do tablicy, o dataktórym twój interfejs użytkownika nie będzie wiedział):

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

Spróbuj tego:

 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]);
      }
    });
  };

Oto skrzypce, które pokazują różnicę między ustawieniem nowej tablicy a opróżnieniem i dodaniem do istniejącej. Nie mogłem uruchomić twojego plnkr, ale mam nadzieję, że to zadziała dla ciebie!

Gloopy
źródło
to nie zadziałało. w dzienniku konsoli widziałem, że d jest poprawnie aktualizowany w przypadku wywołania zwrotnego powodzenia, ale nie danych. Być może funkcja jest już wykonana.
bsr
Ta metoda powinna zdecydowanie zadziałać, być może ma to coś wspólnego z typem danych d nie będącym tablicą (w asp.net trzeba na przykład uzyskać dostęp do dd dla tablicy). Zobacz ten plik plnkr, aby zobaczyć przykład pchania ciągu do tablicy w przypadku błędu: plnkr.co/edit/7FuwlN?p=preview
Gloopy
1
angular.copy(d, data)będzie również działać. Gdy miejsce docelowe zostanie dostarczone do metody copy (), najpierw usunie elementy miejsca docelowego, a następnie skopiuje nowe ze źródła.
Mark Rajcok
4

W związku z tym przeszedłem podobny problem, ale nie z pobieraniem lub wysyłaniem przez Angular, ale z rozszerzeniem wykonanym przez inną firmę (w moim przypadku rozszerzenie Chrome).
Problem, z którym się spotkałem, polega na tym, że rozszerzenie Chrome nie powróci, then()więc nie mogłem tego zrobić w powyższym rozwiązaniu, ale wynik jest nadal asynchroniczny.
Więc moim rozwiązaniem jest stworzenie usługi i przejście do oddzwaniania

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

Potem w moim kontrolerze

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

Mam nadzieję, że pomoże to innym uzyskać ten sam problem.

Shadoweb
źródło
4

Przeczytałem http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/ [AngularJS pozwala nam usprawnić logikę naszego kontrolera, składając obietnicę bezpośrednio na zakresie, a nie ręcznie przekazując rozwiązane wartość w wywołaniu zwrotnym sukcesu.]

tak prosto i poręcznie :)

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();
            });

Mam nadzieję, że to pomoże

Whisher
źródło
nie działa zwracana wartość defrred.promisenie jest funkcją.
Jürgen Paul
@PineappleUndertheSea dlaczego musi to być funkcja? To przedmiot obietnicy.
Chev
@PineappleUndertheSea miałeś na myśli odroczenie, a nie odroczenie?
Derrick,
2
Jak zauważył PeteBD, ta forma $scope.items = Data.getData(); jest przestarzała w Anglular
najsilniejszy
2

Naprawdę nie podoba mi się fakt, że z powodu „obiecującego” sposobu robienia rzeczy konsument usługi korzystającej z $ http musi „wiedzieć”, jak rozpakować odpowiedź.

Chcę tylko zadzwonić i pobrać dane, podobnie jak w starym $scope.items = Data.getData();sposobie, który jest już przestarzały .

Próbowałem przez chwilę i nie znalazłem idealnego rozwiązania, ale oto mój najlepszy strzał ( Plunker ). Może być komuś przydatny.

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;
}); 

Następnie kontroler:

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
  };
});

Są wady, które już dostrzegam

  • Musisz przekazać obiekt, do którego chcesz dodać dane , co nie jest intuicyjnym ani powszechnym wzorcem w Angular
  • getDatamoże zaakceptować objparametr tylko w postaci obiektu (chociaż może również zaakceptować tablicę), co nie będzie problemem dla wielu aplikacji, ale jest to bolesne ograniczenie
  • Trzeba przygotować obiekt wejściowy $scope.dataz = {}aby uczynić go przedmiotem (w zasadzie to, co $scope.clearData()robi powyżej), lub = []na tablicy, czy to nie będzie działać (mamy już konieczności zakładają coś o jakie dane nadchodzi). Próbowałem wykonać ten krok przygotowawczy IN getData, ale bez powodzenia.

Niemniej jednak zapewnia wzorzec, który usuwa płytę kontrolną „obiecaj rozpakuj” i może być przydatny w przypadkach, gdy chcesz użyć pewnych danych uzyskanych z $ http w więcej niż jednym miejscu, zachowując jednocześnie SUCHOŚĆ.

najlepiej
źródło
1

Jeśli chodzi o buforowanie odpowiedzi w usłudze, oto kolejna wersja, która wydaje się bardziej prosta niż to, co widziałem do tej pory:

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;
            });

    })();

});

ta usługa zwróci buforowane dane lub $http.get;

 dataStorage.then(function(data) {
     $scope.data = data;
 },function(e){
    console.log('err: ' + e);
 });
maioman
źródło
0

Wypróbuj poniższy kod

Możesz podzielić kontroler (PageCtrl) i usługę (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
źródło