Как отменить запросы $resource

Я пытаюсь понять, как использовать свойство timeout $resource для динамической отмены ожидающих запросов. В идеале я хотел бы просто иметь возможность отменять запросы с определенными атрибутами (на основе отправленных параметров), но, похоже, это невозможно. Тем временем я просто пытаюсь отменить все ожидающие запросы, а затем сбрасываю обещание тайм-аута, чтобы разрешить новые запросы.

Похоже, проблема в том, что конфигурация $resource допускает только одно статическое обещание для значения тайм-аута. Имеет смысл, как я мог бы сделать это, если бы я делал отдельные вызовы $http, поскольку я мог бы просто передать новые обещания для тайм-аута, но как это может работать для $resource? Я установил здесь пример планкера: http://plnkr.co/edit/PP2tqDYXh1NAOU3yqCwP?p=preview

Вот мой код контроллера:

app.controller('MainCtrl', function($scope, $timeout, $q, $resource) {
  $scope.canceller = $q.defer();
  $scope.pending = 0;
  $scope.actions = [];
  var API = $resource(
    'index.html', {}, {
      get: {
        method: 'GET',
        timeout: $scope.canceller.promise
      }
    }
  )

  $scope.fetchData = function() {
    if ($scope.pending) {
      $scope.abortPending();
    }
    $scope.pending = 1;
    $scope.actions.push('request');
    API.get({}, function() {
      $scope.actions.push('completed');
      $scope.pending = 0;
    }, function() {
      $scope.actions.push('aborted');
    });
  }

  $scope.abortPending = function() {
    $scope.canceller.resolve();
    $scope.canceller = $q.defer();
  }
});

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

Я уверен, что что-то упускаю, поскольку возможность отмены ожидающих запросов кажется довольно важной функцией большинства веб-приложений (по крайней мере, тех, которые я создал).

Спасибо


person Greg Michalec    schedule 10.02.2014    source источник
comment
Похоже, это больше не работает с AngularJS 1.3. - stackoverflow.com/questions/28497318/github.com/angular/angular.js/issues/9332 (Больше нельзя отменить запрос $resource обещанием)   -  person Daniel Darabos    schedule 08.10.2015


Ответы (6)


Ответ Gecko IT у меня работает, но мне пришлось внести некоторые изменения, чтобы:

  • Включить отмену ajax-вызова ресурса несколько раз без необходимости повторного создания ресурса.
  • Сделать ресурс обратно совместимым. Это означает, что нет необходимости изменять какой-либо код приложения (контроллера), кроме фабрики ресурсов.
  • Сделать код совместимым с JSLint

Это полная реализация фабрики сервисов (вам просто нужно указать правильное имя модуля):

'use strict';

/**
 * ResourceFactory creates cancelable resources.
 * Work based on: https://stackoverflow.com/a/25448672/1677187
 * which is based on: https://developer.rackspace.com/blog/cancelling-ajax-requests-in-angularjs-applications/
 */
/* global array */
angular.module('module_name').factory('ResourceFactory', ['$q', '$resource',
    function($q, $resource) {

        function abortablePromiseWrap(promise, deferred, outstanding) {
            promise.then(function() {
                deferred.resolve.apply(deferred, arguments);
            });

            promise.catch(function() {
                deferred.reject.apply(deferred, arguments);
            });

            /**
             * Remove from the outstanding array
             * on abort when deferred is rejected
             * and/or promise is resolved/rejected.
             */
            deferred.promise.finally(function() {
                array.remove(outstanding, deferred);
            });
            outstanding.push(deferred);
        }

        function createResource(url, options, actions) {
            var resource;
            var outstanding = [];
            actions = actions || {};

            Object.keys(actions).forEach(function(action) {
                var canceller = $q.defer();
                actions[action].timeout = canceller.promise;
                actions[action].Canceller = canceller;
            });

            resource = $resource(url, options, actions);

            Object.keys(actions).forEach(function(action) {
                var method = resource[action];

                resource[action] = function() {
                    var deferred = $q.defer(),
                    promise = method.apply(null, arguments).$promise;

                    abortablePromiseWrap(promise, deferred, outstanding);

                    return {
                        $promise: deferred.promise,

                        abort: function() {
                            deferred.reject('Aborted');
                        },
                        cancel: function() {
                            actions[action].Canceller.resolve('Call cancelled');

                            // Recreate canceler so that request can be executed again
                            var canceller = $q.defer();
                            actions[action].timeout = canceller.promise;
                            actions[action].Canceller = canceller;
                        }
                    };
                };
            });

            /**
             * Abort all the outstanding requests on
             * this $resource. Calls promise.reject() on outstanding [].
             */
            resource.abortAll = function() {
                for (var i = 0; i < outstanding.length; i++) {
                    outstanding[i].reject('Aborted all');
                }
                outstanding = [];
            };

            return resource;
        }

        return {
            createResource: function (url, options, actions) {
                return createResource(url, options, actions);
            }
        };
    }
]);

Использование такое же, как в примере Gecko IT. Сервисный завод:

