Summary: in this tutorial, you will learn how to download a file from a server in JavaScript using the fetch()
method and track the download progress.
Introduction to stream in JavaScript
In JavaScript, a stream is a way of handling data that is being transferred over a network. It is a sequence of bytes processed over time instead of all at once.
By using stream, you can process data as soon as it arrives, which can be more efficient than waiting for all the available data.
Typically, streams handle data in chunks. A chunk is a small piece of data transferred in a stream. This is useful when large amounts of data are arriving gradually over time.
For example, when streaming video over the internet, the server sends video data in chunks, allowing the video to start playing before the entire video file has been downloaded.
The fetch()
method allows you to track the download progress of a file by using the ReadableStream
. Here are the steps:
Step 1. Call the fetch()
to start the download:
const response = await fetch(url);
Code language: JavaScript (javascript)
Step 2. Create a ReadableStream
(called source stream) to read the body of the HTTP response in chunks:
const reader = response.body.getReader();
Code language: JavaScript (javascript)
Step 3. Create another ReadableStream to accumulate data from the source stream:
const stream = new ReadableStream(start);
Code language: JavaScript (javascript)
The start
is a function that is responsible for setting up the stream and controlling how data flows through it.
Step 4. Track the progress by accumulating the size of chunks received and update the progress to the app’s user interface (UI).
The following diagram illustrates how to track the download progress using stream:
Fetch Download Progress example
Step 1. Create a new project directory fetch-progress
:
mkdir fetch-progress
cd fetch-progress
Code language: JavaScript (javascript)
Step 2. Create a data.txt
file in the project directory. We’ll download this file from the app.
Step 3. Create an index.html
file with the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download with Progress</title>
<link rel="stylesheet" href="css/style.css">
<script src="js/app.js" defer type="module"></script>
</head>
<body>
<main>
<h1>File Download Progress</h1>
<button id="download-btn" class="download-btn">Download File</button>
<div class="progress-container">
<progress id="progress-bar" value="0" max="100">
</progress>
<div id="progress-text" class="progress-text">0%</div>
</div>
</main>
</body>
</html>
Code language: HTML, XML (xml)
Step 4. Create css/style.css
file.
Step 5. Create the js/app.js
file to store the JavaScript code:
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const downloadBtn = document.getElementById('download-btn');
downloadBtn.addEventListener('click', () => {
const url = 'data.txt';
downloadFile(url);
});
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
async function downloadFile(url) {
downloadBtn.disabled = true;
progressBar.value = 0;
progressText.textContent = '0%';
try {
// fetch the file
const response = await fetch(url);
// check if the response is ok
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
// get the total size of the file
const contentLength = response.headers.get('content-length');
// set the max value of the progress bar
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
// create the stream
const reader = response.body.getReader();
const stream = new ReadableStream({
// start the stream
async start(controller) {
let loaded = 0;
while (true) {
// read the next chuck of data
const { done, value } = await reader.read();
// simulate network delay
await delay(200);
if (done) break;
// calcualte the progress %
loaded += value.length;
const progress = totalSize ? (loaded / totalSize) * 100 : 0;
// update the progressbar
progressBar.value = progress;
progressText.textContent = `${progress.toFixed(2)}%`;
// send the data to the controller
controller.enqueue(value);
}
// close the stream
controller.close();
},
});
// create the download link
createDownloadLink(url, stream);
// Update the progress text
progressText.textContent = 'Download Complete!';
} catch (error) {
// update the progress text
progressText.textContent = 'Download Failed!';
} finally {
// enable the download button
downloadBtn.disabled = false;
}
}
const createDownloadLink = async (url, stream) => {
// Create a new blob URL
const responseStream = new Response(stream);
const blob = await responseStream.blob();
// Create a link element, set the download attribute and trigger a download
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = url.split('/').pop();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
Code language: JavaScript (javascript)
How it works.
First, define a function delay
that simulates a network delay for a specified number of milliseconds. This is optional to test the progress in case the file is small or the network speed is fast, so you cannot see the progress.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
Code language: JavaScript (javascript)
Second, select the download button element and register a click event handler:
const downloadBtn = document.getElementById('download-btn');
downloadBtn.addEventListener('click', () => {
downloadFile('data.txt');
});
Code language: JavaScript (javascript)
Inside the click event handler, call the downloadFile()
function to download the data.txt
file from the app’s root directory.
Third, define the downloadFile()
function that accepts a URL to download:
async function downloadFile(url)
Code language: JavaScript (javascript)
In the downloadFile
function:
1) Select the progress-bar
and progress-text
elements:
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
Code language: JavaScript (javascript)
2) Disable the download button and set the progress bar value to zero and progress text to 0%:
downloadBtn.disabled = true;
progressBar.value = 0;
progressText.textContent = '0%';
Code language: JavaScript (javascript)
3) Start downloading the file using the fetch()
method:
const response = await fetch(url);
Code language: JavaScript (javascript)
4) Throw an error if the request is not successful:
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
Code language: JavaScript (javascript)
5) If the request was successful, get the file size by retrieving the content-length
from the header of the HTTP response:
const contentLength = response.headers.get('content-length');
Code language: JavaScript (javascript)
6) Convert the content-length
value to an integer and assign it to the totalSize
variable:
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
Code language: JavaScript (javascript)
7) Get the ReadableStream
by calling the getReader()
method of the request.body
property:
const reader = response.body.getReader();
Code language: JavaScript (javascript)
This is a source stream that reads data from the file in chunks.
8) Create a second stream (custom stream) that reads data from the source stream:
const stream = new ReadableStream({
// start the stream
async start(controller) {
// ...
Code language: JavaScript (javascript)
The start()
method will be called automatically when the custom stream starts. It is in charge of setting up the stream and controlling how data flows through it.
9) Initializes a variable loaded
to track how much data has been read so far:
let loaded = 0;
Code language: JavaScript (javascript)
10) Create an infinite loop that continuously reads data from the stream until the stream ends:
while (true) {
Code language: JavaScript (javascript)
11) Read the next data chunk from the stream:
const { done, value } = await reader.read();
Code language: JavaScript (javascript)
The read()
method returns a Promise that resolves to an object with two properties:
done
: A boolean indicating if the stream has finished reading.value
: The chunk of data that was read.
12) Simulate a network delay of 200ms each time reading data from the stream:
await delay(2000);
Code language: JavaScript (javascript)
Note that you need to delete this line of code if you want to use it for a production system.
13) If no more data from the stream to read, the done is set to true, exit the loop:
if (done) break;
Code language: JavaScript (javascript)
14) Accumulate the size of the data chunks that have been read by adding the current chunk size (value.length
) to the loaded
variable.
loaded += value.length;
Code language: JavaScript (javascript)
15) Calculate the percentage of data that has been loaded relative to the total size of the data (totalSize
).
const progress = totalSize ? (loaded / totalSize) * 100 : 0;
Code language: JavaScript (javascript)
16) Update the progress bar and progress text:
progressBar.value = progress;
progressText.textContent = `${progress.toFixed(2)}%`;
Code language: JavaScript (javascript)
17) Add the current data chunk (value
) to the stream:
controller.enqueue(value);
Code language: JavaScript (javascript)
18) Close the stream once all data has been read:
controller.close();
Code language: JavaScript (javascript)
19) Call the createDownloadLink()
function to download the file:
createDownloadLink(url, stream);
Code language: JavaScript (javascript)
20) Update the progress text:
progressText.textContent = 'Download Complete!';
Code language: JavaScript (javascript)
21) If an error occurs while streaming, set an error message to the progressText
element:
progressText.textContent = 'Download Failed!';
Code language: JavaScript (javascript)
22) Enable the download button whether the streaming is successful or not:
} finally {
// enable the download button
downloadBtn.disabled = false;
}
Code language: JavaScript (javascript)
Step 6. Launch the index.html
file on the web browser. It’ll show the following page:
Note that you need to host the index.html file on a web server. Alternatively, you can launch the index.html file using liveserver extension of VS Code.
If you click the Download File
button, it’ll download the data.txt
file from the web server, displaying the progress.
Once the download is completed, you’ll be prompted to save the file to a directory on your computer:
Download the project source code
Click here to download the project source code
Summary
- Streams allow you to process data transferred over the network in chunks, as soon as the first chunk of data arrives.
- Use ReadableStream API to handle stream data.