Come cancellare una richiesta $ http in AngularJS?

Data una richiesta Ajax in AngularJS

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

qual è il modo più efficace per cancellare quella richiesta se viene lanciata un’altra richiesta (stesso back-end, diversi parametri per esempio).

Questa funzionalità è stata aggiunta alla versione 1.1.5 tramite un parametro di timeout:

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

Annullare l’Ajax $ $ angular con la proprietà timeout non funziona in Angular 1.3.15. Per quelli che non vedono l’ora che questo venga risolto, sto condividendo una soluzione jQuery Ajax avvolta in Angular.

La soluzione prevede due servizi:

  • HttpService (un wrapper attorno alla funzione jQuery Ajax);
  • PendingRequestsService (tiene traccia delle richieste Ajax in sospeso / aperte)

Qui va il servizio 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); 

Il servizio 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); 

Più tardi nel tuo servizio quando carichi i dati, useresti HttpService invece di $ http:

 (function (angular) { angular.module('app').service('dataService', ["HttpService", function (httpService) { this.getResources = function (params) { return httpService.post('/serverMethod', { param: params }); }; }]); })(window.angular); 

Più avanti nel tuo codice vorresti caricare i dati:

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

L’annullamento delle richieste emesse con $http non è supportato con la versione corrente di AngularJS. È stata aperta una richiesta di pull per aggiungere questa funzionalità, ma questa PR non è stata ancora esaminata, quindi non è chiaro se sarà in grado di trasformarla in core AngularJS.

Se si desidera annullare le richieste in sospeso su stateChangeStart con ui-router, è ansible utilizzare qualcosa come questo:

// in servizio

  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; 

// in UIrouter config

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

Per qualche ragione config.timeout non funziona per me. Ho usato questo approccio:

 let cancelRequest = $q.defer(); let cancelPromise = cancelRequest.promise; let httpPromise = $http.get(...); $q.race({ cancelPromise, httpPromise }) .then(function (result) { ... }); 

Questo migliora la risposta accettata decorando il servizio $ http con un metodo abort come segue …

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

CHE COSA E ‘QUESTO CODICE?

Per cancellare una richiesta è necessario impostare un timeout “promise”. Se nessun timeout è impostato sulla richiesta HTTP, il codice aggiunge un timeout “promise”. (Se è già impostato un timeout, non viene modificato nulla).

Tuttavia, per risolvere la promise abbiamo bisogno di un handle sul “rinviato”. Usiamo quindi una mappa in modo da poter recuperare il “rinviato” più tardi. Quando chiamiamo il metodo abort, il “posticipato” viene recuperato dalla mappa e quindi chiamiamo il metodo di risoluzione per annullare la richiesta http.

Spero che questo aiuti qualcuno.

LIMITAZIONI

Attualmente questo funziona solo per $ http.get ma è ansible aggiungere codice per $ http.post e così via

COME USARE …

Puoi quindi usarlo, ad esempio, sul cambio di stato, come segue …

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

ecco una versione che gestisce più richieste, controlla anche lo stato annullato in callback per sopprimere gli errori nel blocco di errore. (in Typescript)

livello del controller:

  requests = new Map>(); 

nel mio http ottieni:

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

metodi di supporto

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

ora guardando la scheda di rete, vedo che funziona beatuifully. Ho chiamato il metodo 4 volte e solo l’ultimo è andato a buon fine.

inserisci la descrizione dell'immagine qui

Puoi aggiungere una funzione personalizzata al servizio $http usando un “decoratore” che aggiungerebbe la funzione abort() alle tue promesse.

Ecco un codice funzionante:

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

Questo codice utilizza la funzionalità di decoratore di angularjs per aggiungere una funzione with_abort() al servizio $http .

with_abort() usa l’opzione di timeout $http che ti consente di annullare una richiesta http.

La promise restituita viene modificata per includere una funzione abort() . Ha anche il codice per assicurarsi che l’ abort() funzioni anche se si promettono a catena.

Ecco un esempio di come lo useresti:

 // 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 

Per impostazione predefinita, quando si chiama abort() la richiesta viene annullata e nessuno dei gestori di promesse viene eseguito.

Se vuoi che i tuoi gestori di errori siano chiamati passa true ad abort(true) .

Nel tuo gestore di errori puoi controllare se “l’errore” era dovuto a un “abort” verificando la proprietà xhrStatus . Ecco un esempio:

 var promise = $http.with_abort({ method: 'GET', url: '/names' }).then( function(names) { do_something(names)); }, function(error) { if (er.xhrStatus === "abort") return; });