O problema original do GitHub para "Aborting a fetch" foi aberto em 2015. Agora, se eu tirar 2015 de 2017 (o ano atual), vou ter 2. Isso demonstra um bug na matemática, porque 2015 foi "há muito tempo".
Começamos a estudar o aborto de transferências em andamento em 2015. Depois de 780 comentários no GitHub, algumas tentativas malsucedidas e cinco solicitações de pull, finalmente temos uma página de destino de transferências abortáveis nos navegadores, sendo o primeiro o Firefox 57.
Atualização:não, eu estava errado. O Edge 16 foi lançado com suporte a abortar primeiro. Parabéns à equipe do Edge!
Vou me aprofundar na história mais tarde, mas primeiro, a API:
O controle + manobra de sinal
Conheça os AbortController
e AbortSignal
:
const controller = new AbortController();
const signal = controller.signal;
O controlador tem apenas um método:
controller.abort();
Ao fazer isso, o sinal é notificado:
signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});
Essa API é fornecida pelo padrão DOM, e é a API inteira. Ele é genérico de propósito para que possa ser usado por outros padrões da Web e bibliotecas JavaScript.
Interromper os indicadores e buscar
A busca pode receber um AbortSignal
. Por exemplo, veja como fazer um tempo limite de busca após 5
segundos:
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 você aborta uma busca, a solicitação e a resposta são abortadas. Assim, qualquer leitura do corpo da resposta
(como response.text()
) também é abortada.
Confira uma demonstração: no momento da redação deste artigo, o único navegador que oferece suporte a isso é o Firefox 57. Além disso, prepare-se, ninguém com habilidade de design foi envolvido na criação da demonstração.
Como alternativa, o indicador pode ser fornecido a um objeto de solicitação e depois transmitido para a busca:
const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });
fetch(request);
Isso funciona porque request.signal
é um AbortSignal
.
Como reagir a uma busca abortada
Quando você aborta uma operação assíncrona, a promessa é rejeitada com um DOMException
chamado 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);
}
});
Não é recomendável mostrar uma mensagem de erro se o usuário abortar a operação, porque não é um "erro" se você fizer o que o usuário pediu. Para evitar isso, use uma instrução if, como a acima, para processar erros de interrupção especificamente.
Confira um exemplo que oferece ao usuário um botão para carregar conteúdo e outro para abortar. Se a busca falha, um erro é mostrado, a menos que seja um erro de interrupção:
// 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;
});
Confira uma demonstração: no momento em que este artigo foi escrito, os únicos navegadores compatíveis eram o Edge 16 e o Firefox 57.
Um indicador, várias buscas
Um único indicador pode ser usado para interromper várias transferências de uma só vez:
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);
}
No exemplo acima, o mesmo indicador é usado para a busca inicial e para as buscas paralelas
de capítulos. Confira como usar fetchStory
:
const controller = new AbortController();
const signal = controller.signal;
fetchStory({ signal }).then(chapters => {
console.log(chapters);
});
Nesse caso, chamar controller.abort()
vai interromper as buscas em andamento.
O futuro
Outros navegadores
O Edge fez um ótimo trabalho ao lançar isso primeiro, e o Firefox está no caminho certo. Os engenheiros implementaram a partir do pacote de testes enquanto a especificação estava sendo escrita. Para outros navegadores, siga estes tickets:
Em um worker de serviço
Preciso concluir a especificação das partes do service worker, mas este é o plano:
Como mencionei antes, cada objeto Request
tem uma propriedade signal
. Em um service worker,
fetchEvent.request.signal
vai sinalizar o aborto se a página não estiver mais interessada na resposta.
Como resultado, um código como este funciona:
addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
Se a página interromper a busca, o fetchEvent.request.signal
vai sinalizar a interrupção, e a busca no
service worker também será interrompida.
Se você estiver buscando algo diferente de event.request
, será necessário transmitir o indicador para suas
buscas personalizadas.
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 })
);
}
});
Siga a especificação para acompanhar isso. Vou adicionar links aos tickets do navegador quando estiver tudo pronto para implementação.
O histórico
Sim… levou muito tempo para criar essa API relativamente simples. Veja o motivo:
Conflito de API
Como você pode ver, a discussão do GitHub é bem longa.
Há muitas nuances nessa discussão (e algumas falta de nuances), mas o principal desacordo é que um
grupo queria que o método abort
existisse no objeto retornado por fetch()
, enquanto o outro
queria uma separação entre receber a resposta e afetá-la.
Esses requisitos são incompatíveis, então um grupo não vai conseguir o que quer. Se for o caso, sentimos muito. Se isso te faz se sentir melhor, eu também estava nesse grupo. No entanto, o fato de AbortSignal
atender aos
requisitos de outras APIs faz com que pareça a escolha certa. Além disso, permitir que promessas conectadas
se tornem abortáveis se tornaria muito complicado, se não impossível.
Se você quiser retornar um objeto que forneça uma resposta, mas também possa ser abortado, crie um wrapper simples:
function abortableFetch(request, opts) {
const controller = new AbortController();
const signal = controller.signal;
return {
abort: () => controller.abort(),
ready: fetch(request, { ...opts, signal })
};
}
False começa em TC39
Foi feito um esforço para diferenciar uma ação cancelada de um erro. Isso incluiu um terceiro estado de promessa para indicar "cancelado" e uma nova sintaxe para processar o cancelamento no código de sincronização e assíncrono:
Código não real: a proposta foi retirada
try { // Start spinner, then: await someAction(); } catch cancel (reason) { // Maybe do nothing? } catch (err) { // Show error message } finally { // Stop spinner }
A coisa mais comum a fazer quando uma ação é cancelada é nada. A proposta acima separou
o cancelamento dos erros para que você não precisasse processar erros de interrupção especificamente. O catch cancel
informa sobre ações canceladas, mas, na maioria das vezes, você não precisa fazer isso.
Isso chegou à fase 1 na TC39, mas o consenso não foi alcançado e a proposta foi retirada.
Nossa proposta alternativa, AbortController
, não exigia nenhuma nova sintaxe, então não fazia sentido
especificá-la no TC39. Tudo o que precisávamos do JavaScript já estava lá, então definimos as
interfaces na plataforma da Web, especificamente o padrão DOM. Depois que tomamos essa decisão,
o resto aconteceu relativamente rápido.
Grande mudança de especificação
XMLHttpRequest
pode ser abortado há anos, mas a especificação era bastante vaga. Não estava claro em
quais pontos a atividade de rede subjacente poderia ser evitada ou encerrada ou o que aconteceria se
houvesse uma condição de corrida entre a chamada de abort()
e a conclusão da busca.
Queríamos fazer certo dessa vez, mas isso resultou em uma grande mudança de especificação que exigiu muita revisão (a culpa é minha, e um grande agradecimento a Anne van Kesteren e Domenic Denicola por me ajudar) e um conjunto decente de testes.
Mas já estamos aqui! Temos uma nova primitiva da Web para interromper ações assíncronas, e várias transferências podem ser controladas de uma só vez. Mais adiante, vamos analisar como ativar as mudanças de prioridade ao longo da vida de uma busca e uma API de nível mais alto para observar o progresso da busca.