'use strict';

angular.module('module_name').factory('YourResourceServiceName', ['ResourceFactory', function(ResourceFactory) {
    return ResourceFactory.createResource('some/api/path/:id', { id: '@id' }, {
        create: {
            method: 'POST'
        },
        update: {
            method: 'PUT'
        }
    });
}]);

Использование в контроллере (обратная совместимость):

var result = YourResourceServiceName.create(data);
result.$promise.then(function success(data, responseHeaders) {
    // Successfully obtained data
}, function error(httpResponse) {
    if (httpResponse.status === 0 && httpResponse.data === null) { 
        // Request has been canceled
    } else { 
        // Server error 
    }
});
result.cancel(); // Cancels XHR request

Альтернативный подход:

var result = YourResourceServiceName.create(data);
result.$promise.then(function success(data, responseHeaders) {       
    // Successfully obtained data
}).catch(function (httpResponse) {
    if (httpResponse.status === 0 && httpResponse.data === null) { 
        // Request has been canceled
    } else { 
        // Server error 
    }
});
result.cancel(); // Cancels XHR request

Дальнейшие улучшения:

  • Мне не нравится проверять, был ли запрос отменен. Лучшим подходом было бы прикрепить атрибут httpResponse.isCanceled при отмене запроса и аналогичный для прерывания.
person Nikola M.    schedule 12.09.2014
comment
Отмена() действия отменяет все ожидающие экземпляры этого действия, потому что все они использовали один и тот же экземпляр Canceller при запуске. (Я не проверял, просто читал код - поправьте меня, если я ошибаюсь) - person robert4; 20.04.2015
comment
Почему лучше иметь abort() и cancel() отдельно, чем только cancel()? (Есть ли лучшее место, чтобы задать этот вопрос?) - person robert4; 20.04.2015
comment
Первый вопрос: Кажется, вы правы (я тоже не пробовал). Однако это не кажется проблемой, потому что, когда вы работаете с несколькими экземплярами ресурса, у каждого экземпляра есть свой собственный Canceller. Это может быть проблемой только в том случае, если вы выполняете .query() несколько раз одновременно, а затем cancel(). - person Nikola M.; 21.04.2015
comment
Второй вопрос: Вы правы. Вы вполне можете жить без abort(). Вероятно, у него есть какая-то другая цель, но первоначальный создатель этого кода может ответить на это лучше :) - person Nikola M.; 21.04.2015
comment
Обратите внимание, что в настоящее время отмена не работает из-за ошибки в ngResource. Также я не понимаю, как прерывание когда-либо сработает. См. код angular. . Только при разрешении обещания тайм-аута xhr будет отменен. - person jtheoof; 28.04.2015

(для Angular 1.2.28+) Привет всем, я просто хотел, чтобы это было легко понять, как я справился с этой проблемой, выглядит следующим образом:

Здесь я объявляю параметр тайм-аута

$scope.stopRequestGetAllQuestions=$q.defer();

то я использую его следующим образом

return $resource(urlToGet, {}, {get:{ timeout: stopRequestGetAllQuestions.promise }});

и если я хочу остановить предыдущие вызовы $resource, я просто разрешаю этот объект stopRequestGetAllQuestions, вот и все.

stopRequestGetAllQuestions.resolve();

но если я хочу остановить предыдущие и начать новый вызов $resource, я делаю это после stopRequestGetAllQuestions.resolve();:

stopRequestGetAllQuestions = $q.defer(); 
person katmanco    schedule 12.09.2015

В настоящее время существует довольно много примеров. Следующие два я нашел довольно информативными:

В этом примере показано, как работать с запросами $resource и $http: https://developer.rackspace.com/blog/cancelling-ajax-requests-in-angularjs-applications/

а также

Это проще и предназначено только для $http: http://odetocode.com/blogs/scott/archive/2014/04/24/canceling-http-requests-in-angularjs.aspx

person Tsonev    schedule 24.07.2014

Привет, я сделал собственный обработчик на основе https://developer.rackspace.com/blog/cancelling-ajax-requests-in-angularjs-applications/

.factory('ResourceFactory', ["$q", "$resource", function($q, $resource) {
    function createResource(url, options, actions) {
        var actions = actions || {},
        resource,
        outstanding = [];

        Object.keys(actions).forEach(function (action) {
            console.log(actions[action]);
            var canceller = $q.defer();
            actions[action].timeout = canceller.promise;
            actions[action].Canceller = canceller;
        });

        resource = $resource(url, options, actions);

        Object.keys(actions).forEach(function (action) {
            var method = resource[action];

            resource[action] = function () {
                var deferred = $q.defer(),
                promise = method.apply(null, arguments).$promise;

                abortablePromiseWrap(promise, deferred, outstanding);

                return {
                    promise: deferred.promise,

                    abort: function () {
                        deferred.reject('Aborted');
                    },
                    cancel: function () {
                        console.log(actions[action]);
                        actions[action].Canceller.resolve("Call cancelled");
                    }
                };
            };
        });

        /**
        * Abort all the outstanding requests on
        * this $resource. Calls promise.reject() on outstanding [].
        */
        resource.abortAll = function () {
            for (var i = 0; i < outstanding.length; i++) {
                outstanding[i].reject('Aborted all');
            }
            outstanding = [];
        };

        return resource;
    }

    return {
        createResource: function (url, options, actions) {
            return createResource(url, options, actions);
        }
    }
}])

