Recupero interrotto

Jake Archibald
Jake Archibald

Il problema originale di GitHub relativo all'interruzione del recupero è stato rilevato aperto nel 2015. Ora, se tolgo il 2015 dal 2017 (l'anno in corso), ottengo 2. Ciò dimostra un bug in matematica, perché il 2015 è stato in realtà "per sempre" fa.

Nel 2015 abbiamo iniziato a esplorare l'interruzione dei recuperi in corso e, dopo 780 commenti su GitHub, un paio di falsi avvii e 5 richieste di pull, finalmente abbiamo il recupero interrompebile nei browser, Il primo è Firefox 57.

Aggiornamento: Noooope, mi sbagliavo. Edge 16 è atterrato prima con interrompere il supporto! Congratulazioni alle Team Edge!

Approfondiremo la cronologia più avanti. Prima di tutto l'API:

Il controller + la manovra del segnale

Scopri AbortController e AbortSignal:

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

Il controller ha un solo metodo:

controller.abort();

In questo modo, il segnale riceve una notifica:

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

Questa API è fornita dallo standard DOM, ossia l'intera API. È deliberatamente generico, in modo che possa essere utilizzato da altri standard web e librerie JavaScript.

Interrompi gli indicatori e recupera

Il recupero può richiedere un AbortSignal. Ad esempio, ecco come eseguire un timeout di recupero dopo 5 secondi:

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

Quando interrompi un recupero, vengono interrotte sia la richiesta sia la risposta, quindi qualsiasi lettura del corpo della risposta (ad esempio response.text()) viene interrotta.

Ecco una demo: al momento della stesura di questo documento, l'unico browser che supporta questa versione è Firefox 57. Inoltre, preparati: non è stato coinvolto nessuno con alcuna abilità di progettazione durante la creazione della demo.

In alternativa, l'indicatore può essere fornito a un oggetto della richiesta e successivamente passato al recupero:

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

fetch(request);

Questo funziona perché request.signal è un AbortSignal.

Reazione a un recupero interrotto

Quando interrompi un'operazione asincrona, la promessa viene rifiutata con un valore DOMException denominato 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);
    }
});

Spesso, non si tratta di un messaggio di errore da mostrare se l'utente ha interrotto l'operazione, "errore" se esegui correttamente quanto richiesto dall'utente. Per evitare che ciò accada, utilizza un'istruzione if come una precedente per gestire nello specifico gli errori di interruzione.

Ecco un esempio che offre all'utente un pulsante per caricare i contenuti e un pulsante per interrompere. Se il recupero , viene mostrato un errore, a meno che non si tratti di un errore di interruzione:

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

Ecco una demo: al momento della scrittura, gli unici browser che sono supportati Edge 16 e Firefox 57.

Un solo indicatore, molti recuperi

È possibile utilizzare un singolo indicatore per interrompere molti recuperi contemporaneamente:

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

Nell'esempio precedente, viene utilizzato lo stesso indicatore per il recupero iniziale e per il capitolo parallelo i recuperi. Ecco come usare fetchStory:

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

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

In questo caso, la chiamata a controller.abort() interromperà i recuperi in corso.

Il futuro

Altri browser

Edge ha fatto un ottimo lavoro nel primo prodotto e Firefox è in fase di sviluppo. I loro ingegneri implementato dalla suite di test, mentre la specifica era in fase di scrittura. Per altri browser, ecco le richieste di assistenza da seguire:

In un service worker

Devo completare le specifiche per i componenti del Service worker, ma ecco il piano:

Come accennato prima, ogni oggetto Request ha una proprietà signal. All'interno di un service worker, fetchEvent.request.signal segnalerà l'interruzione se la pagina non è più interessata alla risposta. Di conseguenza, il seguente codice funziona correttamente:

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

Se la pagina interrompe il recupero, fetchEvent.request.signal segnala l'interruzione, in modo che il recupero all'interno del interrompe anche il service worker.

Se stai recuperando un elemento diverso da event.request, dovrai passare l'indicatore al tuo recuperi personalizzati.

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

Segui le specifiche per eseguire il monitoraggio; aggiungerò i link a i ticket del browser quando sono pronti per l'implementazione.

La storia

Sì... ci è voluto molto tempo prima che questa API relativamente semplice si combinasse. per i seguenti motivi:

Disaccordo sulle API

Come puoi vedere, la discussione su GitHub è piuttosto lunga. C'è molta sfumatura in quel thread (e una certa mancanza di sfumature), ma il principale disaccordo è uno voleva che il metodo abort esistesse nell'oggetto restituito da fetch(), mentre l'altro voleva una separazione tra l'ottenimento della risposta e l'influenzare la risposta.

Questi requisiti non sono compatibili, quindi un gruppo non avrebbe ottenuto ciò che voleva. Se si tratta di te, scusa! Se ti fa sentire meglio, ero anch'io in quel gruppo. Ma la considerazione di AbortSignal è adatta alla dei requisiti delle altre API la fa sembrare la scelta giusta. Inoltre, consentire alle promesse concatenate abortibili diventerebbe molto complicato, se non impossibile.

Se vuoi restituire un oggetto che fornisce una risposta, ma può anche interrompere l'operazione, puoi creare un wrapper semplice:

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

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

Inizio falso in TC39

È stato effettuato un tentativo di distinguere un'azione annullata da un errore. Ciò includeva una terza promessa per indicare "annullato" e una nuova sintassi per gestire l'annullamento sia in sincronizzazione che in asincrono codice:

Cosa non fare

Non si tratta di codice reale, la proposta è stata ritirata

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

La cosa più comune da fare quando un'azione viene annullata è nulla. Separata dalla proposta precedente l'annullamento in caso di errori, così non dovevi gestire specificamente gli errori di interruzione. catch cancel ha lasciato senti di parlare di azioni annullate, ma la maggior parte delle volte non è necessario.

Si è raggiunto la prima fase del TC39, ma non è stato raggiunto un consenso e la proposta è stata ritirata.

La nostra proposta alternativa, AbortController, non richiedeva una nuova sintassi, quindi non aveva senso per specificarlo nel TC39. Tutto ciò che ci serviva da JavaScript era già lì, quindi abbiamo definito il all'interno della piattaforma web, in particolare lo standard DOM. Una volta presa questa decisione, gli altri si sono riuniti relativamente rapidamente.

Modifica grande delle specifiche

XMLHttpRequest è interrompibile per anni, ma le specifiche erano piuttosto vaghe. Non era chiaro alle ore quali sono i punti in cui l'attività di rete di base potrebbe essere evitata o interrotta, oppure cosa è successo se si è verificata una condizione di gara tra la chiamata di abort() e il completamento del recupero.

Questa volta volevamo fare la cosa giusta, ma questo ha comportato una grande modifica alle specifiche che ha richiesto (è colpa mia e un enorme ringraziamento ad Anne van Kesteren e Domenic Denicola per avermi trascinato con noi) e una discreta serie di test.

Ma ora siamo qui! Abbiamo una nuova primitiva web per interrompere le azioni asincrone e più recuperi possono essere possono essere controllati contemporaneamente. Più avanti, esamineremo l'attivazione di modifiche di priorità durante l'intero ciclo di vita di un recupero e una valutazione API per osservare l'avanzamento del recupero.