Wenn Sie jemals die JavaScript- fetch
API zum Verbessern einer Formularübermittlung verwendet haben, besteht eine gute Chance, dass Sie versehentlich einen Fehler bei doppelten Anforderungen/Race-Conditions eingeführt haben. Heute werde ich Sie durch das Problem und meine Empfehlungen führen, um es zu vermeiden.
(Video am Ende, wenn Sie das bevorzugen)
Betrachten wir etwas ganz Grundlegendes
<form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form>
Wenn wir auf die Schaltfläche „Senden“ klicken, führt der Browser eine Aktualisierung der gesamten Seite durch.
Beachten Sie, wie der Browser neu geladen wird, nachdem auf die Schaltfläche „Senden“ geklickt wurde.
Die Seitenaktualisierung ist nicht immer das Erlebnis, das wir unseren Benutzern bieten möchten, daher ist die Verwendung eine gängige Alternativefetch
API zu senden.
Ein vereinfachter Ansatz könnte wie das folgende Beispiel aussehen.
Nachdem die Seite (oder Komponente) bereitgestellt wurde, greifen wir auf den DOM-Knoten des Formulars zu und fügen einen Ereignis-Listener hinzu, der mithilfe des Formulars eine fetch
erstelltpreventDefault()
des Ereignisses auf.
const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); function handleSubmit(event) { const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }); event.preventDefault(); }
Bevor jetzt irgendwelche JavaScript-Hotshots anfangen, über GET vs. POST und den Anforderungstext zu twitternfetch
bewusst einfach, da sie nicht im Vordergrund steht.
Das Hauptproblem hierbei ist event.preventDefault()
. Diese Methode verhindert, dass der Browser das Standardverhalten ausführt, nämlich das Laden der neuen Seite und das Absenden des Formulars.
Wenn wir nun auf den Bildschirm schauen und auf „Senden“ klicken, sehen wir, dass die Seite nicht neu geladen wird, aber wir sehen die HTTP-Anfrage in unserem Netzwerk-Tab.
Beachten Sie, dass der Browser nicht die gesamte Seite neu lädt.
Leider haben wir durch die Verwendung von JavaScript zur Verhinderung des Standardverhaltens tatsächlich einen Fehler eingeführt, den das Standardverhalten des Browsers nicht aufweist.
Wenn wir plain verwenden
Wenn wir das mit dem JavaScript-Beispiel vergleichen, werden wir sehen, dass alle Anfragen gesendet wurden und alle vollständig sind, ohne dass eine Anfrage abgebrochen wurde.
Dies kann ein Problem darstellen, da zwar jede Anfrage unterschiedlich lange dauern kann, sie jedoch möglicherweise in einer anderen Reihenfolge als der Reihenfolge, in der sie initiiert wurde, gelöst wird. Das heißt, wenn wir der Lösung dieser Anfragen Funktionen hinzufügen, kann es zu unerwartetem Verhalten kommen.
Als Beispiel könnten wir eine Variable erstellen, die für jede Anfrage erhöht wird („ totalRequestCount
“). Jedes Mal, wenn wir die Funktion handleSubmit
ausführen, können wir die Gesamtzahl erhöhen und die aktuelle Zahl erfassen, um die aktuelle Anfrage („ thisRequestNumber
“) zu verfolgen.
Wenn eine fetch
gelöst wird, können wir die entsprechende Nummer in der Konsole protokollieren.
const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); let totalRequestCount = 0 function handleSubmit(event) { totalRequestCount += 1 const thisRequestNumber = totalRequestCount const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }).then(() => { console.log(thisRequestNumber) }) event.preventDefault(); }
Wenn wir nun mehrmals auf die Schaltfläche „Senden“ drücken, werden auf der Konsole möglicherweise verschiedene Zahlen in falscher Reihenfolge angezeigt: 2, 3, 1, 4, 5. Das hängt von der Netzwerkgeschwindigkeit ab, aber ich denke, wir sind uns alle einig dass das nicht ideal ist.
Stellen Sie sich ein Szenario vor, in dem ein Benutzer mehrere fetch
kurz hintereinander auslöst und Ihre Anwendung nach Abschluss die Seite mit ihren Änderungen aktualisiert. Der Benutzer könnte letztendlich ungenaue Informationen sehen, weil Anfragen nicht in der richtigen Reihenfolge bearbeitet werden.
In der Nicht-JavaScript-Welt stellt dies kein Problem dar, da der Browser alle vorherigen Anfragen abbricht und die Seite lädt, nachdem die letzte Anfrage abgeschlossen ist, wobei die aktuellste Version geladen wird. Aber Seitenaktualisierungen sind nicht so sexy.
Die gute Nachricht für JavaScript-Liebhaber ist, dass wir beides haben können
Wir müssen nur noch ein bisschen mehr Laufarbeit leisten.
Wenn Sie sich die Dokumentation fetch
API ansehen, werden Sie feststellen, dass es möglich ist, einen Abruf mithilfe eines AbortController
und der signal
der fetch
abzubrechen. Es sieht ungefähr so aus:
const controller = new AbortController(); fetch(url, { signal: controller.signal });
Durch die Bereitstellung des AbortContoller
-Signals für die fetch
können wir die Anforderung jederzeit abbrechen, wenn die abort
Methode des AbortContoller
ausgelöst wird.
Ein klareres Beispiel finden Sie in der JavaScript-Konsole. Versuchen Sie, einen AbortController
zu erstellen, die fetch
zu initiieren und dann sofort die abort
Methode auszuführen.
const controller = new AbortController(); fetch('', { signal: controller.signal }); controller.abort()
Sie sollten sofort eine Ausnahme sehen, die auf der Konsole ausgegeben wird. In Chromium-Browsern sollte es lauten: „Uncaught (in versprochen) DOMException: Der Benutzer hat eine Anfrage abgebrochen.“ Und wenn Sie die Registerkarte „Netzwerk“ erkunden, sollten Sie eine fehlgeschlagene Anfrage mit dem Statustext „(abgebrochen)“ sehen.
Vor diesem Hintergrund können wir einen AbortController
zum Submit-Handler unseres Formulars hinzufügen. Die Logik wird wie folgt sein:
AbortController
für alle vorherigen Anforderungen. Wenn einer vorhanden ist, brechen Sie ihn ab.
AbortController
für die aktuelle Anfrage, der bei nachfolgenden Anfragen abgebrochen werden kann.
AbortController
.
Es gibt mehrere Möglichkeiten, dies zu tun, aber ich verwende eine WeakMap
, um Beziehungen zwischen jedem übermittelten <form>
DOM-Knoten und seinem jeweiligen AbortController
zu speichern. Wenn ein Formular übermittelt wird, können wir die WeakMap
überprüfen und entsprechend aktualisieren.
const pendingForms = new WeakMap(); function handleSubmit(event) { const form = event.currentTarget; const previousController = pendingForms.get(form); if (previousController) { previousController.abort(); } const controller = new AbortController(); pendingForms.set(form, controller); fetch(form.action, { method: form.method, body: new FormData(form), signal: controller.signal, }).then(() => { pendingForms.delete(form); }); event.preventDefault(); } const forms = document.querySelectorAll('form'); for (const form of forms) { form.addEventListener('submit', handleSubmit); }
Der Schlüssel liegt darin, einen Abbruch-Controller mit dem entsprechenden Formular verknüpfen zu können. Die Verwendung des DOM-Knotens des Formulars als WeakMap
-Schlüssel ist eine bequeme Möglichkeit, dies zu tun.
Damit können wir das Signal des AbortController
zur fetch
hinzufügen, alle vorherigen Controller abbrechen, neue hinzufügen und sie nach Abschluss löschen.
Hoffentlich macht das alles Sinn.
Wenn wir nun mehrmals auf die Schaltfläche „Senden“ dieses Formulars drücken, können wir sehen, dass alle API-Anfragen außer der letzten abgebrochen werden.
Das bedeutet, dass sich jede Funktion, die auf diese HTTP-Antwort antwortet, eher so verhält, wie Sie es erwarten würden.
Wenn wir nun dieselbe Zähl- und Protokollierungslogik wie oben verwenden, können wir die Senden-Schaltfläche sieben Mal drücken und würden sechs Ausnahmen (aufgrund des AbortController
) und ein Protokoll von „7“ in der Konsole sehen.
Wenn wir die Anfrage erneut einreichen und genügend Zeit für die Lösung einplanen, wird in der Konsole „8“ angezeigt. Und wenn wir mehrmals auf die Schaltfläche „Senden“ drücken, werden die Ausnahmen und die endgültige Anzahl der Anfragen weiterhin in der richtigen Reihenfolge angezeigt.
Wenn Sie weitere Logik hinzufügen möchten, um zu vermeiden, dass DOMExceptions in der Konsole angezeigt werden, wenn eine Anfrage abgebrochen wird, können Sie nach Ihrer fetch
einen .catch()
Block hinzufügen und prüfen, ob der Name des Fehlers mit „ AbortError
“ übereinstimmt:
fetch(url, { signal: controller.signal, }).catch((error) => { // If the request was aborted, do nothing if (error.name === 'AbortError') return; // Otherwise, handle the error here or throw it back to the console throw error });
Dieser ganze Beitrag konzentrierte sich auf JavaScript-erweiterte Formulare, aber es ist wahrscheinlich eine gute Idee, jedes Mal, wenn Sie eine fetch
erstellen, einen AbortController
einzuschließen. Es ist wirklich schade, dass es nicht bereits in die API integriert ist. Aber hoffentlich zeigt Ihnen dies eine gute Methode, es einzubinden.
Erwähnenswert ist auch, dass dieser Ansatz den Benutzer nicht davon abhält, mehrmals auf die Schaltfläche „Senden“ zu spammen. Die Schaltfläche ist weiterhin anklickbar und die Anfrage wird weiterhin ausgelöst. Dies bietet lediglich eine konsistentere Möglichkeit, mit Antworten umzugehen.
Wenn ein Benutzer eine Absenden-Schaltfläche als Spam versendet , würden diese Anfragen leider immer noch an Ihr Backend weitergeleitet und könnten eine Menge unnötiger Ressourcen verbrauchen.
Einige naive Lösungen bestehen möglicherweise darin, die Schaltfläche „Senden“ mithilfe von a zu deaktivieren
Sie bekämpfen Missbrauch nicht über skriptgesteuerte Anfragen.
Um Missbrauch durch zu viele Anfragen an Ihren Server zu verhindern, möchten Sie wahrscheinlich welche einrichten
Erwähnenswert ist auch, dass die Ratenbegrenzung das ursprüngliche Problem doppelter Anfragen, Race Conditions und inkonsistenter UI-Updates nicht löst. Idealerweise sollten wir beide verwenden, um beide Enden abzudecken.
Jedenfalls ist das alles, was ich für heute habe. Wenn Sie sich ein Video ansehen möchten, das dasselbe Thema behandelt, schauen Sie sich dieses an.
Vielen Dank fürs Lesen. Wenn Ihnen dieser Artikel gefallen hat, bitte
Ursprünglich veröffentlicht am