Comment annuler une requête $ http dans AngularJS?


190

Étant donné une requête Ajax dans AngularJS

$http.get("/backend/").success(callback);

quel est le moyen le plus efficace d'annuler cette requête si une autre requête est lancée (même backend, paramètres différents par exemple).


8
Aucune des réponses ci-dessous n'annule la demande elle-même. Il n'y a aucun moyen d'annuler une requête HTTP une fois qu'elle quitte le navigateur. Toutes les réponses ci-dessous abandonnent simplement l'auditeur d'une manière ou d'une autre. La requête HTTP atteint toujours le serveur, est toujours traitée et le serveur enverra toujours une réponse, il s'agit simplement de savoir si le client écoute toujours cette réponse ou non.
Liam



@Liam ma question ne s'annulait pas sur le serveur. ce serait très spécifique à la technologie / mise en œuvre de votre serveur. Je craignais d'abandonner le rappel
Sonic Soul

Réponses:


326

Cette fonctionnalité a été ajoutée à la version 1.1.5 via un paramètre de délai d'expiration:

var canceler = $q.defer();
$http.get('/someUrl', {timeout: canceler.promise}).success(successCallback);
// later...
canceler.resolve();  // Aborts the $http request if it isn't finished.

14
que dois-je faire si j'ai besoin à la fois d'un délai d'expiration et d'une annulation manuelle via promesse?
Raman Chodźka

15
@ RamanChodźka Vous pouvez faire les deux avec une promesse; vous pouvez définir un délai pour annuler la promesse après un certain temps, soit avec la setTimeoutfonction native de JavaScript, soit avec le $timeoutservice Angular .
Quinn Strahl

9
canceler.resolve () annulera les futures requêtes. C'est une meilleure solution: odetocode.com/blogs/scott/archive/2014/04/24/…
Toolkit

7
un autre bon exemple d'une solution plus complète de Ben Nadel: bennadel.com/blog
Pete

3
Ça ne marche pas vraiment. Pourriez-vous fournir un échantillon fonctionnel?
Edward Olamisan

10

L'annulation d'Angular $ http Ajax avec la propriété timeout ne fonctionne pas dans Angular 1.3.15. Pour ceux qui ne peuvent pas attendre que cela soit corrigé, je partage une solution jQuery Ajax enveloppée dans Angular.

La solution passe par deux services:

  • HttpService (un wrapper autour de la fonction jQuery Ajax);
  • PendingRequestsService (suit les demandes Ajax en attente / ouvertes)

Voici le service PendingRequestsService:

    (function (angular) {
    'use strict';
    var app = angular.module('app');
    app.service('PendingRequestsService', ["$log", function ($log) {            
        var $this = this;
        var pending = [];
        $this.add = function (request) {
            pending.push(request);
        };
        $this.remove = function (request) {
            pending = _.filter(pending, function (p) {
                return p.url !== request;
            });
        };
        $this.cancelAll = function () {
            angular.forEach(pending, function (p) {
                p.xhr.abort();
                p.deferred.reject();
            });
            pending.length = 0;
        };
    }]);})(window.angular);

Le service HttpService:

     (function (angular) {
        'use strict';
        var app = angular.module('app');
        app.service('HttpService', ['$http', '$q', "$log", 'PendingRequestsService', function ($http, $q, $log, pendingRequests) {
            this.post = function (url, params) {
                var deferred = $q.defer();
                var xhr = $.ASI.callMethod({
                    url: url,
                    data: params,
                    error: function() {
                        $log.log("ajax error");
                    }
                });
                pendingRequests.add({
                    url: url,
                    xhr: xhr,
                    deferred: deferred
                });            
                xhr.done(function (data, textStatus, jqXhr) {                                    
                        deferred.resolve(data);
                    })
                    .fail(function (jqXhr, textStatus, errorThrown) {
                        deferred.reject(errorThrown);
                    }).always(function (dataOrjqXhr, textStatus, jqXhrErrorThrown) {
                        //Once a request has failed or succeeded, remove it from the pending list
                        pendingRequests.remove(url);
                    });
                return deferred.promise;
            }
        }]);
    })(window.angular);

