Die Organisation des Zugriffs auf Serverdaten ist die Grundlage für fast jede einseitige Anwendung. Alle dynamischen Inhalte in solchen Anwendungen werden vom Backend heruntergeladen.
In den meisten Fällen arbeiten HTTP-Anforderungen an den Server zuverlässig und geben das gewünschte Ergebnis zurück. In einigen Situationen können Anforderungen jedoch fehlschlagen.
Stellen Sie sich vor, wie jemand mit Ihrer Website über einen Zugangspunkt in einem Zug arbeitet, der mit einer Geschwindigkeit von 200 Stundenkilometern durch das Land fährt. Die Netzwerkverbindung in diesem Szenario kann langsam sein, aber Serveranforderungen erfüllen ihre Aufgabe trotzdem.
Aber was ist, wenn der Zug in den Tunnel fährt? Es besteht eine hohe Wahrscheinlichkeit, dass die Verbindung zum Internet unterbrochen wird und die Webanwendung den Server nicht "erreichen" kann. In diesem Fall muss der Benutzer die Anwendungsseite neu laden, nachdem der Zug den Tunnel verlassen und die Internetverbindung wiederhergestellt wurde.
Das Neuladen der Seite kann sich auf den aktuellen Status der Anwendung auswirken. Dies bedeutet, dass der Benutzer beispielsweise die Daten verlieren kann, die er in das Formular eingegeben hat.
Anstatt sich einfach mit der Tatsache abzustimmen, dass eine bestimmte Anfrage nicht erfolgreich war, ist es besser, sie mehrmals zu wiederholen und dem Benutzer eine entsprechende Benachrichtigung anzuzeigen. Wenn der Benutzer bei diesem Ansatz feststellt, dass die Anwendung versucht, das Problem zu lösen, wird er die Seite höchstwahrscheinlich nicht neu laden.

