Javascript Fetch: progresso del download
Scopri come monitorare il progresso del download con la Fetch API in JavaScript usando ReadableStream e aggiornare la UI in tempo reale.
La Fetch API è il modo moderno per effettuare richieste di rete in JavaScript, ma await fetch(...) si risolve non appena arrivano gli header della risposta — ben prima che il corpo abbia terminato il download. Per mostrare una barra di avanzamento per un file di grandi dimensioni è necessario leggere il corpo in modo incrementale e contare i byte man mano che arrivano. Questo capitolo spiega come farlo con un ReadableStream, perché Content-Length è importante, come costruire una UI con indicatore di progresso e cosa fetch non è in grado di fare (progresso dell'upload).
Perché fetch ha bisogno di uno stream per il progresso
Quando si scrive const data = await response.json(), il browser bufferizza internamente l'intero corpo e lo restituisce solo quando è completo — non esiste alcun hook per osservare i byte durante il trasferimento. Il corpo della Response, tuttavia, è esposto come ReadableStream tramite response.body. Prelevando i chunk da quello stream manualmente, è possibile misurare quanto è arrivato e aggiornare la UI ad ogni chunk.
La procedura è sempre la stessa:
- Ottenere un reader:
const reader = response.body.getReader(). - Eseguire un loop su
reader.read(), che si risolve in{ done, value }—valueè un chunkUint8Array. - Aggiungere
value.lengtha un totale progressivo e segnalare il progresso. - Conservare i chunk, quindi riassemblarli una volta che
doneètrue.
Leggere il corpo come ReadableStream
Per conoscere la percentuale è necessario anche la dimensione totale. Il server dovrebbe inviare un header di risposta Content-Length; si legge con response.headers.get('Content-Length'). Spesso è assente (codifica a trasferimento chunked, compressione gzip o un server che semplicemente lo omette), quindi l'esempio seguente ricorre a una dimensione fornita dal chiamante come fallback.
La funzione fetchWithProgress scarica dati da un URL specificato e tiene traccia del progresso del download. Legge il corpo della risposta in chunk usando l'interfaccia ReadableStream e chiama la callback onProgress con la lunghezza ricevuta e la lunghezza totale del contenuto. I dati scaricati vengono poi ricostruiti dai chunk e restituiti come stringa decodificata.
La funzione tenta di leggere automaticamente l'header Content-Length. Se l'header è assente (comune con i trasferimenti chunked o la compressione), ricorre al parametro fallbackSize. In produzione, lo sviluppatore o il server devono fornire questa dimensione di fallback. Per file di grandi dimensioni, evitare di bufferizzare tutti i chunk in memoria; elaborarli in modo incrementale o usare response.blob() per le risposte non testuali.
Una volta terminato il loop si dispone di un array di chunk Uint8Array. Per trasformarli in dati utilizzabili, copiarli in un unico buffer e decodificarli (per il testo) oppure avvolgerli in un Blob (per file binari, immagini, download). Vedere JavaScript Blob per lavorare con dati binari e attivare download di file.
Mostrare il progresso agli utenti
Una barra di avanzamento è semplicemente un elemento la cui larghezza riflette received / total. Passare una callback updateProgressBar al posto di quella di logging:
<body>
<div id="progress-bar" style="width: 100%; background-color: #e0e0e0;">
<div id="progress" style="width: 0; height: 20px; background-color: #76c7c0;"></div>
</div>
<div id="output"></div>
<script>
function updateProgressBar(received, total) {
const progressElement = document.getElementById('progress');
const percentage = Math.min(100, (received / total) * 100);
progressElement.style.width = percentage + '%';
}
document.addEventListener('DOMContentLoaded', () => {
const url = 'https://api.w3docs.com/uploads/media/default/0001/05/dd10c28a7052fb6d2ff13bc403842b797a73ff3b.txt';
const size = 3_900_000; // fallback size
// fetchWithProgress is defined in the previous code block
fetchWithProgress(url, updateProgressBar, size)
.then(data => {
document.getElementById('output').textContent = 'File content: ' + data.slice(0, 1000) + '...';
})
.catch(err => console.error("Download failed:", err));
});
</script>
</body>Se si sta scaricando un file molto grande, evitare di aggiornare la UI troppo frequentemente. Ad esempio, invece di aggiornare la barra di avanzamento per ogni singolo chunk, è possibile aggiornarla meno spesso (ad esempio, ogni pochi chunk o in base a un intervallo di tempo). Questo aiuta a mantenere la UI leggera.
Gli elementi HTML dell'esempio precedente impostano una barra di avanzamento per visualizzare il progresso del download e un'area di testo preformattata per mostrare il contenuto scaricato. Il div progress-bar funge da contenitore, mentre il div progress rappresenta l'avanzamento effettivo del download.
Il codice JavaScript aggiorna la barra di avanzamento in base alla lunghezza dei dati ricevuti e alla lunghezza totale del contenuto. La funzione updateProgressBar calcola la percentuale di dati scaricati e regola di conseguenza la larghezza della barra di avanzamento. Il listener degli eventi attiva la funzione fetchWithProgress al caricamento della pagina, aggiornando la barra di avanzamento e mostrando il contenuto scaricato nell'elemento output.
Nota: fetchWithProgress è definita nello snippet precedente. In un progetto reale, assicurarsi che sia nell'ambito corretto (ad esempio, tramite importazioni di moduli, bundling o un tag script globale).
Cosa fetch non può fare: progresso dell'upload
La tecnica con stream tiene traccia solo del progresso del download (risposta). Ad oggi non esiste un modo standard per osservare il progresso dell'upload (corpo della richiesta) con fetch — il corpo della richiesta non è esposto come stream osservabile nei browser. Se si ha bisogno di una barra di avanzamento durante l'invio di un file, ricorrere a XMLHttpRequest, il cui oggetto upload genera eventi progress:
function uploadWithProgress(url, file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url);
// upload.onprogress fires repeatedly while the body is sent
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
onProgress(event.loaded, event.total, percent);
}
};
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(new Error('Upload failed'));
xhr.send(file);
});
}event.lengthComputable indica se event.total è noto; verificare sempre questa condizione prima di calcolare una percentuale.
Annullare un download in corso
Un download prolungato dovrebbe essere annullabile. Passare un AbortSignal a fetch e chiamare controller.abort() per interrompere sia la richiesta che il loop sullo stream — vedere Fetch: Abort per il pattern completo.
Consigli professionali
- Gestire i dati binari:
TextDecoderfunziona solo per il testo. Usareresponse.blob()oresponse.arrayBuffer()per i file binari. - Limitare gli aggiornamenti del progresso: letture rapide dei chunk possono bloccare il thread della UI. Applicare throttling o debounce alla callback di progresso.
- Gestione della memoria: evitare di memorizzare tutti i chunk in un array per file di grandi dimensioni. Elaborarli in modo incrementale.
- Fallback per Content-Length: l'header è spesso assente. Fornire sempre un
fallbackSizeaffidabile. - Ottimizzare il feedback all'utente: fornire un feedback in tempo reale sul progresso del download migliora la soddisfazione degli utenti e può rendere l'applicazione più reattiva.
- Sfruttare le capacità del browser: browser diversi possono avere supporto variabile per le funzionalità avanzate. Testare l'implementazione su più browser per garantire la compatibilità.
- Semplificare quando possibile: per i download in cui il progresso dello streaming non è strettamente necessario,
response.arrayBuffer()offre un modo più semplice per ricostruire i dati senza concatenare manualmente i chunk.
Con questi suggerimenti ed esempi, si è ora pronti a implementare la Fetch API con il monitoraggio del progresso del download nei propri progetti, offrendo agli utenti un'esperienza fluida e informativa.
Conclusione
Padroneggiare la Fetch API significa comprendere non solo come effettuare richieste di base, ma anche come gestire scenari più avanzati come il monitoraggio del progresso del download. Utilizzando i ReadableStream, è possibile monitorare i download e fornire un feedback sugli stessi, migliorando significativamente l'esperienza utente. L'implementazione di queste tecniche garantirà che le applicazioni siano robuste, facili da usare e in grado di gestire trasferimenti di dati di grandi dimensioni in modo efficiente.