Traitement de la réponse $ http en service

233

J'ai récemment publié une description détaillée du problème auquel je suis confronté ici à SO. Comme je n'ai pas pu envoyer de $httpdemande réelle , j'ai utilisé le délai d'attente pour simuler un comportement asynchrone. La liaison de données de mon modèle à la vue fonctionne correctement, avec l'aide de @Gloopy

Maintenant, lorsque j'utilise $httpau lieu de $timeout(testé localement), je pouvais voir que la demande asynchrone a réussi et dataest remplie de réponse json dans mon service. Mais, ma vue n'est pas à jour.

Plunkr mis à jour ici

bsr
la source

Réponses:

419

Voici un Plunk qui fait ce que vous voulez: http://plnkr.co/edit/TTlbSv?p=preview

L'idée est que vous travaillez directement avec les promesses et leurs fonctions "alors" pour manipuler et accéder aux réponses renvoyées de manière asynchrone.

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

Voici une version légèrement plus compliquée qui met en cache la demande afin que vous ne la fassiez que la première fois ( 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
la source
13
Existe-t-il un moyen d'appeler toujours les méthodes de réussite et d'erreur dans le contrôleur après l'interception du service then?
andyczerwonka
2
@PeteBD Si je veux appeler myService.async()plusieurs fois depuis différents contrôleurs, comment organiseriez-vous le service pour qu'il ne fasse $http.get()que la première demande, et toutes les demandes suivantes renvoient simplement un tableau d'objets locaux qui est défini lors du premier appel à myService.async(). En d'autres termes, je veux éviter les demandes multiples et inutiles au service JSON, alors que vraiment je n'ai besoin que d'en faire une.
GFoley83
5
@ GFoley83 - c'est parti : plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview . Si vous regardez la console, vous verrez que la demande n'est faite qu'une seule fois.
Pete BD
3
@PeteBD Je pense que vous pouvez également utiliser $scope.data = myService.async()directement dans le contrôleur.
Julian
2
@ Blowsie- J'ai mis à jour les Plunks. Voici l'original (mis à jour à 1.2RC3): plnkr.co/edit/3Nwxxk?p=preview En voici un en utilisant le service: plnkr.co/edit/a993Mn?p=preview
Pete BD
82

Que ce soit simple. C'est aussi simple que

  1. Retour promisedans votre service (pas besoin d'utiliser thenen service)
  2. Utilisation thendans votre contrôleur

Démo. 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
la source
Dans votre lien, c'est app.factory, et dans votre code, c'est app.service. C'est supposé app.factorydans ce cas.
Re Captcha
1
travail app.service aussi. De plus, cela me semble être la solution la plus élégante. Suis-je en train de manquer quelque chose?
user1679130
1
Il semble que chaque fois que j'ai un problème angulaire, @allenhwkim a la réponse! (3e fois cette semaine - super composant ng-map btw)
Yarin
je veux juste savoir comment mettre le succès et l'erreur ici avec status_code
Anuj
58

Parce qu'il est asynchrone, le $scopeobtient les données avant la fin de l'appel ajax.

Vous pouvez utiliser $qdans votre service pour le créer promiseet le rendre au contrôleur, et le contrôleur obtiendra le résultat dans l' then()appel contre promise.

A votre service,

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

Ensuite, dans votre contrôleur:

app.controller('MainCtrl', function( myService,$scope) {
  myService.async().then(function() {
    $scope.data = myService.data();
  });
});
Tosh
la source
2
+1 J'aime celui-ci le meilleur car il est plus OO que les autres. Mais y a-t-il une raison pour laquelle vous ne le faites pas this.async = function() {et this.getData = function() {return data}? J'espère que vous comprenez ce que je veux dire
vélo
@bicycle Je le voulais de la même manière mais cela ne fonctionnera pas car la promesse doit être résolue jusqu'au bout. Si vous ne le faites pas et essayez d'y accéder comme vous le feriez normalement, vous obtiendrez une erreur de référence lors de l'accès aux données internes. J'espère que cela a du sens?
user6123723
Si je comprends bien, il est nécessaire d'ajouter deffered = $q.defer()à l'intérieur de myService.async si je veux appeler myService.async () deux fois ou plus
demas
1
Cet exemple est un anti-motif différé classique . Il n'est pas nécessaire de fabriquer une promesse avec $q.defercar le $httpservice renvoie déjà une promesse. La promesse retournée se bloquera si le $httpretourne une erreur. De plus, les méthodes .successet .errorsont obsolètes et ont été supprimées d'AngularJS 1.6 .
georgeawg
23

tosh shimayama a une solution mais vous pouvez simplifier beaucoup si vous utilisez le fait que $ http renvoie des promesses et que les promesses peuvent renvoyer une valeur:

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

});

Une petite démonstration en coffeescript: http://plunker.no.de/edit/ksnErx?live=preview

Votre plunker mis à jour avec ma méthode: http://plnkr.co/edit/mwSZGK?p=preview

Guillaume86
la source
J'essaierai plus loin dans votre approche. Mais, j'aime capturer le résultat en service au lieu de revenir. Voir la question à ce sujet ici stackoverflow.com/questions/12504747/… . J'aime traiter les données renvoyées par $ http de différentes manières dans le contrôleur. Merci encore pour votre aide.
bsr
vous pouvez utiliser des promesses dans les services, si vous n'aimez pas $ watch, vous pouvez faire ´promise.then (function (data) {service.data = data;}, onErrorCallback); `
Guillaume86
J'ai ajouté un plunker bifurqué du vôtre
Guillaume86
1
Alternativement, vous pouvez utiliser $ scope. $ emit depuis le service et $ scope. $ sur le ctrl pour vous dire que le contrôleur a renvoyé les données mais je ne vois pas vraiment d'avantage
Guillaume86
7

