Récupération abortable

Jake Archibald
Jake Archibald

Le problème d'origine GitHub lié à l'annulation d'une récupération était depuis 2015. Maintenant, si je prends 2015 de 2017 (l'année en cours), j'obtiens 2. Cela démontre une en maths, car 2015 était en fait "pour toujours" auparavant.

En 2015, nous avons commencé à envisager l'abandon des récupérations en cours. Après 780 commentaires GitHub, et cinq requêtes d'extraction, nous disposons enfin d'une destination de récupération abandon possible dans les navigateurs. le premier étant Firefox 57.

Information:Noooope, je me trompais. Edge 16 s'est d'abord atterri avec la prise en charge de l'abandon en premier ! Félicitations au Équipe Edge !

Je reviendrai sur l'historique plus tard, mais tout d'abord, l'API:

Manœuvre manette + signal

Découvrez AbortController et AbortSignal:

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

La manette n'a qu'une seule méthode:

controller.abort();

Dans ce cas, le signal reçoit une notification:

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

Cette API est fournie par la norme DOM. Il s'agit de l'API complète. Il est délibérément générique, afin qu'il puisse être utilisé par d'autres normes Web et bibliothèques JavaScript.

Annuler les signaux et récupérer

La récupération peut prendre un AbortSignal. Par exemple, voici comment définir un délai avant expiration de la récupération après 5 secondes:

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

Lorsque vous annulez une extraction, la requête et la réponse sont annulées. Par conséquent, toute lecture du corps de la réponse (response.text(), par exemple) est également annulée.

Voici une démonstration : au moment de la rédaction de ce document, le seul navigateur utilisé qui le prend en charge est Firefox 57. Accrochez-vous : personne ayant des compétences en conception n'a pas été impliqué. lors de la création de la démonstration.

Vous pouvez également transmettre le signal à un objet de requête, puis le transmettre à l'extraction:

const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });

fetch(request);

Cela fonctionne, car request.signal est de type AbortSignal.

Réagir à une récupération annulée

Lorsque vous annulez une opération asynchrone, la promesse est rejetée avec un DOMException nommé AbortError:

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

Souvent, il n'est pas souhaitable d'afficher un message d'erreur si l'utilisateur a annulé l'opération, car il ne s'agit pas "erreur" si vous faites avec succès ce que l’utilisateur a demandé. Pour éviter cela, utilisez une instruction "if" comme l'instruction ci-dessus pour gérer spécifiquement les erreurs d'abandon.

Voici un exemple qui donne à l'utilisateur un bouton pour charger du contenu et un bouton pour annuler. Si l'extraction erreurs, une erreur s'affiche, sauf qu'il s'agit d'une erreur d'annulation:

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

Voici une démonstration : au moment de la rédaction de ce document, les seuls navigateurs pris en charge sont Edge 16 et Firefox 57.

Un signal, plusieurs récupérations

Un seul signal peut être utilisé pour annuler plusieurs extractions à la fois:

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

Dans l'exemple ci-dessus, le même signal est utilisé pour la récupération initiale et pour le chapitre parallèle ou de récupérations. Voici comment utiliser fetchStory:

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

fetchStory({ signal }).then(chapters => {
    console.log(chapters);
});

Dans ce cas, l'appel de controller.abort() annule les extractions en cours.

L'avenir

Autres navigateurs

Edge a fait un excellent travail pour lancer ce premier, et Firefox est sur la bonne voie. Ses ingénieurs implémentée à partir de la suite de tests alors que la spécification en cours d'écriture. Pour les autres navigateurs, procédez comme suit:

Dans un service worker

Je dois terminer les spécifications des pièces du service worker, mais voici le processus:

Comme je l'ai déjà mentionné, chaque objet Request possède une propriété signal. Dans un service worker, fetchEvent.request.signal signale un abandon si la page n'est plus intéressée par la réponse. Par conséquent, un code comme celui-ci fonctionne:

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

