Das ursprüngliche GitHub-Problem „Abbruch eines Abrufs“ wurde 2015 eröffnet. Wenn ich jetzt 2015 von 2017 (dem aktuellen Jahr) abziehe, erhalte ich 2. Das zeigt einen Fehler in der Mathematik, denn 2015 ist in der Tat schon „ewig“ her.
2015 haben wir damit begonnen, die Abbruchfunktion für laufende Abrufe zu untersuchen. Nach 780 GitHub-Kommentaren, einigen Fehlstarts und 5 Pull-Anfragen ist die Funktion jetzt endlich in Browsern verfügbar. Firefox 57 war der erste Browser, in dem sie implementiert wurde.
Update:Nein, ich lag falsch. Edge 16 unterstützt zuerst die Abbruchfunktion. Glückwunsch an das Edge-Team!
Ich werde später auf die Geschichte eingehen, aber zuerst zur API:
Der Controller und das Signalmanöver
AbortController
und AbortSignal
:
const controller = new AbortController();
const signal = controller.signal;
Der Controller hat nur eine Methode:
controller.abort();
Dadurch wird das Signal benachrichtigt:
signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});
Diese API wird vom DOM-Standard bereitgestellt und das ist die gesamte API. Es ist bewusst generisch, damit es von anderen Webstandards und JavaScript-Bibliotheken verwendet werden kann.
Abbruchsignale und Abruf
Für „Abrufen“ kann ein AbortSignal
verwendet werden. So legen Sie beispielsweise ein Abrufzeitlimit von 5 Sekunden fest:
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 5000);
fetch(url, { signal }).then(response => {
return response.text();
}).then(text => {
console.log(text);
});
Wenn Sie einen Abruf abbrechen, werden sowohl die Anfrage als auch die Antwort abgebrochen. Das Lesen des Antworttexts (z. B. response.text()
) wird ebenfalls abgebrochen.
Hier ist eine Demo. Derzeit ist nur Firefox 57 dafür geeignet. Und seien Sie gewarnt: Niemand mit Designkenntnissen war an der Erstellung der Demo beteiligt.
Alternativ kann das Signal einem Anfrageobjekt übergeben und später an „fetch“ übergeben werden:
const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });
fetch(request);
Das funktioniert, weil request.signal
ein AbortSignal
ist.
Auf einen abgebrochenen Abruf reagieren
Wenn Sie einen asynchronen Vorgang abbrechen, wird das Versprechen mit einer DOMException
namens AbortError
abgelehnt:
fetch(url, { signal }).then(response => {
return response.text();
}).then(text => {
console.log(text);
}).catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Uh oh, an error!', err);
}
});
Sie sollten nicht oft eine Fehlermeldung anzeigen, wenn der Nutzer den Vorgang abgebrochen hat, da es sich nicht um einen „Fehler“ handelt, wenn Sie die vom Nutzer angeforderte Aktion erfolgreich ausführen. Um dies zu vermeiden, verwenden Sie eine If-Bedingung wie die oben genannte, um Abbruchfehler speziell zu behandeln.
Hier ein Beispiel, in dem der Nutzer eine Schaltfläche zum Laden von Inhalten und eine Schaltfläche zum Abbrechen hat. Wenn beim Abrufen Fehler auftreten, wird ein Fehler angezeigt, es sei denn, es handelt sich um einen Abbruchfehler:
// This will allow us to abort the fetch.
let controller;
// Abort if the user clicks:
abortBtn.addEventListener('click', () => {
if (controller) controller.abort();
});
// Load the content:
loadBtn.addEventListener('click', async () => {
controller = new AbortController();
const signal = controller.signal;
// Prevent another click until this fetch is done
loadBtn.disabled = true;
abortBtn.disabled = false;
try {
// Fetch the content & use the signal for aborting
const response = await fetch(contentUrl, { signal });
// Add the content to the page
output.innerHTML = await response.text();
}
catch (err) {
// Avoid showing an error message if the fetch was aborted
if (err.name !== 'AbortError') {
output.textContent = "Oh no! Fetching failed.";
}
}
// These actions happen no matter how the fetch ends
loadBtn.disabled = false;
abortBtn.disabled = true;
});
Hier ist eine Demo. Derzeit unterstützen nur Edge 16 und Firefox 57 diese Funktion.
Ein Signal, viele Abrufe
Mit einem einzigen Signal können viele Abrufe gleichzeitig abgebrochen werden:
async function fetchStory({ signal } = {}) {
const storyResponse = await fetch('/story.json', { signal });
const data = await storyResponse.json();
const chapterFetches = data.chapterUrls.map(async url => {
const response = await fetch(url, { signal });
return response.text();
});
return Promise.all(chapterFetches);
}
Im obigen Beispiel wird dasselbe Signal für den ersten Abruf und für die parallelen Kapitelabrufe verwendet. So verwenden Sie fetchStory
:
const controller = new AbortController();
const signal = controller.signal;
fetchStory({ signal }).then(chapters => {
console.log(chapters);
});
In diesem Fall werden durch den Aufruf von controller.abort()
alle laufenden Abrufe abgebrochen.
Die Zukunft
Andere Browser
Edge hat gute Arbeit geleistet, diese Funktion zuerst zu veröffentlichen, und Firefox ist ihm dicht auf den Fersen. Die Entwickler haben die Testsuite während der Erstellung der Spezifikation implementiert. Für andere Browser findest du hier die entsprechenden Tickets:
In einem Service Worker
Ich muss die Spezifikation für die Service Worker-Teile fertigstellen, aber hier ist der Plan:
Wie bereits erwähnt, hat jedes Request
-Objekt eine signal
-Property. Innerhalb eines Service Workers wird mit fetchEvent.request.signal
ein Abbruch signalisiert, wenn die Seite nicht mehr an der Antwort interessiert ist.
Daher funktioniert Code wie dieser einfach:
addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
Wenn die Seite den Abruf abbricht, signalisiert fetchEvent.request.signal
den Abbruch, sodass auch der Abruf im Service Worker abgebrochen wird.
Wenn Sie etwas anderes als event.request
abrufen, müssen Sie das Signal an Ihre benutzerdefinierten Abrufe weitergeben.
addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (event.request.method == 'GET' && url.pathname == '/about/') {
// Modify the URL
url.searchParams.set('from-service-worker', 'true');
// Fetch, but pass the signal through
event.respondWith(
fetch(url, { signal: event.request.signal })
);
}
});
Folge der Spezifikation, um den Fortschritt zu verfolgen. Ich füge Links zu Browser-Tickets hinzu, sobald die Implementierung möglich ist.
Die Geschichte
Ja, es hat lange gedauert, bis diese relativ einfache API fertig war. Das hat mehrere Gründe:
Abweichungen bei der API
Wie Sie sehen, ist die GitHub-Diskussion ziemlich lang.
Dieser Thread enthält viele Nuancen (und einige Unklarheiten), aber die Hauptabweichung besteht darin, dass eine Gruppe wollte, dass die abort
-Methode für das von fetch()
zurückgegebene Objekt vorhanden ist, während die andere Gruppe eine Trennung zwischen dem Abrufen der Antwort und dem Beeinflussen der Antwort wollte.
Diese Anforderungen sind nicht kompatibel, sodass eine Gruppe nicht das bekommen konnte, was sie wollte. Tut mir leid, wenn das auf dich zutrifft. Wenn es Ihnen hilft: Ich war auch in dieser Gruppe. Da AbortSignal
jedoch den Anforderungen anderer APIs entspricht, scheint es die richtige Wahl zu sein. Außerdem wäre es sehr kompliziert, wenn verschachtelte Versprechen abgebrochen werden könnten, wenn nicht gar unmöglich.
Wenn Sie ein Objekt zurückgeben möchten, das eine Antwort liefert, aber auch abgebrochen werden kann, können Sie einen einfachen Wrapper erstellen:
function abortableFetch(request, opts) {
const controller = new AbortController();
const signal = controller.signal;
return {
abort: () => controller.abort(),
ready: fetch(request, { ...opts, signal })
};
}
Fehlstarts in TC39
Wir haben versucht, eine abgebrochene Aktion von einem Fehler zu unterscheiden. Dazu gehörten ein dritter Versprechensstatus, der „abgebrochen“ bedeutet, und eine neue Syntax, um die Abbruchbehandlung sowohl in synchronem als auch in asynchronem Code zu ermöglichen:
Kein gültiger Code – Angebot wurde zurückgezogen
try { // Start spinner, then: await someAction(); } catch cancel (reason) { // Maybe do nothing? } catch (err) { // Show error message } finally { // Stop spinner }
In den meisten Fällen müssen Sie nichts weiter tun, wenn eine Aktion abgebrochen wird. Im obigen Vorschlag wurden Abbruch und Fehler getrennt, sodass Sie Abbruchfehler nicht speziell behandeln mussten. catch cancel
informiert Sie über abgebrochene Aktionen, aber in den meisten Fällen ist das nicht erforderlich.
Dieser Vorschlag erreichte Phase 1 in TC39, aber es wurde kein Konsens erzielt und der Vorschlag wurde zurückgezogen.
Für unseren alternativen Vorschlag, AbortController
, war keine neue Syntax erforderlich. Daher war es nicht sinnvoll, ihn in TC39 zu spezifizieren. Alles, was wir von JavaScript benötigten, war bereits vorhanden. Daher haben wir die Schnittstellen innerhalb der Webplattform definiert, insbesondere den DOM-Standard. Nachdem wir diese Entscheidung getroffen hatten, ging der Rest relativ schnell.
Große Spezifikationsänderung
XMLHttpRequest
kann seit Jahren abgebrochen werden, aber die Spezifikation war ziemlich vage. Es war nicht klar, an welchen Stellen die zugrunde liegende Netzwerkaktivität vermieden oder beendet werden konnte oder was passiert, wenn es zu einer Race-Bedingung zwischen dem Aufruf von abort()
und dem Abschluss des Abrufs kam.
Wir wollten es dieses Mal richtig machen, aber das führte zu einer großen Spezifikationsänderung, die viel Überarbeitung erforderte (das ist meine Schuld, und ich möchte mich ganz herzlich bei Anne van Kesteren und Domenic Denicola bedanken, die mich durch diese Phase begleitet haben) und eine ganze Reihe von Tests.
Aber wir sind jetzt da. Wir haben eine neue Web-Primitivfunktion zum Abbrechen von asynchronen Aktionen. Außerdem können mehrere Abrufe gleichzeitig gesteuert werden. Später werden wir uns damit befassen, wie Prioritätsänderungen während des gesamten Abrufs aktiviert werden können, und eine API der höheren Ebene, um den Abruffortschritt zu beobachten.