JS `Fetch API` 进度事件与可中断下载实现

大家好,我是你们今天的临时码农讲师,今天咱们聊聊前端老伙计Fetch API,特别是它那些容易被忽略的进度事件,以及怎么用它来实现一个可中断的下载功能。准备好了吗?咱们发车!

第一站:Fetch API 基础回顾,别掉队!

Fetch API,这玩意儿基本上是取代老掉牙的XMLHttpRequest的现代网络请求方案。它基于Promise,用起来更优雅,更符合现代JavaScript的编码风格。

先来个最简单的例子热热身:

fetch('https://example.com/data.json')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json(); // 或者 response.text(), response.blob() 等
  })
  .then(data => {
    console.log('数据拿到啦:', data);
  })
  .catch(error => {
    console.error('出错了:', error);
  });

这段代码从https://example.com/data.json获取数据,如果一切顺利,就把数据打印到控制台。否则,就抓住错误并报告。

第二站:进度事件大揭秘,不再迷路!

好了,基础知识复习完毕,咱们进入今天的重点:Fetch API的进度事件。

Fetch API提供了两个主要的进度事件:

  • download 用于监听下载进度,也就是从服务器接收数据的进度。
  • upload 用于监听上传进度,也就是向服务器发送数据的进度 (咱们今天主要关注download)。

这两个事件都在response.body上触发,而response.body是一个ReadableStream对象。 要访问这个ReadableStream,你需要使用response.body属性。 重点来了,直接使用response.body并不能直接获取进度信息,我们需要读取ReadableStream的数据块,并在读取过程中计算进度。

fetch('https://example.com/large_file.zip')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const contentLength = response.headers.get('Content-Length');
    const total = parseInt(contentLength, 10);
    let received = 0;

    const reader = response.body.getReader();

    return new ReadableStream({
      start(controller) {
        function push() {
          reader.read().then(({ done, value }) => {
            if (done) {
              controller.close();
              return;
            }

            received += value.length;
            const progress = Math.round((received / total) * 100);
            console.log(`下载进度: ${progress}%`);

            controller.enqueue(value);
            push();
          });
        }

        push();
      }
    });
  })
  .then(stream => new Response(stream))
  .then(response => response.blob())
  .then(blob => {
    // 下载完成,处理 blob 数据
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'large_file.zip'; // 设置下载文件名
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  })
  .catch(error => {
    console.error('下载出错:', error);
  });

这段代码有点复杂,咱们一步一步来解释:

  1. 获取响应: fetch() 发起请求,拿到响应。
  2. 检查响应状态: 确保响应状态码是 200 OK。
  3. 获取文件大小: 从响应头中获取 Content-Length,这就是要下载的文件总大小。 注意,有些服务器可能不提供这个头,这时候你就没法准确计算进度了,只能显示一个模糊的进度条,比如“正在下载…” 。
  4. 创建 ReadableStream 通过 response.body.getReader() 获取一个 ReadableStream 的 reader。
  5. 自定义 ReadableStream 这里是关键! 我们创建一个新的 ReadableStream,并在 start 方法中定义读取数据的逻辑。
  6. 读取数据块: reader.read() 异步读取数据块。 每次读取到一个数据块,就更新已接收的字节数 (received),然后计算进度 (progress)。
  7. 更新进度: 将进度打印到控制台。 你可以把这个进度更新到页面上的进度条元素。
  8. controller.enqueue(value) 将读取到的数据块放入新的ReadableStream中。 这保证了数据流可以继续传递下去。
  9. 递归读取: 调用 push() 函数递归地读取下一个数据块,直到 donetrue,表示数据读取完毕。
  10. 处理完成: 数据读取完成后,将 ReadableStream 转换成 Response 对象,再转换成 blob,最后创建一个下载链接,触发下载。

第三站:可中断下载实现,说停就停!

现在,咱们来挑战一下更高级的功能:可中断的下载。 实现可中断下载的关键在于 AbortController

AbortController 允许你取消 Fetch API 请求。它提供了一个 abort() 方法,调用这个方法会向 fetch() 发起的请求发送一个中止信号。

let controller = new AbortController();
let signal = controller.signal;

