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:
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.