Bei meinem derzeitigen Kunden haben wir eine große AngularJS-Anwendung, die so konfiguriert ist, dass sie eine ganzseitige Fehlermeldung anzeigt, wenn eine der $http-Anfragen mit einem Fehler endet. Dies ist mit einem Fehlerabfangprogramm implementiert, wie Sie es erwarten würden. Wir verwenden jedoch auch einige rechenintensive Ressourcen, bei denen es hin und wieder zu einer Zeitüberschreitung kommt. Diese Kombination ist knifflig: Ein Benutzer löst beim Navigieren zu einer bestimmten Seite eine Ressourcenanforderung aus, navigiert zu einer zweiten Seite und landet plötzlich mit einer Fehlermeldung, da die Anforderung der ersten Seite einen Timeout-Fehler ausgelöst hat. Dies ist ein besonders unangenehmer Nebeneffekt, den ich in diesem Beitrag auf allgemeine Weise behandeln werde.
Es gibt natürlich mehrere Lösungen für dieses Problem. Wir könnten eine robustere Implementierung im Backend erstellen, die keine Zeitüberschreitung verursacht, sondern Wiederholungsversuche akzeptiert. Wir könnten die Ganzseiten-Fehlermeldung in eine weniger auffällige Form umwandeln (aber Sie würden immer noch eine deplatzierte Fehlermeldung erhalten). In diesem Beitrag werde ich das Problem mit einem anderen Ansatz beheben: alle laufenden Anfragen werden abgebrochen, wenn ein Benutzer zu einem anderen Ort wechselt (der Routenteil der URL). Das macht Sinn: Ihr Browser tut dasselbe, wenn Sie von einer Seite zu einer anderen navigieren. Warum also nicht dieses Verhalten in Ihrer Angular-App nachahmen?
Ich habe eine ziemlich ausführliche Implementierung erstellt, um zu erklären, wie man das macht. Am Ende dieses Beitrags finden Sie einen Link zu dem Code als gepackte Bower-Komponente, die in jede Angular 1.2+ App eingefügt werden kann.
Um eine laufende Anfrage abzubrechen, bietet Angular nicht allzu viele Optionen. Unter der Haube gibt es einige Stellen, an denen Sie sich einhaken können, aber das wird nicht nötig sein. In der Dokumentation zur Verwendung von$http wird die Eigenschaft timeout erwähnt, die ein Versprechen zum Abbruch des zugrunde liegenden Aufrufs akzeptiert. Perfekt! Wenn wir ein Versprechen auf alle erstellten Anfragen setzen und diese auf einmal abbrechen, wenn der Benutzer zu einer anderen Seite navigiert, sind wir (wahrscheinlich) startklar.
Lassen Sie uns einen Interceptor schreiben, der das Versprechen in jede Anfrage einfügt:
[code language="javascript"]
angular.module('angularCancelOnNavigateModule')
.factory('HttpRequestTimeoutInterceptor', function ($q, HttpPendingRequestsService) {
return {
request: function (config) {
config = config || {};
if (config.timeout === undefined && !config.noCancelOnRouteChange) {
config.timeout = HttpPendingRequestsService.newTimeout();
}
return config;
}
};
});
[/code]
Der Interceptor überschreibt die Timeout-Eigenschaft nicht, wenn sie explizit gesetzt ist. Und wenn die Option noCancelOnRouteChange auf true gesetzt ist, wird die Anfrage nicht abgebrochen. Zur besseren Trennung der Bereiche habe ich einen neuen Dienst (den HttpPendingRequestsService) erstellt, der neue Zeitüberschreitungsversprechen ausgibt und Verweise auf diese speichert.
Werfen wir einen Blick auf diesen Dienst für ausstehende Anfragen:
[code language="javascript"]
angular.module('angularCancelOnNavigateModule')
.service('HttpPendingRequestsService', function ($q) {
var cancelPromises = [];
function newTimeout() {
var cancelPromise = $q.defer();
cancelPromises.push(cancelPromise);
return cancelPromise.promise;
}
function cancelAll() {
angular.forEach(cancelPromises, function (cancelPromise) {
cancelPromise.promise.isGloballyCancelled = true;
cancelPromise.resolve();
});
cancelPromises.length = 0;
}
zurück {
newTimeout: newTimeout,
cancelAll: cancelAll
};
});
[/code]
Dieser Dienst erstellt also neue Zeitüberschreitungsversprechen, die in einem Array gespeichert werden. Wenn die Funktion cancelAll aufgerufen wird, werden alle Zeitüberschreitungsversprechen aufgelöst (und damit alle Anfragen, die mit dem Versprechen konfiguriert wurden, abgebrochen) und das Array geleert. Durch Setzen der Eigenschaft isGloballyCancelled für das Versprechen-Objekt kann eine Antwortversprechen-Methode prüfen, ob sie abgebrochen wurde oder eine andere Ausnahme aufgetreten ist. Darauf komme ich in einer Minute zurück. Jetzt schließen wir den Interceptor an und rufen die Funktion cancelAll zu einem sinnvollen Zeitpunkt auf. Es gibt mehrere Ereignisse, die im Root-Bereich ausgelöst werden und sich gut als Hook-Kandidaten eignen. Letztendlich habe ich mich für $locationChangeSuccess . Es wird nur ausgelöst, wenn die Standortänderung erfolgreich war (daher der Name) und nicht durch einen anderen Ereignis-Listener abgebrochen wird.
[code language="javascript"]
angular
.module('angularCancelOnNavigateModule', [])
.config(function($httpProvider) {
$httpProvider.interceptors.push('HttpRequestTimeoutInterceptor');
})
.run(function ($rootScope, HttpPendingRequestsService) {
$rootScope.$on('$locationChangeSuccess', function (event, newUrl, oldUrl) {
if (newUrl !== oldUrl) {
HttpPendingRequestsService.cancelAll();
}
})
});
[/code]
Beim Schreiben von Tests für dieses Setup habe ich festgestellt, dass das Ereignis $locationChangeSuccess zu Beginn jedes Tests ausgelöst wird, auch wenn sich der Standort noch nicht geändert hat. Um diese Situation zu umgehen, führt die Funktion eine einfache Differenzprüfung durch.
Ein weiteres Problem trat beim Testen auf. Wenn die Anfrage abgebrochen wird, erstellt Angular eine leere Fehlerantwort, die in unserem Fall immer noch den Ganzseitenfehler auslöst. Wir müssen diese Fehlerantworten abfangen und behandeln. Wir können einfach eine responseError-Funktion in unseren bestehenden Interceptor einfügen. Und erinnern Sie sich an die spezielle Eigenschaft isGloballyCancelled, die wir für das Versprechen festgelegt haben? Auf diese Weise können wir zwischen abgebrochenen und anderen Antworten unterscheiden.
Wir fügen die folgende Funktion zum Interceptor hinzu:
[code language="javascript"]
responseError: function (response) {
if (response.config.timeout.isGloballyCancelled) {
return $q.defer().promise;
}
return $q.reject(response);
}
[/code]
Die responseError-Funktion muss ein Versprechen zurückgeben, das normalerweise die Antwort als abgelehnt zurückwirft. Das ist jedoch nicht das, was wir wollen: Es soll weder ein Erfolgs- noch ein Fehler-Callback aufgerufen werden. Wir geben einfach ein nie auflösendes Versprechen für alle abgebrochenen Anfragen zurück, um das gewünschte Verhalten zu erreichen.
Das ist alles! Um die Wiederverwendung dieser Funktionalität in Ihrer Angular-Anwendung zu erleichtern, habe ich dieses Modul als vollständig getestete Bower-Komponente verpackt. Sie können das Modul in diesem GitHub Repo ausprobieren.
Verfasst von

Albert Brand
Unsere Ideen
Weitere Blogs
Contact
Let’s discuss how we can support your journey.



