Abbrechbarer Abruf

Jake Archibald
Jake Archibald

Das ursprüngliche GitHub-Problem für "Einen Abruf abbrechen" war seit 2015. Wenn ich jetzt 2015 von 2017 (dem aktuellen Jahr) abziehe, bekomme ich 2. Dies zeigt eine Fehler in der Mathematik, denn 2015 war in Wirklichkeit zurück.

2015 begannen wir damit, den Abbruch laufender Abrufe zu untersuchen, und nach 780 GitHub-Kommentaren Nach ein paar Fehlstarts und 5 Pull-Anfragen gibt es nun endlich eine Abbruchoption für den Abruf in Browsern. wobei das erste Firefox 57 ist.

Update:Nein, ich lag falsch. Edge 16 ist zuerst mit Unterstützung zum Abbrechen gelandet. Herzlichen Glückwunsch an Edge-Team!

Wir sehen uns den Verlauf später noch genauer an. Zuerst möchte ich Ihnen jedoch die API vorstellen:

Steuerung und Signalbewegung

Wir stellen vor: AbortController und AbortSignal:

const controller = new AbortController();
const signal = controller.signal;

Der Controller hat nur eine Methode:

controller.abort();

In diesem Fall wird das Signal gesendet:

signal.addEventListener('abort', () => {
    // Logs true:
    console.log(signal.aborted);
});

Diese API wird durch den DOM-Standard bereitgestellt. Das ist die gesamte API. Es ist bewusst allgemein sein, damit sie auch von anderen Webstandards und JavaScript-Bibliotheken verwendet werden können.

Signale abbrechen und Daten abrufen

Der Abruf kann AbortSignal dauern. Hier sehen Sie zum Beispiel, wie Sie ein Zeitlimit für den Abruf nach 5 Tagen festlegen, Sekunden:

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, sodass jedes Lesen des Antworttexts (z. B. response.text()) wird ebenfalls abgebrochen.

Hier finden Sie eine Demo: Zum Zeitpunkt der Entstehung dieses Artikels war der einzige Browser, unterstützt Firefox 57. Machen Sie sich bereit, es war niemand mit Designkenntnissen beteiligt. bei der Erstellung der Demo.

Alternativ kann das Signal an ein Anfrageobjekt übergeben und später zum Abrufen weitergeleitet 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 abgebrochenen Abruf reagieren

Wenn du einen asynchronen Vorgang abbrichst, wird das Promise mit einem 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);
    }
});

Es kommt selten vor, dass eine Fehlermeldung angezeigt wird, wenn der Nutzer den Vorgang abgebrochen hat, da es sich dabei nicht um "Fehler" wenn Sie dem Wunsch der Nutzenden erfolgreich folgen. Um dies zu vermeiden, verwenden Sie eine if-Anweisung wie die eines speziell für Abbruchfehler.

Das folgende Beispiel zeigt dem Nutzer eine Schaltfläche zum Laden von Inhalten und eine Schaltfläche zum Abbrechen. Wenn der Abruf Fehler 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. Zum Zeitpunkt der Entstehung dieses Artikels waren die einzigen Browser, die unterstützen Edge 16 und Firefox 57.

Ein Signal, viele Abrufe

Mit einem einzelnen Signal können mehrere 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 das parallele Kapitel verwendet. Abrufe. So würden Sie fetchStory verwenden:

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 diese erste Version großartig bereitgestellt und Firefox ist auf der Suche. Ihre Entwickler in der Testsuite implementiert, während die Spezifikation geschrieben wird. Für andere Browser gibt es diese Tickets:

In einem Service Worker

Ich muss noch die Spezifikation für Service Worker-Teile fertigstellen, aber hier ist der Plan:

Wie bereits erwähnt, hat jedes Request-Objekt ein signal-Attribut. Innerhalb eines Service Workers fetchEvent.request.signal signalisiert „Abbrechen“, wenn die Seite nicht mehr an der Antwort interessiert ist. Daher funktioniert Code wie dieser einfach:

addEventListener('fetch', event => {
    event.respondWith(fetch(event.request));
});

Wird der Abruf durch die Seite abgebrochen, wird der Vorgang durch fetchEvent.request.signal-Signale abgebrochen, sodass der Abruf innerhalb der bricht auch der Service Worker ab.