function abortablePromiseWrap(promise, deferred, outstanding) {
    promise.then(function () {
        deferred.resolve.apply(deferred, arguments);
    });

    promise.catch(function () {
        deferred.reject.apply(deferred, arguments);
    });
    /**
    * Remove from the outstanding array
    * on abort when deferred is rejected
    * and/or promise is resolved/rejected.
    */
    deferred.promise.finally(function () {
        array.remove(outstanding, deferred);
    });
    outstanding.push(deferred);
}


//Usage SERVICE
factory("ServiceFactory", ["apiBasePath", "$resource", "ResourceFactory", function (apiBasePath, $resource, QiteResourceFactory) {
    return ResourceFactory.createResource(apiBasePath + "service/:id", { id: '@id' }, null);
}])

//Usage Controller
var result = ServiceFactory.get();
console.log(result);
result.promise.then(function (data) {       
    $scope.services = data;
}).catch(function (a) {
    console.log("catch", a);
})
//Actually cancels xhr request
result.cancel();
person Gecko    schedule 22.08.2014
comment
Что это за array находится в строке array.remove(outstanding, deferred);? Где это определено? JSLint выдает ошибку из-за этого. - person Nikola M.; 09.09.2014
comment
Это может отменить 1 запрос xhr, а затем не больше, потому что после того, как Canceller разрешен (), он не воссоздается и не может быть разрешен снова. - person robert4; 20.04.2015

Одним из решений является повторное создание ресурса каждый раз, когда он вам нужен.

// for canceling an ajax request
$scope.canceler = $q.defer();

// create a resource
// (we have to re-craete it every time because this is the only
// way to renew the promise)
function getAPI(promise) {
    return $resource(
        'index.html', {}, {
            get: {
                method: 'GET',
                timeout: promise
            }
        }
    );
}

$scope.fetchData = function() {

    // abort previous requests if they are still running
    $scope.canceler.resolve();

    // create a new canceler
    $scope.canceler = $q.defer();

    // instead of using "API.get" we use "getAPI().get"
    getAPI( $scope.canceler.promise ).get({}, function() {
        $scope.actions.push('completed');
        $scope.pending = 0;
    }, function() {
        $scope.actions.push('aborted');
    });

}
person Hendrik Jan    schedule 08.05.2014

В нашей попытке решить эту задачу мы пришли к следующему решению:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Cancel resource</title>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.9/angular-resource.js"></script>
  <script>
angular.module("app", ["ngResource"]).
factory(
  "services",
  ["$resource", function($resource)
  {
    function resolveAction(resolve)
    {
      if (this.params)
      {
        this.timeout = this.params.timeout;
        this.params.timeout = null;
      }

      this.then = null;
      resolve(this);
    }

    return $resource(
      "http://md5.jsontest.com/",
      {},
      {
        MD5:
        {
          method: "GET",
          params: { text: null },
          then: resolveAction
        },
      });
  }]).
controller(
  "Test",
  ["services", "$q", "$timeout", function(services, $q, $timeout)
  {
    this.value = "Sample text";
    this.requestTimeout = 100;

    this.call = function()
    {
      var self = this;

      self.result = services.MD5(
      {
        text: self.value,
        timeout: $q(function(resolve)
        {
          $timeout(resolve, self.requestTimeout);
        })
      });
    }
  }]);
  </script>
</head>
<body ng-app="app" ng-controller="Test as test">
  <label>Text: <input type="text" ng-model="test.value" /></label><br/>
  <label>Timeout: <input type="text" ng-model="test.requestTimeout" /></label><br/>
  <input type="button" value="call" ng-click="test.call()"/>
  <div ng-bind="test.result.md5"></div>
</body>
</html>

Как это работает

  1. $resource объединяет определение действия, параметры запроса и данные для создания параметра конфигурации для запроса $http.
  2. параметр конфигурации, переданный в запрос $http, обрабатывается как обещание, подобное объекту, поэтому он может содержать функцию для инициализации конфигурации.
  3. Затем функция действия может передать обещание тайм-аута из параметров в конфигурацию.

Подробнее см. в разделе "Отмена запроса ресурсов Angularjs". .

person Vladimir Nesterovsky    schedule 02.02.2015
comment
Как это отразится на более старых версиях angular, которые все еще используют устаревшие расширения обещаний success() и error()? - person g1de0n_ph; 06.01.2016
comment
github.com/angular/angular.js показывает, что подход работает с версии 1.2.x. и вверх. В более ранних версиях никакие действия не объединялись в конфигурацию http. - person Vladimir Nesterovsky; 07.01.2016