fetch('https://example.com/large_file.zip', { signal: signal })
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const contentLength = response.headers.get('Content-Length');
    const total = parseInt(contentLength, 10);
    let received = 0;

    const reader = response.body.getReader();

    return new ReadableStream({
      start(controller) {
        function push() {
          reader.read().then(({ done, value }) => {
            if (done) {
              controller.close();
              return;
            }

            received += value.length;
            const progress = Math.round((received / total) * 100);
            console.log(`下载进度: ${progress}%`);

            controller.enqueue(value);
            push();
          }).catch(error => {
            if (error.name === 'AbortError') {
              console.log('下载已取消');
            } else {
              console.error('读取数据出错:', error);
            }
            controller.error(error); // 非常重要,将错误传递给 ReadableStream
          });
        }

        push();
      },
      cancel(reason) { // 添加 cancel 方法
        console.log('Stream 被取消', reason);
        reader.cancel(reason).then(() => {
          console.log('Reader 取消成功');
        }).catch(err => {
          console.error('Reader 取消失败', err);
        });
      }
    });
  })
  .then(stream => new Response(stream))
  .then(response => response.blob())
  .then(blob => {
    // 下载完成,处理 blob 数据
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'large_file.zip'; // 设置下载文件名
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('下载已取消');
    } else {
      console.error('下载出错:', error);
    }
  });

// 在适当的时候调用 controller.abort() 取消下载
// 例如,点击一个取消按钮
document.getElementById('cancelButton').addEventListener('click', () => {
  controller.abort();
});

这段代码的关键改动如下:

  1. 创建 AbortController 创建一个 AbortController 实例。
  2. 传递 signalAbortControllersignal 传递给 fetch() 的 options。
  3. 处理 AbortErrorcatch 块中,检查错误是否是 AbortError。 如果是,就说明下载被取消了。
  4. cancel 方法: 在自定义 ReadableStream 中添加 cancel 方法。 当 stream 被取消时,这个方法会被调用。 在这个方法中,我们需要调用 reader.cancel(reason) 来取消 reader,并处理取消失败的情况。
  5. controller.error(error)reader.read().then()catch块中,捕获错误后,需要调用controller.error(error)将错误传递给ReadableStream,否则stream不会被正确关闭。
  6. 触发 abort() 在适当的时候(例如,用户点击了取消按钮),调用 controller.abort() 来取消下载。

重点注意事项:

  • Content-Length 准确的进度计算依赖于服务器返回 Content-Length 头。如果服务器没有返回这个头,你将无法准确计算进度。
  • 错误处理: 一定要正确处理 AbortError 和其他可能的错误。
  • ReadableStreamcancel 方法: 实现可中断下载,ReadableStreamcancel 方法至关重要。
  • 兼容性: Fetch API 在现代浏览器中都支持,但在一些老旧浏览器中可能需要polyfill。
  • controller.error(error) 非常重要,如果流读取过程中发生错误,一定要将错误传递给ReadableStream,否则流无法正常关闭,导致内存泄漏。

第四站:优化与进阶,更上一层楼!

上面的代码已经实现了一个基本的可中断下载功能,但还有很多可以优化的地方:

  • 节流: 频繁更新进度条可能会导致性能问题。可以使用节流函数来限制进度更新的频率。
  • 断点续传: 可以通过 Range 请求头来实现断点续传。 当下载中断后,记录已下载的字节数,下次重新开始下载时,使用 Range 请求头告诉服务器从哪个位置开始传输数据。
  • 更好的用户体验: 可以添加更友好的用户界面,例如显示剩余时间和下载速度。
  • 使用 Service Worker: 可以使用 Service Worker 来缓存下载的文件,并提供离线访问能力。

一些实用技巧:

  • 模拟大文件下载: 可以使用一些在线工具或者自己搭建一个简单的服务器来模拟大文件下载,方便测试你的代码。
  • 使用开发者工具: 使用浏览器的开发者工具可以方便地查看网络请求和响应,以及调试 JavaScript 代码。
  • 参考成熟的库: 有一些成熟的 JavaScript 库封装了 Fetch API,并提供了更高级的功能,例如进度条、断点续传等。 可以参考这些库的实现,学习他们的代码技巧。

总结:

今天咱们一起学习了 Fetch API 的进度事件,以及如何使用 AbortController 来实现可中断的下载功能。 Fetch API 功能强大,但使用起来也有一些需要注意的地方。 掌握了这些技巧,你就可以更好地控制网络请求,并为用户提供更好的下载体验。

希望这次旅行对大家有所帮助。 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注