Обработка $ http ответа в сервисе


233

Недавно я опубликовал подробное описание проблемы, с которой я сталкиваюсь здесь, в SO. Поскольку я не мог отправить реальный $httpзапрос, я использовал тайм-аут для имитации асинхронного поведения. Привязка данных из моей модели для просмотра работает правильно, с помощью @Gloopy

Теперь, когда я использую $httpвместо $timeout(проверено локально), я вижу, что асинхронный запрос был успешным и dataзаполнен ответом json в моем сервисе. Но мой взгляд не обновляется.

обновил Plunkr здесь

Ответы:


419

Вот Plunk, который делает то, что вы хотите: http://plnkr.co/edit/TTlbSv?p=preview

Идея состоит в том, что вы работаете с обещаниями напрямую и их «тогдашними» функциями для манипулирования и доступа к асинхронно возвращаемым ответам.

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

Вот немного более сложная версия, которая кэширует запрос, поэтому вы делаете его только в первый раз ( 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;
    });
  };
});

13
Есть ли способ по-прежнему вызывать методы успеха и ошибки в контроллере после перехвата службы then?
andyczerwonka

2
@PeteBD Если я хочу вызывать мой myService.async()несколько раз с разных контроллеров, как бы вы организовали сервис так, чтобы он выполнял только $http.get()первый запрос, а все последующие запросы просто возвращают массив локальных объектов, который устанавливается при первом вызове myService.async(). Другими словами, я хочу избежать множества ненужных запросов к сервису JSON, когда мне действительно нужно сделать только один.
GFoley83

5
@ GFoley83 - вот и вы: plnkr.co/edit/2yH1F4IMZlMS8QsV9rHv?p=preview . Если вы посмотрите на консоль, то увидите, что запрос делается только один раз.
Пит BD

3
@PeteBD Я думаю, вы также можете использовать $scope.data = myService.async()непосредственно в контроллере.
Джулиан

2
@ Blowsie- Я обновил Плункс. Вот оригинал (обновленный до 1.2RC3): plnkr.co/edit/3Nwxxk?p=preview Вот один, использующий сервис: plnkr.co/edit/a993Mn?p=preview
Пит BD

82

Пусть это будет просто. Это так же просто, как

  1. Возврат promiseв вашем сервисе (не нужно использовать thenв сервисе)
  2. Используйте thenв вашем контроллере

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

В вашей ссылке это app.factory, и в вашем коде это app.service. Это предполагается app.factoryв этом случае.
Re Captcha

1
app.service тоже работает. Также - это для меня выглядит как самое элегантное решение. Я что-то упускаю?
user1679130

1
Кажется, что каждый раз, когда у меня есть проблема Angular, у @allenhwkim есть ответ! (3-й раз на этой неделе - кстати, отличный компонент ng-map)
Ярин,

я просто хочу знать, как поставить успех и ошибку здесь с помощью status_code
Anuj

58

Поскольку он асинхронный, $scopeон получает данные до завершения вызова ajax.

Вы можете использовать $qв своем сервисе, чтобы создать promiseи вернуть его контроллеру, и контроллер получит результат в рамках then()вызова против promise.

К вашим услугам,

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

Затем в вашем контроллере:

app.controller('MainCtrl', function( myService,$scope) {
  myService.async().then(function() {
    $scope.data = myService.data();
  });
});

2
+1 Мне больше нравится этот, так как он больше, чем другие. Однако нет никаких оснований не делать этого this.async = function() {и this.getData = function() {return data}? Я надеюсь, вы понимаете, что я имею в виду
велосипед

@Bicycle Я хотел это так же, но это не сработает, потому что обещание должно быть выполнено полностью. Если вы этого не сделаете и попытаетесь получить к нему доступ, как обычно, вы получите справочную ошибку при доступе к внутренним данным. Надеюсь, это имеет смысл?
user6123723

Если я правильно понимаю, необходимо добавить deffered = $q.defer()в myService.async, если я хочу вызывать myService.async () два или более раз
demas

1
Этот пример является классическим отложенным анти-паттерном . Нет необходимости создавать обещание, так $q.deferкак $httpсервис уже возвращает обещание. Возвращенное обещание будет зависать, если $httpвозвращается ошибка. В дополнение к этому .successи .errorметоды устарели и были удалены из AngularJS 1.6 .
georgeawg

23

У tosh shimayama есть решение, но вы можете упростить многое, если используете тот факт, что $ http возвращает обещания, а обещания могут возвращать значение:

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

});

Небольшая демонстрация в coffeescript: http://plunker.no.de/edit/ksnErx?live=preview

Ваш плункер обновлен по моему методу: http://plnkr.co/edit/mwSZGK?p=preview


Я попробую дальше по вашему подходу. Но мне нравится фиксировать результат в сервисе, а не возвращать. Смотрите вопрос, связанный с этим здесь stackoverflow.com/questions/12504747/… . Мне нравится обрабатывать данные, возвращаемые $ http, различными способами в контроллере. В очередной раз благодарим за помощь.
2012 г.