Une bien meilleure façon, je pense, serait quelque chose comme ceci:

Un service:

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

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

        ...

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

        ...

        return deferred.promise;
    }

    return{
        getAllFruits:getAllFruits
    }

});

Et dans le contrôleur, vous pouvez simplement utiliser:

$scope.fruits = FruitsManager.getAllFruits();

Angular mettra automatiquement le résolu awesomeFruitsdans le $scope.fruits.

HasanAboShally
la source
4
deferred.resolve ()? Soyez plus précis s'il vous plaît et où est l'appel $ http? Aussi pourquoi renvoyez-vous un objet dans un .service?
6

J'ai eu le même problème, mais lorsque je surfais sur Internet, j'ai compris que $ http renvoyait par défaut une promesse, alors je pouvais l'utiliser avec "puis" après avoir renvoyé les "données". regardez le code:

 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
la source
4

Lorsque vous liez l'interface utilisateur à votre tableau, vous devez vous assurer de mettre à jour ce même tableau directement en définissant la longueur sur 0 et en poussant les données dans le tableau.

Au lieu de cela (qui définit une référence de tableau différente à datalaquelle votre interface utilisateur ne sera pas au courant):

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

essaye ça:

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

Voici un violon qui montre la différence entre définir un nouveau tableau et vider et en ajouter un existant. Je n'ai pas pu faire fonctionner votre plnkr mais j'espère que cela fonctionne pour vous!

Gloopy
la source
cela n'a pas fonctionné. dans le journal de la console, je pouvais voir que d était correctement mis à jour en cas de rappel réussi, mais pas les données. Peut-être que la fonction est déjà exécutée.
bsr
Cette méthode devrait certainement fonctionner, peut-être qu'elle a quelque chose à voir avec le type de données de d qui n'est pas un tableau (dans asp.net, vous devez accéder à dd pour le tableau par exemple). Voir ce plnkr pour un exemple de poussée d'une chaîne dans le tableau en cas d'erreur: plnkr.co/edit/7FuwlN?p=preview
Gloopy
1
angular.copy(d, data)fonctionnera également. Lorsqu'une destination est fournie à la méthode copy (), elle supprimera d'abord les éléments de la destination, puis copiera les nouveaux à partir de la source.
Mark Rajcok
4

À ce sujet, j'ai rencontré un problème similaire, mais pas avec get ou post créé par Angular mais avec une extension faite par un tiers (dans mon cas, Chrome Extension).
Le problème que j'ai rencontré est que l'extension Chrome ne reviendra pas then(), je n'ai donc pas pu le faire de la manière indiquée dans la solution ci-dessus, mais le résultat est toujours asynchrone.
Donc ma solution est de créer un service et de procéder à un rappel

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

Puis dans mon contrôleur

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

J'espère que cela peut aider les autres à résoudre le même problème.

Shadoweb
la source
4

J'ai lu http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/ [AngularJS nous permet de rationaliser la logique de notre contrôleur en plaçant une promesse directement sur la portée, plutôt que de remettre manuellement la résolution valeur dans un rappel réussi.]

si simple et pratique :)

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

J'espère que cette aide

Whisher
la source
ne fonctionne pas. la valeur de retour de defrred.promisen'est pas une fonction.
Jürgen Paul
@PineappleUndertheSea pourquoi faut-il que ce soit une fonction? C'est un objet promis.
Chev
@PineappleUndertheSea vouliez-vous utiliser différé et non défrisé?
Derrick
2
Comme PeteBD l'a souligné, cette forme $scope.items = Data.getData(); est déconseillée dans Anglular
poshest
2

Je n'aime vraiment pas le fait que, en raison de la façon "prometteuse" de faire les choses, le consommateur du service qui utilise $ http doit "savoir" comment décompresser la réponse.

Je veux juste appeler quelque chose et extraire les données, comme à l'ancienne $scope.items = Data.getData();, qui est maintenant obsolète .

J'ai essayé pendant un certain temps et je n'ai pas trouvé de solution parfaite, mais voici mon meilleur coup ( Plunker ). Cela peut être utile à quelqu'un.

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

Puis contrôleur:

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

Les défauts que je peux déjà repérer sont

  • Vous devez passer l'objet auquel vous souhaitez ajouter les données , ce qui n'est pas un modèle intuitif ou courant dans Angular
  • getDatane peut accepter le objparamètre que sous la forme d'un objet (bien qu'il puisse également accepter un tableau), ce qui ne sera pas un problème pour de nombreuses applications, mais c'est une limitation douloureuse
  • Vous devez préparer l'objet d'entrée $scope.dataavec = {}pour en faire un objet (essentiellement ce qui $scope.clearData()fait ci-dessus), ou = []pour un tableau, ou cela ne fonctionnera pas (nous devons déjà supposer quelque chose sur les données qui arrivent). J'ai essayé de faire cette étape de préparation IN getData, mais pas de chance.

Néanmoins, il fournit un modèle qui supprime le passe-partout "promettre le déballage" du contrôleur, et peut être utile dans les cas où vous souhaitez utiliser certaines données obtenues à partir de $ http à plusieurs endroits tout en le maintenant SEC.

chic
la source
1

En ce qui concerne la mise en cache de la réponse en service, voici une autre version qui semble plus simple que ce que j'ai vu jusqu'à présent:

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

    })();

});

ce service renverra soit les données mises en cache, soit $http.get;

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

Veuillez essayer le code ci-dessous

Vous pouvez diviser le contrôleur (PageCtrl) et le service (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
la source