Wenn Sie etwas anderes als event.request abrufen, müssen Sie das Signal an Ihren benutzerdefinierte Abrufe.

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

Folgen Sie dazu der Spezifikation, um dies nachzuverfolgen. Ich füge Links sobald die Implementierung bereit ist.

Die Geschichte

Ja... es hat lange gedauert, bis diese relativ einfache API entwickelt wurde. Das hat mehrere Gründe:

API-Widerspruch

Wie Sie sehen, ist die GitHub-Diskussion ziemlich lang. In diesem Thread gibt es viele Nuancen und ein wenig Nuancen, aber der Hauptunterschied ist eine Die Gruppe wollte, dass die Methode abort in dem von fetch() zurückgegebenen Objekt vorhanden sein soll, während die andere Methode eine Trennung zwischen dem Erhalt der Antwort und der Beeinflussung der Antwort wünschten.

Diese Anforderungen sind nicht kompatibel, sodass eine Gruppe nicht das Gewünschte finden konnte. Wenn das Tut mir leid! Wenn du dich dadurch besser fühlst, war ich auch in dieser Gruppe. Aber AbortSignal als Anforderungen anderer APIs scheint die richtige Wahl zu sein. Außerdem können verkettete Versprechen abschreckbar zu werden, sehr kompliziert oder sogar unmöglich werden.

Wenn Sie ein Objekt zurückgeben wollten, das eine Antwort liefert, aber auch abbrechen kann, können Sie ein Einfacher Wrapper:

function abortableFetch(request, opts) {
    const controller = new AbortController();
    const signal = controller.signal;

    return {
    abort: () => controller.abort(),
    ready: fetch(request, { ...opts, signal })
    };
}

„False“ beginnt in TC39

Es wurde versucht, eine abgebrochene Aktion von einem Fehler zu unterscheiden. Dazu gehörte ein drittes Versprechen, Status, der für „abgebrochen“ steht, und eine neue Syntax für die Stornierung sowohl bei der Synchronisierung als auch im asynchronen Modus Code:

Don'ts

Kein echter 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
    }

Die häufigste Maßnahme nach dem Abbruch einer Aktion ist nichts. Das obige Angebot wurde getrennt Abbruchfehler beheben, sodass Sie sich nicht speziell mit Abbruchfehlern befassen mussten. catch cancel mal hören Sie von abgebrochenen Aktionen. In den meisten Fällen ist dies jedoch nicht erforderlich.

Dies erreichte im TC39 Phase 1, aber kein Konsens erreichte, und der Vorschlag wurde zurückgezogen.

Für unseren alternativen Vorschlag AbortController war keine neue Syntax erforderlich und er ergab daher keinen Sinn. um sie in TC39 zu spezifizieren. Alles, was wir von JavaScript benötigten, war bereits vorhanden, also haben wir die Schnittstellen innerhalb der Webplattform, insbesondere DOM-Standard Nachdem wir diese Entscheidung getroffen hatten, der Rest kam relativ schnell zusammen.

Große Änderung der Spezifikation

XMLHttpRequest kann seit Jahren abgetrieben werden, aber die Angaben waren ziemlich vage. Es war nicht klar, zu welchen Punkten die zugrunde liegende Netzwerkaktivität vermieden oder beendet werden könnte, oder was passiert ist, wenn Es gab eine Race-Bedingung zwischen dem Aufruf von abort() und dem Abschluss des Abrufs.

Dieses Mal wollten wir, dass alles richtig ist, aber das führte zu einer großen Spezifikationsänderung, die viel Zeit erforderte. (das ist meine Schuld. Vielen Dank an Anne van Kesteren und Domenic Denicola dafür, dass er mich durchzieht) und eine Reihe von Tests.

Aber wir sind jetzt hier! Es gibt ein neues Web-Primitive zum Abbrechen von asynchronen Aktionen. gleichzeitig gesteuert werden. Später werden wir uns ansehen, wie Prioritätsänderungen während der gesamten Dauer eines Abrufs aktiviert werden können. Außerdem wird eine übergeordnete Die API zur Beobachtung des Abruffortschritts.