Przerwane pobieranie

Jake Archibald
Jake Archibald

Pierwotny problem z przerywaniem pobierania na GitHubie: Otwarty w 2015 r. Jeśli odróżnię rok 2015 od roku 2017 (bieżący), uzyskam 2. To pokazuje, ale błąd w matematyce, bo 2015 rok był tak naprawdę „na zawsze” temu.

W 2015 roku zaczęliśmy badać przerywanie trwającego pobierania, a po 780 komentarzach na GitHubie kilka fałszywych startów i 5 żądań pull, w końcu mamy przerwane pobieranie w przeglądarkach, a pierwszą z nich była Firefox 57.

Aktualizacja: nieee, myliłam się. Na urządzeniu Edge 16 najpierw zakończono obsługę przerwania! Gratulujemy! Zespół Edge!

Później omówię historię, ale najpierw przedstawię interfejs API:

Kontroler + manewr sygnału

Poznaj AbortController i AbortSignal:

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

Kontroler ma tylko jedną metodę:

controller.abort();

Gdy to zrobisz, otrzymasz powiadomienie:

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

Ten interfejs API jest udostępniany przez standard DOM, a to cały interfejs API. Jest jest celowo ogólny, więc może być używany przez inne standardy internetowe i biblioteki JavaScriptu.

Przerwij sygnały i pobieraj

Pobieranie może zająć AbortSignal. Aby na przykład ustawić limit czasu pobierania po 5 sek.:

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

Przerwanie pobierania powoduje przerwanie zarówno żądania, jak i odpowiedzi, więc jakikolwiek odczyt treści odpowiedzi (np. response.text()) również zostanie przerwana.

Oto wersja demonstracyjna – w chwili pisania jedyna przeglądarka która obsługuje tę przeglądarkę, Firefox 57. Przygotuj się też – nikt, kto nie miał umiejętności projektowania, podczas tworzenia wersji demonstracyjnej.

Sygnał można też przekazać do obiektu żądania, a później przekazać do pobrania:

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

fetch(request);

Działa, ponieważ request.signal to AbortSignal.

Reakcja na przerwane pobieranie

Gdy przerwiesz operację asynchroniczną, obietnica odrzuci wartość DOMException o nazwie 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);
    }
});

Często nie chcesz wyświetlać komunikatu o błędzie, jeśli użytkownik przerwał operację, ponieważ nie jest to „błąd” i udało Ci się zrobić to, o co prosił użytkownik. Aby tego uniknąć, używaj instrukcji if-takich jak powyżej, aby obsługiwać błędy przerywania.

Oto przykład, który daje użytkownikowi przycisk do wczytywania treści oraz przycisk przerwania. Jeśli pobieranie , pojawia się błąd, chyba że jest to błąd przerwania:

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

Oto wersja demonstracyjna – w chwili pisania witryny jedyne przeglądarki, które obsługują tę funkcję obsługują Edge 16 i Firefox 57.

Jeden sygnał, wiele pobrań

Jeden sygnał może przerwać wiele pobrań jednocześnie:

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

W powyższym przykładzie ten sam sygnał jest używany przy pierwszym pobieraniu i w rozdziale równoległym pobierania. Oto jak możesz użyć usługi fetchStory:

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

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

W tym przypadku wywołanie metody controller.abort() spowoduje przerwanie trwającego pobierania.

Przyszłość

Inne przeglądarki

Edge świetnie sobie radził z dostarczaniem tego produktu jako pierwszy, a Firefox bardzo się cieszę. Ich inżynierowie z pakietu testowego, gdy specyfikacja była w danym momencie. Jeśli używasz innej przeglądarki, zapoznaj się z tymi informacjami:

W mechanizmie Service Worker

Muszę dokończyć specyfikację części mechanizmu obsługi, ale oto plan:

Jak już wspomnieliśmy, każdy obiekt Request ma właściwość signal. Skrypt service worker fetchEvent.request.signal zasygnalizuje przerwanie, jeśli strona nie będzie już zainteresowana daną odpowiedzią. W efekcie taki kod po prostu działa:

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

