Procesando $ http respuesta en servicio

233

Recientemente publiqué una descripción detallada del problema que estoy enfrentando aquí en SO. Como no podía enviar una $httpsolicitud real , utilicé el tiempo de espera para simular un comportamiento asincrónico. El enlace de datos de mi modelo para ver funciona correctamente, con la ayuda de @Gloopy

Ahora, cuando uso en $httplugar de $timeout(probado localmente), pude ver que la solicitud asincrónica fue exitosa y dataestá llena de respuesta json en mi servicio. Pero mi opinión no se está actualizando.

Plunkr actualizado aquí

bsr
fuente

Respuestas:

419

Aquí hay un Plunk que hace lo que quieres: http://plnkr.co/edit/TTlbSv?p=preview

La idea es que trabaje directamente con las promesas y sus funciones "entonces" para manipular y acceder a las respuestas devueltas asincrónicamente.

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

Aquí hay una versión un poco más complicada que almacena en caché la solicitud para que solo la haga la primera 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
fuente
13
¿Hay alguna forma de llamar a los métodos de éxito y error en el controlador después de que el servicio haya interceptado then?
andyczerwonka
2
@PeteBD Si quiero llamar a mis myService.async()múltiples veces desde varios controladores, ¿cómo organizaría el servicio para que solo se haga $http.get()para la primera solicitud, y todas las solicitudes posteriores solo devuelven una matriz de objetos local que se establece en la primera llamada myService.async()? En otras palabras, quiero evitar múltiples solicitudes innecesarias al servicio JSON, cuando realmente solo necesito hacer una.
GFoley83
55
@ GFoley83: aquí tienes: plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview . Si observa la consola, verá que la solicitud solo se realiza una vez.
Pete BD
3
@PeteBD Creo que también puedes usarlo $scope.data = myService.async()directamente en el controlador.
Julián
2
@ Blowsie- He actualizado los Plunks. Aquí está el original (actualizado a 1.2RC3): plnkr.co/edit/3Nwxxk?p=preview Aquí hay uno que usa el servicio: plnkr.co/edit/a993Mn?p=preview
Pete BD
82

Que sea simple. Es tan simple como

  1. Devolución promiseen su servicio (no es necesario usarlo thenen el servicio)
  2. Úselo thenen su controlador

Manifestación. 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
fuente
En su enlace, está app.factory, y en su código está app.service. Se supone app.factoryen este caso.
Re Captcha
1
El servicio de aplicaciones también funciona. Además, esto me parece la solución más elegante. ¿Me estoy perdiendo de algo?
user1679130
1
¡Parece que cada vez que tengo un problema angular @allenhwkim tiene la respuesta! (3ª vez este gran componente ng-mapa de semana por cierto)
Yarin
Solo quiero saber cómo colocar el éxito y el error aquí con status_code
Anuj
58

Como es asíncrono, $scopeobtiene los datos antes de que se complete la llamada ajax.

Puede usarlo $qen su servicio para crear promisey devolverlo al controlador, y el controlador obtendrá el resultado dentro de la then()llamada en contra promise.

A su servicio

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

Luego, en su controlador:

app.controller('MainCtrl', function( myService,$scope) {
  myService.async().then(function() {
    $scope.data = myService.data();
  });
});
Tosh
fuente
2
+1 Me gusta este mejor porque es más OO que los otros. Sin embargo, ¿hay alguna razón por la que no haces esto this.async = function() {y this.getData = function() {return data}? Espero que entiendas lo que quiero decir
bicicleta
@bicycle Lo quería de la misma manera, pero no funcionará porque la promesa debe resolverse por completo. Si no lo hace e intenta acceder a él como lo haría normalmente, obtendrá un error de referencia al acceder a los datos internos. Espero que tenga sentido?
user6123723
Si entiendo correctamente, es necesario agregar deffered = $q.defer()dentro de myService.async si quiero llamar a myService.async () dos o más veces
demas
1
Este ejemplo es un clásico anti-patrón diferido . No es necesario fabricar una promesa $q.deferya que el $httpservicio ya devuelve una promesa. La promesa devuelta se suspenderá si $httpdevuelve un error. Además, los métodos .successy .errorestán en desuso y se han eliminado de AngularJS 1.6 .
georgeawg
23

tosh shimayama tiene una solución, pero puede simplificar mucho si usa el hecho de que $ http devuelve promesas y que las promesas pueden devolver un 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
    }
  });

});

Una pequeña demostración en coffeescript: http://plunker.no.de/edit/ksnErx?live=preview

Su plunker actualizado con mi método: http://plnkr.co/edit/mwSZGK?p=preview

Guillaume86
fuente
Voy a intentar más a lo largo de su enfoque. Pero, me gusta capturar el resultado en el servicio en lugar de regresar. Vea la pregunta relacionada con esto aquí stackoverflow.com/questions/12504747/… . Me gusta procesar los datos devueltos por $ http de diferentes maneras en el controlador. De nuevo, gracias por tu ayuda.
bsr
puede usar promesas en los servicios, si no le gusta $ watch, puede hacer 'prometer. luego (function (data) {service.data = data;}, onErrorCallback); `
Guillaume86
He añadido una plunker bifurcada a la suya
Guillaume86
1
alternativamente, puede usar $ scope. $ emit del servicio y $ scope. $ on en el ctrl para decirle al controlador que los datos han regresado pero realmente no veo un beneficio
Guillaume86
7

