¿Cómo cancelar una solicitud de $ http en AngularJS?

Dada una solicitud de Ajax en AngularJS

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

¿Cuál es la forma más efectiva de cancelar esa solicitud si se lanza otra solicitud (el mismo back-end, diferentes parámetros, por ejemplo).

Esta característica se agregó a la versión 1.1.5 a través de un parámetro de tiempo de espera:

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

La cancelación de Angular $ http Ajax con la propiedad de tiempo de espera no funciona en Angular 1.3.15. Para aquellos que no pueden esperar a que esto se solucione, estoy compartiendo una solución jQuery Ajax envuelta en Angular.

La solución involucra dos servicios:

  • HttpService (un envoltorio alrededor de la función jQuery Ajax);
  • PendingRequestsService (rastrea las solicitudes pendientes / abiertas de Ajax)

Aquí va el servicio 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); 

El servicio 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); 

Más adelante en su servicio cuando esté cargando datos, usaría HttpService en lugar de $ http:

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

Más tarde en su código, desea cargar los datos:

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

La cancelación de solicitudes emitidas con $http no es compatible con la versión actual de AngularJS. Se ha abierto una solicitud de extracción para agregar esta capacidad, pero este PR todavía no se ha revisado, por lo que no está claro si se convertirá en el núcleo de AngularJS.

Si desea cancelar solicitudes pendientes en stateChangeStart con ui-router, puede usar algo como esto:

// en servicio

  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; 

// en la configuración de 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(); } }); }); 

Por alguna razón, config.timeout no funciona para mí. Usé este enfoque:

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

Esto mejora la respuesta aceptada al decorar el servicio $ http con un método de cancelación de la siguiente manera …

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

¿QUÉ ESTÁ HACIENDO ESTE CÓDIGO?

Para cancelar una solicitud, se debe establecer un tiempo de espera de “promesa”. Si no se establece el tiempo de espera en la solicitud HTTP, el código agrega un tiempo de espera de “promesa”. (Si se establece un tiempo de espera ya no se cambia nada).

Sin embargo, para resolver la promesa, necesitamos un control sobre el “diferido”. Por lo tanto, usamos un mapa para que podamos recuperar el “diferido” más tarde. Cuando llamamos al método abort, el “diferido” se recupera del mapa y luego llamamos al método resolve para cancelar la solicitud http.

Espero que esto ayude a alguien.

LIMITACIONES

Actualmente, esto solo funciona para $ http.get pero puede agregar código para $ http.post y así sucesivamente

CÓMO UTILIZAR …

Puede usarlo, por ejemplo, en el cambio de estado, de la siguiente manera …

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

Aquí hay una versión que maneja múltiples solicitudes, también comprueba el estado cancelado en la callback para suprimir los errores en el bloque de errores. (en Mecanografiado)

nivel de controlador:

  requests = new Map>(); 

en mi http obtener:

  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étodos de ayuda

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

ahora mirando la pestaña de la red, veo que funciona maravillosamente. Llamé al método 4 veces y solo el último pasó.

enter image description here

Puede agregar una función personalizada al servicio $http usando un “decorador” que agregaría la función abort() a sus promesas.

Aquí hay un código de trabajo:

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

Este código usa la función de decorador de angularjs para agregar una función with_abort() al servicio $http .

with_abort() usa la opción $http timeout que le permite cancelar una solicitud http.

La promesa devuelta se modifica para incluir una función abort() . También tiene un código para asegurarse de que el abort() funciona incluso si realiza una cadena de promesas.

Aquí hay un ejemplo de cómo lo usarías:

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

Por defecto, cuando llamas a abort() la solicitud se cancela y no se ejecuta ninguno de los manejadores de promesa.

Si desea que se llame a los manejadores de errores, pase true para abort(true) .

En su controlador de errores puede verificar si el “error” se debió a un “aborto” al marcar la propiedad xhrStatus . Aquí hay un ejemplo:

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