Das Material, dessen Übersetzung wir heute veröffentlichen, widmet sich der Analyse verschiedener Möglichkeiten, erfolglose Anfragen in Angular-Anwendungen zu wiederholen.
Wiederholen Sie fehlgeschlagene Anforderungen
Lassen Sie uns eine Situation reproduzieren, auf die ein Benutzer, der von einem Zug aus im Internet arbeitet, möglicherweise stößt. Wir erstellen ein Backend, das die Anforderung während der ersten drei Zugriffsversuche falsch verarbeitet und erst ab dem vierten Versuch Daten zurückgibt.
Normalerweise erstellen wir mit Angular einen Service, verbinden den
HttpClient
und verwenden ihn, um Daten vom Backend abzurufen.
import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError} from 'rxjs/operators'; @Injectable() export class GreetingService { private GREET_ENDPOINT = 'http://localhost:3000'; constructor(private httpClient: HttpClient) { } greet(): Observable<string> { return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe( catchError(() => {
Hier gibt es nichts Besonderes. Wir schließen das Angular
HttpClient
Modul an und führen eine einfache GET-Anfrage aus. Wenn die Anforderung einen Fehler zurückgibt, führen wir einen Code aus, um ihn zu verarbeiten, und geben ein leeres
Observable
(beobachtbares Objekt) zurück, um darüber zu informieren, was die Anforderung ausgelöst hat. Dieser Code sagt sozusagen: "Es ist ein Fehler aufgetreten, aber alles ist in Ordnung, ich kann damit umgehen."
Die meisten Anwendungen führen HTTP-Anforderungen auf diese Weise aus. Im obigen Code wird die Anforderung nur einmal ausgeführt. Danach gibt es entweder vom Server empfangene Daten zurück oder ist nicht erfolgreich.
Wie kann ich die Anforderung wiederholen, wenn der Endpunkt
/greet
nicht verfügbar ist oder einen Fehler zurückgibt? Vielleicht gibt es eine passende RxJS-Anweisung? Natürlich existiert es. RxJS hat Operatoren für alles.
Das erste, was in dieser Situation in den Sinn kommt, ist die
retry
. Schauen wir uns die Definition an: „Gibt ein Observable zurück, das das ursprüngliche Observable mit Ausnahme von
error
wiedergibt. Wenn das ursprüngliche Observable einen
error
aufruft, abonniert diese Methode das ursprüngliche Observable erneut, anstatt den Fehler zu verbreiten.
Die maximale Anzahl von Neuabonnements ist auf die
count
beschränkt (dies ist der numerische Parameter, der an die Methode übergeben wird). "
Die
retry
sehr ähnlich zu dem, was wir brauchen. Also lasst es uns in unsere Kette einbetten.
import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError, retry, shareReplay} from 'rxjs/operators'; @Injectable() export class GreetingService { private GREET_ENDPOINT = 'http://localhost:3000'; constructor(private httpClient: HttpClient) { } greet(): Observable<string> { return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe( retry(3), catchError(() => {
Wir haben den
retry
erfolgreich verwendet. Schauen wir uns an, wie sich dies auf das Verhalten der HTTP-Anforderung auswirkt, die in der experimentellen Anwendung ausgeführt wird.
Hier ist eine große GIF-Datei, die den Bildschirm dieser Anwendung und die Registerkarte Netzwerk der Browser-Entwicklertools zeigt. Weitere solche Demonstrationen finden Sie hier.
Unsere Anwendung ist sehr einfach. Es wird nur eine HTTP-Anfrage gestellt, wenn auf die Schaltfläche
PING THE SERVER
geklickt wird.
Wie bereits erwähnt, gibt das Backend einen Fehler zurück, wenn die ersten drei Versuche ausgeführt werden, eine Anforderung an es auszuführen, und wenn eine vierte Anforderung eingeht, gibt es eine normale Antwort zurück.
Auf der Registerkarte "Tool" des Netzwerkentwicklers können Sie sehen, dass die
retry
die ihr zugewiesene Aufgabe löst und die Ausführung der fehlgeschlagenen Anforderung dreimal wiederholt. Der letzte Versuch ist erfolgreich, die Anwendung erhält eine Antwort, eine entsprechende Meldung erscheint auf der Seite.
Das alles ist sehr gut. Jetzt kann die Anwendung fehlgeschlagene Anforderungen wiederholen.
Dieses Beispiel kann jedoch noch verbessert werden. Bitte beachten Sie, dass jetzt wiederholte Anforderungen unmittelbar nach der Ausführung von Anforderungen ausgeführt werden, die nicht erfolgreich sind. Dieses Verhalten des Systems wird in unserer Situation keinen großen Nutzen bringen - wenn der Zug in den Tunnel fährt und die Internetverbindung für eine Weile unterbrochen wird.
Verzögerte Wiederholung fehlgeschlagener Anforderungen
Der Zug, der in den Tunnel gefahren ist, verlässt ihn nicht sofort. Er verbringt einige Zeit dort. Daher müssen wir den Zeitraum "verlängern", in dem wir wiederholte Anforderungen an den Server ausführen. Sie können dies tun, indem Sie Wiederholungen verschieben.
Dazu müssen wir den Prozess der Ausführung wiederholter Anforderungen besser steuern. Wir müssen in der Lage sein, Entscheidungen darüber zu treffen, wann genau Anfragen wiederholt werden sollen. Dies bedeutet, dass die Fähigkeiten des
retry
für uns nicht mehr ausreichen. Daher wenden wir uns erneut der Dokumentation zu RxJS zu.
Die Dokumentation enthält eine Beschreibung der
retryWhen
, die uns zu passen scheint. In der Dokumentation wird Folgendes beschrieben: „Gibt ein Observable zurück, das das ursprüngliche Observable mit Ausnahme des
error
. Wenn das ursprüngliche Observable einen
error
aufruft, löst diese Methode Throwable aus, was den Fehler verursacht hat. Das Observable wird vom
notifier
. Wenn dieses Observable "
complete
oder "
error
aufruft, ruft diese Methode "
complete
oder "
error
im untergeordneten Abonnement auf. Andernfalls wird diese Methode das ursprüngliche Observable erneut abonnieren. "
Ja, die Definition ist nicht einfach. Beschreiben wir dasselbe in einer zugänglicheren Sprache.
Die
retryWhen
akzeptiert einen Rückruf, der eine Observable zurückgibt. Das zurückgegebene Observable entscheidet anhand einiger Regeln, wie sich der Operator
retryWhen
verhält. So
retryWhen
der Operator
retryWhen
:
- Es funktioniert nicht mehr und gibt einen Fehler aus, wenn das zurückgegebene Observable einen Fehler auslöst.
- Es wird beendet, wenn das zurückgegebene Observable den Abschluss meldet.
- In anderen Fällen wiederholt das Observable, wenn es erfolgreich zurückkehrt, die Ausführung des ursprünglichen Observable
Ein Rückruf wird nur aufgerufen, wenn das ursprüngliche Observable zum ersten Mal einen Fehler auslöst.
Jetzt können wir dieses Wissen verwenden, um mithilfe der RxJS-
retryWhen
einen Mechanismus für verzögerte Wiederholungen für eine fehlgeschlagene Anforderung zu erstellen.
retryWhen((errors: Observable<any>) => errors.pipe( delay(delayMs), mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxEntry)) )) )
Wenn die ursprüngliche Observable, bei der es sich um unsere HTTP-Anforderung handelt, einen Fehler zurückgibt, wird die Anweisung
retryWhen
. Im Rückruf haben wir Zugriff auf den Fehler, der den Fehler verursacht hat. Wir verschieben
errors
, reduzieren die Anzahl der Wiederholungsversuche und geben eine neue Observable zurück, die einen Fehler auslöst.
Basierend auf den Regeln der
retryWhen
diese Observable
retryWhen
Anforderung, da sie
retryWhen
. Wenn die Wiederholung mehrmals nicht erfolgreich ist und der Wert der
retries
auf 0 sinkt, beenden wir die Aufgabe mit einem Fehler, der beim Ausführen der Anforderung aufgetreten ist.
Großartig! Anscheinend können wir den obigen Code verwenden und den
retry
in unserer Kette durch ihn ersetzen. Aber hier machen wir etwas langsamer.
Wie
retries
mit den variablen
retries
? Diese Variable enthält den aktuellen Status des fehlgeschlagenen Anforderungswiederholungssystems. Wo wird sie angekündigt? Wann wird der Zustand zurückgesetzt? Der Staat muss innerhalb des Streams verwaltet werden, nicht außerhalb.
▍Erstellen Sie Ihre eigene delayRetry-Anweisung
Wir können das Problem der Zustandsverwaltung lösen und die Lesbarkeit des Codes verbessern, indem wir den obigen Code als separaten RxJS-Operator schreiben.
Es gibt verschiedene Möglichkeiten, eigene RxJS-Operatoren zu erstellen. Welche Methode verwendet werden soll, hängt davon ab, wie der jeweilige Operator strukturiert ist.
Unser Operator basiert auf bestehenden RxJS-Operatoren. Infolgedessen können wir auf einfachste Weise unsere eigenen Operatoren erstellen. In unserem Fall ist der RxJs-Operator nur eine Funktion mit der folgenden Signatur:
const customOperator = (src: Observable<A>) => Observable<B>
Diese Anweisung nimmt das ursprüngliche Observable und gibt ein anderes Observable zurück.
Da unser Operator dem Benutzer erlaubt, anzugeben, wie oft wiederholte Anforderungen ausgeführt werden sollen und wie oft sie ausgeführt werden müssen, müssen wir die obige Funktionsdeklaration in eine Factory-Funktion
delayMs
, die
delayMs
(Verzögerung zwischen
maxRetry
) und
maxRetry
( maximale Anzahl von Wiederholungen).
const customOperator = (delayMs: number, maxRetry: number) => { return (src: Observable<A>) => Observable<B> }
Wenn Sie einen Operator erstellen möchten, der nicht auf vorhandenen Operatoren basiert, müssen Sie auf die Behandlung von Fehlern und Abonnements achten. Darüber hinaus müssen Sie die
Observable
Klasse erweitern und die
Observable
implementieren.
Wenn Sie interessiert sind, schauen Sie
hier .
Schreiben wir also basierend auf den obigen Codefragmenten unseren eigenen RxJs-Operator.
import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) => `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up`; const DEFAULT_MAX_RETRIES = 5; export function delayedRetry(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES) { let retries = maxRetry; return (src: Observable<any>) => src.pipe( retryWhen((errors: Observable<any>) => errors.pipe( delay(delayMs), mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxRetry)) )) ) ); }
Großartig. Jetzt können wir diesen Operator in den Client-Code importieren. Wir werden es verwenden, wenn wir eine HTTP-Anfrage ausführen.
return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe( delayedRetry(1000, 3), catchError(error => { console.log(error);
Wir haben den Operator
delayedRetry
in die Kette
delayedRetry
und die Zahlen 1000 und 3 als Parameter übergeben. Der erste Parameter legt die Verzögerung in Millisekunden zwischen den Versuchen fest, wiederholte Anforderungen zu stellen. Der zweite Parameter bestimmt die maximale Anzahl wiederholter Anforderungen.
Starten Sie die Anwendung neu und
sehen Sie sich an, wie der neue Operator funktioniert.
Nach der Analyse des Verhaltens des Programms mit den Tools des Browser-Entwicklers können wir feststellen, dass sich die Ausführung wiederholter Versuche, die Anforderung auszuführen, um eine Sekunde verzögert. Nach Erhalt der richtigen Antwort auf die Anfrage wird im Anwendungsfenster eine entsprechende Meldung angezeigt.
Exponentielle Anfrage dösen
Lassen Sie uns die Idee der verzögerten Wiederholung fehlgeschlagener Anforderungen entwickeln. Bisher haben wir die Ausführung jeder wiederholten Anforderung immer gleichzeitig verzögert.
Hier sprechen wir darüber, wie die Verzögerung nach jedem Versuch erhöht werden kann. Der erste Versuch, die Anforderung erneut zu versuchen, erfolgt nach einer Sekunde, der zweite nach zwei Sekunden, der dritte nach drei.
Erstellen Sie eine neue Anweisung,
retryWithBackoff
, die dieses Verhalten implementiert.
import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) => `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up.`; const DEFAULT_MAX_RETRIES = 5; const DEFAULT_BACKOFF = 1000; export function retryWithBackoff(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES, backoffMs = DEFAULT_BACKOFF) { let retries = maxRetry; return (src: Observable<any>) => src.pipe( retryWhen((errors: Observable<any>) => errors.pipe( mergeMap(error => { if (retries-- > 0) { const backoffTime = delayMs + (maxRetry - retries) * backoffMs; return of(error).pipe(delay(backoffTime)); } return throwError(getErrorMessage(maxRetry)); } )))); }
Wenn Sie diesen Operator in der Anwendung verwenden und testen, können Sie
sehen, wie sich die Verzögerung bei der Ausführung der wiederholten Anforderung nach jedem neuen Versuch erhöht.
Nach jedem Versuch warten wir eine bestimmte Zeit, wiederholen die Anfrage und verlängern die Wartezeit. Nachdem der Server die richtige Antwort auf die Anfrage zurückgegeben hat, wird hier wie üblich eine Meldung im Anwendungsfenster angezeigt.
Zusammenfassung
Durch das Wiederholen fehlgeschlagener HTTP-Anforderungen werden Anwendungen stabiler. Dies ist besonders wichtig, wenn sehr wichtige Abfragen ausgeführt werden, ohne die Daten, über die die Anwendung nicht normal arbeiten kann. Dies können beispielsweise Konfigurationsdaten sein, die die Adressen der Server enthalten, mit denen die Anwendung interagieren muss.
In den meisten Szenarien reicht die RxJs-Wiederholungsanweisung nicht aus, um ein zuverlässiges Wiederholungssystem für fehlgeschlagene Anforderungen bereitzustellen. Die
retryWhen
gibt dem Entwickler ein höheres Maß an Kontrolle über wiederholte Anforderungen. Hier können Sie das Intervall für wiederholte Anforderungen konfigurieren. Aufgrund der Fähigkeiten dieses Operators ist es möglich, ein verzögertes Wiederholungsschema oder exponentiell verzögerte Wiederholungen zu implementieren.
Bei der Implementierung von Verhaltensmustern, die für die Wiederverwendung in RxJS-Ketten geeignet sind, wird empfohlen, diese als neue Operatoren zu formatieren.
Hier ist das Repository, aus dem der Code in diesem Artikel verwendet wurde.
Liebe Leser! Wie lösen Sie das Problem, dass fehlgeschlagene HTTP-Anforderungen erneut versucht werden?