Plus tard dans votre service, lorsque vous chargez des données, vous utiliserez HttpService au lieu de $ http:

(function (angular) {

    angular.module('app').service('dataService', ["HttpService", function (httpService) {

        this.getResources = function (params) {

            return httpService.post('/serverMethod', { param: params });

        };
    }]);

})(window.angular);

Plus tard dans votre code, vous souhaitez charger les données:

(function (angular) {

var app = angular.module('app');

app.controller('YourController', ["DataService", "PendingRequestsService", function (httpService, pendingRequestsService) {

    dataService
    .getResources(params)
    .then(function (data) {    
    // do stuff    
    });    

    ...

    // later that day cancel requests    
    pendingRequestsService.cancelAll();
}]);

})(window.angular);

9

L'annulation des demandes émises avec $httpn'est pas prise en charge avec la version actuelle d'AngularJS. Il y a une demande de tirage ouverte pour ajouter cette capacité, mais ce PR n'a pas encore été examiné, il n'est donc pas clair s'il va devenir le noyau AngularJS.


que le PR a été rejeté, OP a soumis une mise à jour ici github.com/angular/angular.js/pull/1836
Mark Nadig

Et cela a également été fermé.
frapontillo

Une version de celui-ci a atterri comme ceci . J'essaie toujours de comprendre la syntaxe pour utiliser la version finale. Je souhaite que les PR soient livrés avec des échantillons d'utilisation! :)
SimplGy

La page de documentation angulaire docs.angularjs.org/api/ng/service/$http dans «Utilisation» décrit un paramètre de délai d'expiration, et mentionne également quels objets (une promesse) sont acceptés.
Igor Lino le

6

Si vous souhaitez annuler les demandes en attente sur stateChangeStart avec ui-router, vous pouvez utiliser quelque chose comme ceci:

// en service

                var deferred = $q.defer();
                var scope = this;
                $http.get(URL, {timeout : deferred.promise, cancel : deferred}).success(function(data){
                    //do something
                    deferred.resolve(dataUsage);
                }).error(function(){
                    deferred.reject();
                });
                return deferred.promise;

// dans la configuration d'UIrouter

$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
    //To cancel pending request when change state
       angular.forEach($http.pendingRequests, function(request) {
          if (request.cancel && request.timeout) {
             request.cancel.resolve();
          }
       });
    });

Cela a fonctionné pour moi - très simple et j'en ai ajouté un autre pour nommer l'appel afin que je puisse sélectionner l'appel et annuler uniquement certains des appels
Simon Dragsbæk

Pourquoi la configuration du routeur d'interface utilisateur doit-elle savoir si elle request.timeoutest présente?
trysis

6

Pour une raison quelconque, config.timeout ne fonctionne pas pour moi. J'ai utilisé cette approche:

let cancelRequest = $q.defer();
let cancelPromise = cancelRequest.promise;

let httpPromise = $http.get(...);

$q.race({ cancelPromise, httpPromise })
    .then(function (result) {
...
});

Et cancelRequest.resolve () pour annuler. En fait, cela n'annule pas une demande mais vous n'obtenez au moins aucune réponse inutile.

J'espère que cela t'aides.


Avez-vous vu votre SyntaxError { cancelPromise, httpPromise }?
Mephiztopheles

ceci est la syntaxe ES6, vous pouvez essayer {c: cancelPromise, h: httpPromise}
Aliaksandr Hmyrak

Je vois, objet shortinitializer
Mephiztopheles

3

Cela améliore la réponse acceptée en décorant le service $ http avec une méthode d'abandon comme suit ...