вы можете использовать обещания в сервисах, если вам не нравится $ watch, вы можете сделать ´promise.then (function (data) {service.data = data;}, onErrorCallback); `
Guillaume86

Я добавил плункер, раздвоенный от твоего
Guillaume86

1
в качестве альтернативы вы можете использовать $ scope. $ emit из сервиса и $ scope. $ on на ctrl, чтобы сообщить контроллеру, что данные возвращены, но я не вижу выгоды
Guillaume86

7

Намного лучше, я думаю, что-то вроде этого:

Обслуживание:

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

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

        ...

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

        ...

        return deferred.promise;
    }

    return{
        getAllFruits:getAllFruits
    }

});

А в контроллере вы можете просто использовать:

$scope.fruits = FruitsManager.getAllFruits();

Angular автоматически поместит разрешенное awesomeFruitsв $scope.fruits.


4
deferred.resolve ()? Если быть более точным, пожалуйста, а где $ http звонок? Кроме того, почему вы возвращаете объект в .service?

6

У меня была такая же проблема, но когда я занимался серфингом в интернете, я понял, что $ http возвращает по умолчанию обещание, а затем я могу использовать его с «then» после возврата «data». посмотрите на код:

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

4

При связывании пользовательского интерфейса с вашим массивом вам нужно будет убедиться, что вы обновляете тот же массив напрямую, устанавливая длину в 0 и помещая данные в массив.

Вместо этого (который устанавливает другую ссылку на массив, о dataкоторой ваш пользовательский интерфейс не будет знать):

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

попробуй это:

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

Вот скрипка, которая показывает разницу между установкой нового массива и его очисткой и добавлением к существующему. Я не мог заставить ваш plnkr работать, но, надеюсь, это работает для вас!


это не сработало. в журнале консоли я мог видеть, что d обновляется должным образом при успешном обратном вызове, но не данные Может быть, функция уже выполнена.
2012 г.

Этот метод определенно должен работать, возможно, он как-то связан с тем, что тип данных d не является массивом (например, в asp.net вам потребуется доступ к dd для массива). Смотрите этот plnkr для примера, вставляющего строку в массив при ошибке: plnkr.co/edit/7FuwlN?p=preview
Gloopy

1
angular.copy(d, data)тоже будет работать. Когда в метод copy () передается адресат, он сначала удаляет элементы адресата, а затем копирует новые из источника.
Марк Райкок

4

В связи с этим я столкнулся с подобной проблемой, но не с get или post, созданными Angular, а с расширением, сделанным сторонней организацией (в моем случае Chrome Extension).
Проблема, с которой я столкнулся, заключается в том, что расширение Chrome не вернется, then()поэтому я не смог сделать это так, как в приведенном выше решении, но результат все еще асинхронный.
Поэтому я решил создать сервис и перейти к обратному вызову.

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

Тогда в моем контроллере

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

Надеюсь, что это может помочь другим получить ту же проблему.


4

Я прочитал http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/ [AngularJS позволяет нам упростить логику нашего контроллера, поместив обещание непосредственно в область, а не вручную передавая разрешенную значение в обратном вызове успеха.]

так просто и удобно :)

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

Надеюсь это поможет


не работает возвращаемое значение defrred.promiseне является функцией.
Юрген Пауль

@PineappleUndertheSea, почему это должно быть функцией? Это обещанный объект.
Chev

@PineappleUndertheSea вы имели в виду использовать отложенный, а не отложенный?
Деррик

2
Как указывал PeteBD, эта форма $scope.items = Data.getData(); устарела в Anglular
самое лучшее

2

Мне действительно не нравится тот факт, что из-за «обещанного» способа работы потребитель службы, использующий $ http, должен «знать», как распаковать ответ.

Я просто хочу позвонить и вывести данные, как в старом $scope.items = Data.getData();стиле, который сейчас не рекомендуется .

Я пытался некоторое время и не нашел идеального решения, но вот мой лучший шанс ( Плункер ). Это может быть полезно для кого-то.

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

Затем контроллер:

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

Я могу уже заметить недостатки

  • Вы должны передать объект, к которому вы хотите добавить данные , что не является интуитивным или распространенным шаблоном в Angular.
  • getDataможет принимать objпараметр только в форме объекта (хотя он также может принимать массив), что не будет проблемой для многих приложений, но является серьезным ограничением
  • Вы должны подготовить объект ввода $scope.dataс , = {}чтобы сделать его объектом ( по существу , что $scope.clearData()делает выше), или = []для массива, или он не будет работать (мы уже имея предположить , что - то о том , что грядет данные). Я пытался сделать этот шаг подготовки IN getData, но не повезло.

Тем не менее, он предоставляет шаблон, который удаляет шаблон контроллера "обещание разворачивания" и может быть полезен в тех случаях, когда вы хотите использовать определенные данные, полученные из $ http, в более чем одном месте, сохраняя их СУХОЙ.


1

Что касается кэширования ответа в сервисе, вот еще одна версия, которая кажется более прямой, чем то, что я видел до сих пор:

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

    })();

});

эта услуга будет возвращать либо кэшированные данные или $http.get;

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

0

Пожалуйста, попробуйте следующий код

Вы можете разделить контроллер (PageCtrl) и сервис (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;
             });
         }
    }
}());

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.