Para descargar archivos de internet mediante Javascript, ya sea de nuestro propio backend o desde el API de un tercero, podemos usar tanto el viejo XMLHttpRequest
, como el más reciente API Fetch
. En ambos casos es posible tener una actualización «en vivo» de cuantos bytes se han descargado, de forma que podamos informarnos del avance de la descarga.
Veamos como sería el código usando el Fetch API
:
const respuesta = await fetch('https://picsum.photos/800/800');
if (respuesta.ok){
throw new Error(respuesta.textContent);
}
const lector = respuesta.body.getReader();
const totalBytes = respuesta.headers.get('content-length');
//Aquí pondremos los bytes de la respuesta
let controlador;
const flujoLectura = new ReadableStream({
start(controller){
controlador = controller;
}
});
let avanceBytes = 0;
while (true) {
const { done, value } = await lector.read()
if (done) {//al terminar de leer: salir del bucle
controlador.close();
break
}
controlador.enqueue(value)
avanceBytes += value.byteLength;
calbackProgreso.apply({}, [avanceBytes, totalBytes]);
}
//Al terminar: generar un Blob con el archivo obtenido
const flujoResp = new Response(flujoLectura);
const archivo = await flujoResp.blob();
//Mostramos la vista previa en un control img
imagen.src = URL.createObjectURL(archivo );
Analicemos por partes lo qué hace este fragmento de código. Veamos primero al función Fetch:
- Usaremos picsum para generar una imágen aleatoria de 800×800 px. Puedes usar otro servicio, o tus propias imágenes para probar.
- La función
Fetch
devuelve una promesa, así que usamosawait
para esperar a que se resuelva y obtener la respuesta. Importante: La promesa se resuelve en cuanto el servidor responde con los encabezados (Headers), pero en ese punto no necesariamente ha enviado el cuerpo, que en nuestro caso es un archivo. - Si la respuesta tiene estatus de error (fuera del rango 200-299), la propiedad
Response.ok
seráfalse
, y en ese caso lanzamos una excepción.
Ahora estamos listos para comenzar a leer usando la variable lector
:
- El cuerpo de la respuesta se puede leer de diferentes formas, pero como queremos hacerlo de forma asíncrona usaremos
body.getReader
, que devuelve unReadableStream<Uint8Array>
. - Leemos la longitud total
totalBytes
enviada por el servidor, que se encuentra en el encabezadocontent-length
de la respuesta. - Creamos otro
ReadableStream
, al que llamaremosflujoLectura
: aquí colocaremos los bytes a medida que leamos. Al final, lo usaremos para crear unBlob
con su contenido, y lo mostraremos en un elementoimg
para tener una vista previa. - Ahora, llamamos la función
lector.read()
en un bucle, mientras haya algo qué leer: el valor leído lo encolaremos en el controlador que está dentro deflujoLectura
. La variablevalue
también contiene la longitud del bloque de bytes que leemos en cada bloque, así que la guardaremos en el acumuladoravanceBytes
. - Dentro del mismo bucle de lectura, llamaremos a la función
mostrarProgreso
, que describiremos a continuación.
La siguiente función simplemente actualizará una barra de progreso en pantalla a medida que leamos las partes del archivo que se vayan descargando:
const mostrarProgreso = (avance, totalBytes) =>{
const porcentaje = Math.round(avance / totalBytes * 100);
barra.value = porcentaje;
barra.textContent = porcentaje + '%';
etiqueta.textContent = porcentaje + '% (' + (avance/1024).toFixed(2) + ' KB)';
};
Esta función recibe el acumulado de bytes que hemos recibido hasta ahora, y el total de bytes, según lo indicado en la cabecera. Con esto, calcula el porcentaje de avance, tanto para el valor de la barra como para la etiqueta que pusimos al lado. Como información adicional, también colocamos la cantidad de bytes descargardos hasta el momento.
La interface: HTML
Ahora veamos el HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Descarga con Progreso</title>
<link href="style.css" rel="stylesheet"></link>
</head>
<body>
<main>
<div>
<button id="btnCargar">Cargar Imagen</button>
</div>
<div id="progreso" >
<progress id="progreso__barra" max="100">0%</progress>
<label id="progreso__etiqueta" for="progreso__barra">conectando...</label>
</div>
<img id="imagen" src="">
</main>
</div>
<script> <!-- Javascript: aquí o en un archivo externo -->
</script>
</body>
</html>
La interface es bastante simple: un botón para iniciar la descarga de la imagen, Una barra de progreso, con su respectiva etiqueta para mostrar el % y total de bytes durante la descarga, y un control img para mostrar la vista previa.
El CSS no es relevante para este ejemplo, pero si quieres puedes ver el código completo en este repositorio: https://github.com/roimergarcia/20230822—descarga-con-progreso
Cuando hacemos clic en el botón cargar, se inicia la petición de la imagen. Hasta el momento que obtengamos las cabeceras HTTP, el usuario verá el mensaje «cargando» y la barra e progreso tendrá un valor indeterminado (en Chrome, se verá una barra azul moviendose de lado a lado).
Cuando comienza la descrga del contenido de la imagen, podremos ver el porcentaje de avance de la descarga, y la cantidad de bytes descargados hasta el momento:
Finalmene, cuando la descarga termina, la barra muestra un 100% de descarga, y se muestra una vista previa de la imagen final:
Multiples Archivos
En el ejemplo anterior hicimos una petición básica con Fetch, un simple GET para obtener un archivo de imagen. Este mismo código funciona para cualquier otro tipo de archivo, aunque tal vez haya que modificar la parte de la vista previa en el img
.
Para descargar varios archivos será necesario modificar solo la última parte, pero la modificación depende de cómo el servidor envíe los archivos. Por ejemplo, si el servidor envía los archivos como un Array
, donde cada ítem es un Array
de bytes, podemos hacer lo siquiente:
//Para un archivo:
const archivo = await flujoResp.blob();
//Para varios archivos en un Array JSON
//Supongamos que todos son imágenes JPEG
const archivos = await flujoResp.json();
for each (var bloque in archivos) {
const archivo = new Blob(bloque, {type: 'image/jpeg' })
//Aquí: hacer algo con cada archivo
}
Referencia
- Repositorio con código completo: https://github.com/roimergarcia/20230822—descarga-con-progreso
- Fetch API: https://developer.mozilla.org/es/docs/Web/API/Fetch_API/Using_Fetch
Derecho de uso
Los contenidos generados por el autor de este artículo (explicaciones, código fuente, y archivos adjuntos creados por el autor) están disponibles bajo licencia CC BY-SA 3.0, y pueden ser usados, derivados y compartidos bajo los términos indicados en la misma. Los contenidos no generados por el autor de este artículo son propiedad de sus respectivos dueños y están regidos por las licencias que estos hayan dispuesto.