'use strict';
angular.module('admin')
  .config(["$provide", function ($provide) {

$provide.decorator('$http', ["$delegate", "$q", function ($delegate, $q) {
  var getFn = $delegate.get;
  var cancelerMap = {};

  function getCancelerKey(method, url) {
    var formattedMethod = method.toLowerCase();
    var formattedUrl = encodeURI(url).toLowerCase().split("?")[0];
    return formattedMethod + "~" + formattedUrl;
  }

  $delegate.get = function () {
    var cancelerKey, canceler, method;
    var args = [].slice.call(arguments);
    var url = args[0];
    var config = args[1] || {};
    if (config.timeout == null) {
      method = "GET";
      cancelerKey = getCancelerKey(method, url);
      canceler = $q.defer();
      cancelerMap[cancelerKey] = canceler;
      config.timeout = canceler.promise;
      args[1] = config;
    }
    return getFn.apply(null, args);
  };

  $delegate.abort = function (request) {
    console.log("aborting");
    var cancelerKey, canceler;
    cancelerKey = getCancelerKey(request.method, request.url);
    canceler = cancelerMap[cancelerKey];

    if (canceler != null) {
      console.log("aborting", cancelerKey);

      if (request.timeout != null && typeof request.timeout !== "number") {

        canceler.resolve();
        delete cancelerMap[cancelerKey];
      }
    }
  };

  return $delegate;
}]);
  }]);

QUE FAIT CE CODE?

Pour annuler une demande, un délai d'expiration de «promesse» doit être défini. Si aucun délai d'expiration n'est défini sur la requête HTTP, le code ajoute un délai d'expiration «promesse». (Si un délai d'expiration est déjà défini, rien n'est changé).

Cependant, pour résoudre la promesse, nous avons besoin d'une poignée sur le «différé». On utilise donc une carte pour pouvoir récupérer le "différé" plus tard. Lorsque nous appelons la méthode d'abandon, le "différé" est récupéré de la carte, puis nous appelons la méthode de résolution pour annuler la requête http.

J'espère que cela aide quelqu'un.

LIMITES

Actuellement, cela ne fonctionne que pour $ http.get mais vous pouvez ajouter du code pour $ http.post et ainsi de suite

COMMENT UTILISER ...

Vous pouvez ensuite l'utiliser, par exemple, lors d'un changement d'état, comme suit ...

rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
  angular.forEach($http.pendingRequests, function (request) {
        $http.abort(request);
    });
  });

Je crée une application qui déclenche des requêtes http en même temps et je dois toutes les abandonner manuellement. J'ai essayé votre code mais il n'annule que la dernière requête. Cela vous est-il déjà arrivé? Toute aide serait appréciée.
Miguel Trabajo

1
le code ici maintient une recherche des références aux objets différés afin qu'ils puissent être récupérés plus tard puisque l'objet différé est nécessaire pour faire un abandon. la chose importante avec la recherche est la paire clé: valeur. La valeur est l'objet différé. La clé est une chaîne générée en fonction de la méthode / de l'url de la requête. Je suppose que vous abandonnez plusieurs requêtes à la même méthode / URL. Pour cette raison, toutes les clés sont identiques et se remplacent les unes les autres dans la carte. Vous devez modifier la logique de génération de clé afin qu'une seule soit générée même si l'url / la méthode sont les mêmes.
danday74

1
suite d'en haut ... ce n'est pas un bogue dans le code, le code gère l'abandon de plusieurs requêtes ... mais le code n'a tout simplement jamais été destiné à traiter l'abandon de plusieurs requêtes à la même URL en utilisant la même méthode http ... mais si vous modifiez la logique, vous devriez pouvoir la faire fonctionner assez facilement.
danday74

1
Merci beaucoup! Je faisais plusieurs demandes à la même URL mais avec des paramètres différents, et après que vous en ayez parlé, j'ai changé cette ligne et cela a fonctionné comme un charme!
Miguel Trabajo

1

voici une version qui gère plusieurs demandes, vérifie également l'état annulé dans le rappel pour supprimer les erreurs dans le bloc d'erreur. (en tapuscrit)