Una forma mucho mejor creo que sería algo como esto:

Servicio:

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

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

        ...

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

        ...

        return deferred.promise;
    }

    return{
        getAllFruits:getAllFruits
    }

});

Y en el controlador simplemente puede usar:

$scope.fruits = FruitsManager.getAllFruits();

Angular pondrá automáticamente el resuelto awesomeFruitsen el $scope.fruits.

HasanAboShally
fuente
44
deferred.resolve ()? Sea más preciso por favor y ¿dónde está la llamada $ http? Además, ¿por qué devuelve un objeto en un .service?
6

Tuve el mismo problema, pero cuando estaba navegando en Internet entendí que $ http devuelve por defecto una promesa, luego podría usarlo con "luego" después de devolver los "datos". mira el 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
fuente
4

Al vincular la interfaz de usuario a su matriz, querrá asegurarse de actualizar esa misma matriz directamente estableciendo la longitud en 0 e insertando los datos en la matriz.

En lugar de esto (que establece una referencia de matriz diferente a la dataque su IU no sabrá):

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

prueba esto:

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

Aquí hay un violín que muestra la diferencia entre configurar una nueva matriz frente a vaciar y agregar una existente. No pude hacer que tu plnkr funcionara, pero espero que esto funcione para ti.

Gloopy
fuente
Eso no funcionó. en el registro de la consola, pude ver que d se actualiza correctamente en la devolución de llamada exitosa, pero no en los datos. Puede ser la función ya está ejecutada.
bsr
Este método definitivamente debería funcionar, tal vez tenga algo que ver con que el tipo de datos de d no sea una matriz (en asp.net necesitaría acceder a dd para la matriz, por ejemplo). Vea este plnkr para ver un ejemplo al insertar una cadena en la matriz por error: plnkr.co/edit/7FuwlN?p=preview
Gloopy
1
angular.copy(d, data)También funcionará. Cuando se suministra un destino al método copy (), primero eliminará los elementos del destino y luego copiará los nuevos del origen.
Mark Rajcok
4

Relacionado con esto, pasé por un problema similar, pero no con get o post hecho por Angular sino con una extensión hecha por un tercero (en mi caso, Chrome Extension).
El problema que enfrenté es que la extensión de Chrome no volverá, then()por lo que no pude hacerlo de la manera anterior, pero el resultado sigue siendo asíncrono.
Entonces, mi solución es crear un servicio y proceder a una devolución de llamada

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

Entonces en mi controlador

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

Espero que esto pueda ayudar a otros a tener el mismo problema.

Shadoweb
fuente
4

He leído http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/ [AngularJS nos permite simplificar la lógica de nuestro controlador al hacer una promesa directamente en el alcance, en lugar de entregar manualmente el resuelto valor en una devolución de llamada exitosa.]

tan simple y práctico :)

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 esto ayude

Whisher
fuente
no funciona El valor de retorno de defrred.promiseno es una función.
Jürgen Paul
@PineappleUndertheSea ¿por qué necesita ser una función? Es un objeto de promesa.
Chev
@PineappleUndertheSea, ¿querías usar diferido y no defrred?
Derrick
2
Como PeteBD señaló, esta forma $scope.items = Data.getData(); está en desuso en Anglular
poshest
2

Realmente no me gusta el hecho de que, debido a la forma "prometedora" de hacer las cosas, el consumidor del servicio que usa $ http tiene que "saber" cómo desempaquetar la respuesta.

Solo quiero llamar a algo y obtener los datos, similar a la $scope.items = Data.getData();forma anterior, que ahora está en desuso .

Lo intenté por un tiempo y no encontré una solución perfecta, pero aquí está mi mejor oportunidad ( Plunker ). Puede ser útil para alguien.

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

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

Las fallas que ya puedo detectar son

  • Debe pasar el objeto al que desea agregar los datos , que no es un patrón intuitivo o común en Angular
  • getDatasolo puede aceptar el objparámetro en forma de un objeto (aunque también podría aceptar una matriz), lo que no será un problema para muchas aplicaciones, pero es una limitación importante
  • Tienes que preparar el objeto de entrada $scope.datacon = {}para que sea un objeto (en esencia, lo que $scope.clearData()lo hace más arriba), o = []de una matriz, o de lo contrario no funciona (ya estamos tener que asumir algo acerca de lo que está llegando datos). Traté de hacer este paso de preparación getData, pero no tuve suerte.

Sin embargo, proporciona un patrón que elimina la repetitiva "promesa de desenvolver" del controlador, y podría ser útil en los casos en que desee utilizar ciertos datos obtenidos de $ http en más de un lugar mientras lo mantiene SECO.

poshest
fuente
1

En cuanto al almacenamiento en caché de la respuesta en el servicio, aquí hay otra versión que parece más sencilla que la que he visto hasta ahora:

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

    })();

});

este servicio devolverá los datos en caché o $http.get;

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

Por favor, intente el siguiente código

Puede dividir el controlador (PageCtrl) y el servicio (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
fuente