摘要:在本教程中,您将学习如何使用 fetch() 方法在 JavaScript 中从服务器下载文件,并跟踪下载进度。
JavaScript 中流的介绍
在 JavaScript 中,流是一种处理通过网络传输的数据的方法。它是一系列随着时间的推移而处理的字节,而不是一次性处理所有字节。
通过使用流,您可以尽快处理到达的数据,这比等待所有可用数据更有效率。
通常,流以块的形式处理数据。块是流中传输的一小部分数据。当大量数据随着时间的推移逐渐到达时,这很有用。
例如,在通过互联网流式传输视频时,服务器会以块的形式发送视频数据,从而允许视频在整个视频文件下载完成之前开始播放。
fetch() 方法允许您通过使用 ReadableStream 来跟踪文件的下载进度。以下是步骤
步骤 1. 调用 fetch() 启动下载
const response = await fetch(url);Code language: JavaScript (javascript)步骤 2. 创建一个 ReadableStream(称为源流)以分块读取 HTTP 响应的主体
const reader = response.body.getReader();Code language: JavaScript (javascript)步骤 3. 创建另一个 ReadableStream 以累积来自源流的数据
const stream = new ReadableStream(start);Code language: JavaScript (javascript)start 是一个函数,负责设置流并控制数据流经它的方式。
步骤 4. 通过累积接收到的块的大小来跟踪进度,并将进度更新到应用程序的用户界面 (UI)。
以下图表说明了如何使用流跟踪下载进度

获取下载进度示例
步骤 1. 创建一个新的项目目录 fetch-progress
mkdir fetch-progress
cd fetch-progressCode language: JavaScript (javascript)步骤 2. 在项目目录中创建一个 data.txt 文件。我们将从应用程序下载此文件。
步骤 3. 使用以下代码创建一个 index.html 文件
<!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)步骤 4. 创建 css/style.css 文件。
步骤 5. 创建 js/app.js 文件以存储 JavaScript 代码
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)它是如何工作的。
首先,定义一个函数 delay 来模拟指定的毫秒数的网络延迟。这对于在文件很小或网络速度很快的情况下测试进度是可选的,因此您无法看到进度。
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));Code language: JavaScript (javascript)其次,选择下载按钮元素并注册一个点击事件处理程序
const downloadBtn = document.getElementById('download-btn');
downloadBtn.addEventListener('click', () => {
downloadFile('data.txt');
});Code language: JavaScript (javascript)在点击事件处理程序中,调用 downloadFile() 函数从应用程序的根目录下载 data.txt 文件。
第三,定义接受要下载的 URL 的 downloadFile() 函数
async function downloadFile(url)Code language: JavaScript (javascript)在 downloadFile 函数中
1) 选择 progress-bar 和 progress-text 元素
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');Code language: JavaScript (javascript)2) 禁用下载按钮,并将进度条值设置为零,并将进度文本设置为 0%
downloadBtn.disabled = true;
progressBar.value = 0;
progressText.textContent = '0%';Code language: JavaScript (javascript)3) 使用 fetch() 方法开始下载文件
const response = await fetch(url);Code language: JavaScript (javascript)4) 如果请求不成功,则抛出错误
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);Code language: JavaScript (javascript)5) 如果请求成功,则通过从 HTTP 响应的标头中检索 content-length 来获取文件大小
const contentLength = response.headers.get('content-length');Code language: JavaScript (javascript)6) 将 content-length 值转换为整数,并将其分配给 totalSize 变量
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;Code language: JavaScript (javascript)7) 通过调用 request.body 属性的 getReader() 方法来获取 ReadableStream
const reader = response.body.getReader();Code language: JavaScript (javascript)这是一个源流,它以块的形式从文件中读取数据。
8) 创建第二个流(自定义流)以从源流读取数据
const stream = new ReadableStream({
// start the stream
async start(controller) {
// ...Code language: JavaScript (javascript)当自定义流启动时,start() 方法会自动调用。它负责设置流并控制数据流经它的方式。
9) 初始化一个变量 loaded 来跟踪到目前为止已读取了多少数据
let loaded = 0;Code language: JavaScript (javascript)10) 创建一个无限循环,不断从流中读取数据,直到流结束
while (true) {Code language: JavaScript (javascript)11) 从流中读取下一个数据块
const { done, value } = await reader.read();Code language: JavaScript (javascript)read() 方法返回一个 Promise,它解析为一个具有两个属性的对象
done:一个布尔值,指示流是否已完成读取。value:读取的数据块。
12) 每次从流中读取数据时,模拟 200 毫秒的网络延迟
await delay(2000);Code language: JavaScript (javascript)请注意,如果您想在生产系统中使用它,则需要删除此代码行。
13) 如果流中没有更多数据要读取,则 done 设置为 true,退出循环
if (done) break;Code language: JavaScript (javascript)14) 通过将当前块大小 (value.length) 添加到 loaded 变量来累积已读取的数据块的大小。
loaded += value.length;Code language: JavaScript (javascript)15) 计算相对于数据总大小 (totalSize) 已加载的数据百分比。
const progress = totalSize ? (loaded / totalSize) * 100 : 0;Code language: JavaScript (javascript)16) 更新进度条和进度文本
progressBar.value = progress;
progressText.textContent = `${progress.toFixed(2)}%`;Code language: JavaScript (javascript)17) 将当前数据块 (value) 添加到流中
controller.enqueue(value);Code language: JavaScript (javascript)18) 一旦所有数据都已读取,就关闭流
controller.close();Code language: JavaScript (javascript)19) 调用 createDownloadLink() 函数下载文件
createDownloadLink(url, stream);Code language: JavaScript (javascript)20) 更新进度文本
progressText.textContent = 'Download Complete!';Code language: JavaScript (javascript)21) 如果流式传输过程中发生错误,则将错误消息设置为 progressText 元素
progressText.textContent = 'Download Failed!';Code language: JavaScript (javascript)22) 无论流式传输是否成功,都启用下载按钮
} finally {
// enable the download button
downloadBtn.disabled = false;
}Code language: JavaScript (javascript)步骤 6. 在 Web 浏览器上启动 index.html 文件。它将显示以下页面

请注意,您需要将 index.html 文件托管在 Web 服务器上。或者,您可以使用 liveserver VS Code 扩展启动 index.html 文件。
如果您单击 下载文件 按钮,它将从 Web 服务器下载 data.txt 文件,并显示进度。

下载完成后,将提示您将文件保存到计算机上的目录中

下载项目源代码
总结
- 流允许您尽快处理通过网络传输的数据,并在第一个数据块到达时以块的形式处理数据。
- 使用 ReadableStream API 处理流数据。