Si la page annule l'exploration, fetchEvent.request.signal indique l'abandon. L'exploration dans le et l'abandon du service worker.

Si vous récupérez autre chose que event.request, vous devez transmettre le signal à votre récupération(s) personnalisée(s).

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

Suivez les spécifications pour en effectuer le suivi. J'ajouterai des liens vers des tickets de navigateur une fois qu'il est prêt à être implémenté.

L'histoire

Oui. Il a fallu beaucoup de temps pour créer cette API relativement simple. Voici pourquoi :

Non-respect des règles concernant l'API

Comme vous pouvez le voir, la discussion GitHub est assez longue. Ce fil de discussion comporte de nombreuses nuances (et quelques nuances), mais le principal désaccord est voulait que la méthode abort existe sur l'objet renvoyé par fetch(), tandis que les autres voulait une séparation entre l'obtention de la réponse et l'incidence sur la réponse.

Ces exigences étant incompatibles, un groupe n’obtiendrait pas ce qu’il voulait. Si c'est Désolé ! Si ça te rassure, j'étais aussi dans ce groupe. Toutefois, voir AbortSignal correspond des exigences d'autres API font que cela semble être le bon choix. De plus, autoriser les promesses enchaînées à devenir avorables deviendrait très compliqué, voire impossible.

Si vous souhaitez renvoyer un objet qui fournit une réponse, mais que vous pouvez également annuler l'opération, vous pouvez créer un wrapper simple:

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

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

Les faux débuts dans TC39

Nous avons fait en sorte qu'une action annulée soit distincte d'une erreur. Cela comprenait une troisième promesse pour indiquer "cancelled" et une nouvelle syntaxe pour gérer l'annulation à la fois en mode synchrone et asynchrone. code:

À éviter

Code non réel : la proposition a été retirée

    try {
      // Start spinner, then:
      await someAction();
    }
    catch cancel (reason) {
      // Maybe do nothing?
    }
    catch (err) {
      // Show error message
    }
    finally {
      // Stop spinner
    }

La chose la plus courante lorsqu'une action est annulée, c'est n'importe quoi. La proposition ci-dessus a séparé de l'annulation des erreurs afin que vous n'ayez pas à gérer spécifiquement les erreurs d'abandon. catch cancel permet vous entendez parler d'actions annulées, mais la plupart du temps, vous n'en avez pas besoin.

Cela a atteint l'étape 1 de TC39, mais le consensus n'a pas été obtenu et la proposition a été retirée.

Notre proposition alternative, AbortController, ne nécessitait pas de nouvelle syntaxe, elle n'avait donc aucun sens pour le spécifier dans TC39. Tout ce dont nous avions besoin dans JavaScript était déjà présent. Nous avons donc défini au sein de la plate-forme Web, en particulier la norme DOM. Une fois cette décision prise, le reste est venu assez rapidement.

Modification importante des spécifications

Abandon de XMLHttpRequest depuis des années, les spécifications étaient assez vagues. Ce n’était pas clair à qui indique que l'activité réseau sous-jacente pourrait être évitée, arrêtée, ou que s'est-il passé si une condition de concurrence s'est produite entre l'appel de abort() et la fin de la récupération.

Nous voulions faire les bons choix cette fois-ci, mais cela s'est traduit par d'importants changements de spécifications qui nécessitaient (c'est ma faute. Merci beaucoup à Anne van Kesteren et Domenic Denicola de m'avoir entraîné dans cette vidéo) et de bonne série de tests.

Mais on y est maintenant ! Nous disposons d'une nouvelle primitive Web pour l'annulation des actions asynchrones. Plusieurs extractions peuvent être contrôlé en même temps ! Plus loin, nous verrons comment activer les modifications prioritaires tout au long du cycle d'exploration, ainsi qu'un niveau de priorité plus élevé. API pour observer la progression de la récupération.