niveau du contrôleur:

    requests = new Map<string, ng.IDeferred<{}>>();

dans mon http obtenir:

    getSomething(): void {
        let url = '/api/someaction';
        this.cancel(url); // cancel if this url is in progress

        var req = this.$q.defer();
        this.requests.set(url, req);
        let config: ng.IRequestShortcutConfig = {
            params: { id: someId}
            , timeout: req.promise   // <--- promise to trigger cancellation
        };

        this.$http.post(url, this.getPayload(), config).then(
            promiseValue => this.updateEditor(promiseValue.data as IEditor),
            reason => {
                // if legitimate exception, show error in UI
                if (!this.isCancelled(req)) {
                    this.showError(url, reason)
                }
            },
        ).finally(() => { });
    }

méthodes d'assistance

    cancel(url: string) {
        this.requests.forEach((req,key) => {
            if (key == url)
                req.resolve('cancelled');
        });
        this.requests.delete(url);
    }

    isCancelled(req: ng.IDeferred<{}>) {
        var p = req.promise as any; // as any because typings are missing $$state
        return p.$$state && p.$$state.value == 'cancelled';
    }

maintenant en regardant l'onglet réseau, je vois que cela fonctionne à merveille. J'ai appelé la méthode 4 fois et seule la dernière est passée.

entrez la description de l'image ici


req.resolve ('annulé'); ne fonctionne pas pour moi, j'utilise la version 1.7.2. Même je veux annuler un appel s'il est rappelé et que le premier appel est toujours en attente. s'il vous plaît aider. Je veux toujours fournir les données d'appel nouvellement appelées en annulant toutes les API en attente de la même URL
Sudarshan Kalebere

1

Vous pouvez ajouter une fonction personnalisée au $httpservice en utilisant un "décorateur" qui ajouterait la abort()fonction à vos promesses.

Voici un code de travail:

app.config(function($provide) {
    $provide.decorator('$http', function $logDecorator($delegate, $q) {
        $delegate.with_abort = function(options) {
            let abort_defer = $q.defer();
            let new_options = angular.copy(options);
            new_options.timeout = abort_defer.promise;
            let do_throw_error = false;

            let http_promise = $delegate(new_options).then(
                response => response, 
                error => {
                    if(do_throw_error) return $q.reject(error);
                    return $q(() => null); // prevent promise chain propagation
                });

            let real_then = http_promise.then;
            let then_function = function () { 
                return mod_promise(real_then.apply(this, arguments)); 
            };

            function mod_promise(promise) {
                promise.then = then_function;
                promise.abort = (do_throw_error_param = false) => {
                    do_throw_error = do_throw_error_param;
                    abort_defer.resolve();
                };
                return promise;
            }

            return mod_promise(http_promise);
        }

        return $delegate;
    });
});

Ce code utilise la fonctionnalité de décorateur d'angularjs pour ajouter une with_abort()fonction au $httpservice.

with_abort()utilise l' $httpoption de délai d'expiration qui vous permet d'annuler une requête http.

La promesse retournée est modifiée pour inclure une abort()fonction. Il a également du code pour s'assurer que cela abort()fonctionne même si vous enchaînez les promesses.

Voici un exemple de la façon dont vous l'utiliseriez:

// your original code
$http({ method: 'GET', url: '/names' }).then(names => {
    do_something(names));
});

// new code with ability to abort
var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
    function(names) {
        do_something(names));
    });

promise.abort(); // if you want to abort

Par défaut, lorsque vous appelez, abort()la demande est annulée et aucun des gestionnaires de promesse ne s'exécute.

Si vous voulez que vos gestionnaires d'erreurs soient appelés, passez true à abort(true).

Dans votre gestionnaire d'erreurs, vous pouvez vérifier si "l'erreur" est due à un "abandon" en vérifiant la xhrStatuspropriété. Voici un exemple:

var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
    function(names) {
        do_something(names));
    }, 
    function(error) {
        if (er.xhrStatus === "abort") return;
    });
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.