El problema original de GitHub sobre “anular la recuperación” fue abierto en 2015. Ahora, si quito el 2015 del 2017 (el año actual), obtengo 2. Esto demuestra un error en matemáticas, porque 2015 fue de hecho "por siempre" hace.
En 2015 empezamos a explorar la anulación de recuperaciones en curso y, después de 780 comentarios en GitHub, se creó un par de inicios falsos y 5 solicitudes de extracción. Por último, tenemos el aterrizaje de recuperación anulable El primero es Firefox 57.
Actualización: No, me equivoqué. Edge 16 ya cuenta con compatibilidad para anular. Felicitaciones a los ¡Equipo de Edge!
Más adelante me adentraré en la historia, pero primero, la API:
El control y la maniobra de la señal
Conoce la AbortController
y la AbortSignal
:
const controller = new AbortController();
const signal = controller.signal;
El controlador solo tiene un método:
controller.abort();
Cuando haces esto, notifica a la señal:
signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});
El estándar del DOM proporciona esta API, y esa es toda la API. Es genéricamente deliberadamente para que otros estándares web y bibliotecas de JavaScript puedan usarla.
Anular indicadores y recuperar
La recuperación puede tardar un AbortSignal
. Por ejemplo, a continuación, se muestra cómo establecer un tiempo de espera de recuperación después de 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);
});
Cuando anulas una recuperación, se anulan la solicitud y la respuesta, por lo que cualquier lectura del cuerpo de la respuesta
(como response.text()
).
Aquí te presentamos una demostración: Al momento de la redacción, el único navegador que es compatible con esto es Firefox 57. Además, prepárate, nadie con experiencia de diseño participó en la creación de la demostración.
Como alternativa, el indicador se puede proporcionar a un objeto de solicitud y, luego, pasarse a recuperar:
const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });
fetch(request);
Esto funciona porque request.signal
es un AbortSignal
.
Reacciona a una recuperación anulada
Cuando anulas una operación asíncrona, la promesa se rechaza con un DOMException
llamado 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);
}
});
A menudo, no querrás mostrar un mensaje de error si el usuario anuló la operación, ya que no es una "error" si hiciste con éxito lo que el usuario solicitó. Para evitar esto, usa una sentencia if, como el una de las anteriores para manejar específicamente los errores de anulación.
Este es un ejemplo que le da al usuario un botón para cargar contenido y un botón para anular. Si el botón de errores, se muestra un error, a menos que sea un error de anulación:
// 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;
});
Aquí te presentamos una demostración: Al momento de la redacción, los únicos navegadores que compatibles son Edge 16 y Firefox 57.
Una señal, muchas recuperaciones
Se puede usar un solo indicador para anular muchas recuperaciones a la 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);
}
En el ejemplo anterior, se usa el mismo indicador para la recuperación inicial y la del capítulo paralelo
recuperaciones de datos. A continuación, te mostramos cómo usar fetchStory
:
const controller = new AbortController();
const signal = controller.signal;
fetchStory({ signal }).then(chapters => {
console.log(chapters);
});
En este caso, si llamas a controller.abort()
, se anularán las recuperaciones en curso.
El futuro
Otros navegadores
Edge hizo un gran trabajo al enviar esto primero, y Firefox está muy activo. Sus ingenieros implementada desde el paquete de pruebas mientras se realizaba la especificación mientras se está escribiendo. Para otros navegadores, sigue estos pasos:
En un service worker
Necesito terminar las especificaciones de los componentes del service worker, pero este es el plan:
Como mencioné antes, cada objeto Request
tiene una propiedad signal
. Dentro de un service worker,
fetchEvent.request.signal
indicará que se anuló si la página ya no está interesada en la respuesta.
Como resultado, un código como este simplemente funciona:
addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
Si la página anula la recuperación, los indicadores de fetchEvent.request.signal
anulan, de modo que la recuperación dentro del
service worker también se anula.
Si recuperas un valor distinto de event.request
, deberás pasar el indicador a tu
recuperaciones 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 })
);
}
});
Sigue las especificaciones para realizar un seguimiento de esto; agregaré vínculos tickets del navegador una vez que esté listo para la implementación.
La historia
Sí... tardó mucho tiempo en combinarse esta API relativamente simple. Esto se debe a los siguientes motivos:
No coincide con la API
Como puedes ver, la conversación sobre GitHub dura bastantes.
Hay mucho matiz en ese hilo (y algo de falta de matices), pero el desacuerdo clave es uno
quería que el método abort
existiera en el objeto que muestra fetch()
, mientras que el otro
quisiera una separación entre obtener la respuesta y afectarla.
Estos requisitos son incompatibles, por lo que un grupo no iba a conseguir lo que quería. Si se trata
tú, ¡perdón! Si eso te hace sentir mejor, yo también estaba en ese grupo. Pero ver que AbortSignal
encaja en
de seguridad de otras APIs hace que parezca la elección correcta. Además, permitir que las promesas encadenadas
volverse anulables se volverían muy complicados, sino imposibles.
Si quisieras mostrar un objeto que proporcione una respuesta, pero que también pueda anularse, podrías crear un wrapper simple:
function abortableFetch(request, opts) {
const controller = new AbortController();
const signal = controller.signal;
return {
abort: () => controller.abort(),
ready: fetch(request, { ...opts, signal })
};
}
Inicios falsos en TC39
Se realizó un esfuerzo para diferenciar una acción cancelada de un error. Esto incluyó una tercera promesa para indicar "cancelada" y una nueva sintaxis para controlar la cancelación tanto en modo síncrono como asíncrono código:
No es un código real; se retiró la propuesta
try { // Start spinner, then: await someAction(); } catch cancel (reason) { // Maybe do nothing? } catch (err) { // Show error message } finally { // Stop spinner }
Lo más común que se debe hacer cuando se cancela una acción es nada. La propuesta anterior separa
la cancelación de los errores, de modo que no tuvieras que controlar
los errores de anulación específicamente. catch cancel
dejó
escuchas sobre acciones canceladas, pero la mayoría de las veces no necesitarás hacerlo.
Esto llegó a la etapa 1 en las TC39, pero no se logró el consenso y se retiró la propuesta.
Nuestra propuesta alternativa, AbortController
, no requería ninguna sintaxis nueva, por lo que no tenía sentido
para especificarlo dentro de TC39. Todo lo que necesitábamos de JavaScript ya estaba ahí, así que definimos el
interfaces de la plataforma web, específicamente el estándar DOM. Una vez que tomamos esa decisión,
el resto se reunieron relativamente rápido.
Gran cambio de especificaciones
XMLHttpRequest
se puede anular durante años, pero la especificación era bastante imprecisa. No estaba claro en
puntos en los que la actividad de red subyacente podría evitarse o finalizar, o qué ocurrió si
hubo una condición de carrera entre la llamada a abort()
y la finalización de la recuperación.
Queríamos hacerlo bien esta vez, pero eso generó un gran cambio de especificaciones que requirió muchos (eso es mi culpa, y un gran agradecimiento a Anne van Kesteren y Domenic Denicola por arrastrarme) y un conjunto de pruebas decente.
¡Pero estamos aquí ahora! Tenemos una nueva primitiva web para anular las acciones asíncronas, y varias recuperaciones pueden controlarse al mismo tiempo. Más adelante, veremos cómo habilitar los cambios de prioridad durante el ciclo de vida de una recuperación y un nivel API para observar el progreso de la recuperación.