获取和跟踪下载进度

摘要:在本教程中,您将学习如何使用 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)。

以下图表说明了如何使用流跟踪下载进度

javascript tracking download progress

获取下载进度示例

步骤 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-barprogress-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 文件。它将显示以下页面

fetch download progress

请注意,您需要将 index.html 文件托管在 Web 服务器上。或者,您可以使用 liveserver VS Code 扩展启动 index.html 文件。

如果您单击 下载文件 按钮,它将从 Web 服务器下载 data.txt 文件,并显示进度。

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

下载项目源代码

单击此处下载项目源代码

总结

  • 流允许您尽快处理通过网络传输的数据,并在第一个数据块到达时以块的形式处理数据。
  • 使用 ReadableStream API 处理流数据。
本教程是否有帮助?