Jeśli strona przerwie pobieranie, fetchEvent.request.signal sygnalizuje przerwanie pobierania, więc pobieranie w ciągu skrypt service worker także przerywa.

Jeśli pobierasz dane z innych źródeł niż event.request, musisz przekazać sygnał do pobierania niestandardowych.

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

Postępuj zgodnie ze specyfikacją – dodam linki do zgłoszenia przeglądarki.

Historia

Tak... połączenie tego prostego interfejsu API zajęło dużo czasu. Wyjaśnijmy to:

Niezgodność z interfejsem API

Jak widać, dyskusja na GitHubie trwa dość długo. Wątek zawiera wiele niuansów (i pewnych ich brak), ale najważniejszym nieporozumieniem jest jedna chciała, aby metoda abort istniała na obiekcie zwracanym przez funkcję fetch(), a druga oczekiwano rozróżnienia między otrzymaniem odpowiedzi a jej wpływem.

Te wymagania nie są zgodne, więc jedna z grup nie otrzyma tego, czego oczekiwali. Jeśli wy, przepraszam! Jeśli to sprawiło, że poczujesz się lepiej, ja także byłem w tej grupie. AbortSignal, które pasują do że jest to słuszny wybór. Dodatkowo dzięki łańcuchom obietnic aborcja staje się bardzo skomplikowana, jeśli nie jest możliwa.

Jeśli chcesz zwrócić obiekt, który udziela odpowiedzi, ale możesz go przerwać, możesz utworzyć prosty kod:

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

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

Fałsz zaczyna się w TC39

Podjęto starania, aby anulowane działanie było inne niż błąd. Obejmowały one trzecią obietnicę „cancelled” (anulowano), a także nową składnię do obsługi anulowania zarówno w trybie synchronizacji, jak i asynchronicznej. kod:

Nie

To nie jest prawdziwy kod – oferta pakietowa została wycofana

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

Najczęstszą czynnością, którą wykonuje się po anulowaniu działania, jest nic. Powyższa oferta została podzielona na anulowania rezerwacji z powodu błędów, więc nie trzeba było załatwiać ich konkretnie. catch cancel pozwala o anulowanych działaniach, ale w większości przypadków nie musisz tego robić.

Udało się to dotrzeć do etapu 1 w ramach TC39, ale nie udało się osiągnąć konsensusu, więc oferta została wycofana.

Nasza alternatywna propozycja, AbortController, nie wymagała nowej składni, więc nie miała sensu zgodnie z normą TC39. Było tam wszystko, czego potrzebowaliśmy od JavaScriptu, więc zdefiniowaliśmy platformy internetowej, a zwłaszcza standardu DOM. Gdy podejmiemy decyzję, reszta zebrała się stosunkowo szybko.

Duża zmiana w specyfikacjach

Metodę XMLHttpRequest można było przerywać od lat, ale jej specyfikacja była dość niejasna. Nie było jasne, wskazujących, co można by uniknąć, zakończyć lub zakończyć bieżącą aktywność w sieci lub co się stało, między wywołaniem użytkownika abort() a ukończeniem pobierania wystąpił warunek wyścigu.

Tym razem chcieliśmy to zrobić dobrze, ale doprowadziło to do dużej zmiany w specyfikacji, która wymagała wielu działań. (to moja wina, a bardzo dziękuję Anne van Kesteren i Domenic Denicola za przeciągnięcie mnie przez ten tekst) i cały zestaw testów.

Jesteśmy tutaj! Dysponujemy nowym podstawowym elementem sieciowym do przerywania działań asynchronicznych, a wielokrotne pobieranie może i można je kontrolować. Później zajmiemy się zmianami priorytetów w całym cyklu życia pobierania oraz umożliwimy korzystanie z wyższych poziomów. API do obserwowania postępu pobierania.