发布于 2026-01-05 3 阅读
0

如何监控 Javascript fetch 请求的进度并根据需要取消它。

如何监控 Javascript fetch 请求的进度并根据需要取消它。

TL;DR -> 带我去看代码:https://github.com/tq-bit/fetch-progress

在之前的文章中,我已经概述了如何使用 fetch 与 API 进行交互。在本文中,我想深入探讨两个更具体的用例:

  • 在发出HTTP请求的同时监控下载进度。
  • 根据用户输入优雅地取消请求。

如果您想跟着做,可以使用这个 Github 分支开始。它不包含 Javascript,只有一些样式和 HTML:https://github.com/tq-bit/fetch-progress/tree/get-started

一张显示下载进度条的图片,一个用于发起下载请求的按钮和一个用于取消下载的按钮。

这是我们首先要看到的界面。进度指示器将显示获取进度。 

所以,打开你最喜欢的代码编辑器,让我们开始吧。

创建基本获取请求

在开始学习更高级的内容之前,我们先来构建一个简单的函数。任务是开发一个实用工具,用于搜索大学。幸运的是,Hipo正好有一个现成的工具可以用来构建这个功能。

  • 我以这个仓库托管的 API 为起点。
  • 其根网址为http://universities.hipolabs.com/
  • 我想通过查询将搜索范围限定为美国所有大学。
  • 技术方面,我希望将我的获取逻辑放在一个包装函数中。

话虽如此,我们首先将以下代码添加到client.js文件中:

export default function http(rootUrl) {
  let loading = false;

  let chunks = [];
  let results = null;
  let error = null;


  // let controller = null; // We will get to this variable in a second

  const json = async (path, options,) => {
    loading = true

    try {
      const response = await fetch(rootUrl + path, { ...options });

      if (response.status >= 200 && response.status < 300) {
        results = await response.json();
        return results
      } else {
        throw new Error(response.statusText)
      }
    } catch (err) {
      error = err
      results = null
      return error
    } finally {
      loading = false
    }
  }

  return { json }
}
Enter fullscreen mode Exit fullscreen mode

接下来,让我们将此函数导入到main.js文件中并对其进行初始化:

// Import the fetch client and initalize it
import http from './client.js';
const { json } = http('http://universities.hipolabs.com/');

// Grab the DOM elements
const progressbutton = document.getElementById('fetch-button');

// Bind the fetch function to the button's click event
progressbutton.addEventListener('click', async () => {
  const universities = await json('search?country=United+States');
  console.log(universities);
});
Enter fullscreen mode Exit fullscreen mode

点击“获取”按钮,即可将请求的大学信息打印到我们的控制台:

重新构建 .json() 方法

为了监控进度,我们需要重建标准方法的大部分内容.json()。这也意味着我们还需要逐块地组装响应体。

我之前写过一篇关于处理Node.js流的文章。这里展示的方法与之非常相似。

client.js所以,让我们在文件中,紧挨着函数下方添加以下内容json

export default function http(rootUrl) { 

  // ... previous functions
  const _readBody = async (response) => {
    const reader = response.body.getReader();

    // Declare received as 0 initially
    let received = 0;

    // Loop through the response stream and extract data chunks
    while (loading) {
      const { done, value } = await reader.read();
      if (done) {
        // Finish loading 
        loading = false;
      } else {
        // Push values to the chunk array
        chunks.push(value);
      }
    }

    // Concat the chinks into a single array
    let body = new Uint8Array(received);
    let position = 0;

    // Order the chunks by their respective position
    for (let chunk of chunks) {
      body.set(chunk, position);
      position += chunk.length;
    }

    // Decode the response and return it
    return new TextDecoder('utf-8').decode(body);
  }
  return { json }
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们response.json()按如下方式替换:

  // results = response.json();
  // return results;
  results = await _readBody(response)
  return JSON.parse(results)
Enter fullscreen mode Exit fullscreen mode

浏览器中的响应仍然与之前相同——一个解码后的 JSON 对象。由于响应体本身是一个可读流,我们现在可以监控何时读取了新数据,或者流是否已关闭。

获取最大和当前数据长度

以下是用于进度监控的两个核心指标:

  • content-length响应头,变量length
  • length接收到的数据块的累积值,变量received

content-length请注意,如果服务器端未配置标头,则此函数将不起作用。

既然我们已经有了可用的变量received,让我们把它添加content-length_readBody函数中:

  const _readBody = async (response) => {
    const reader = response.body.getReader();

    // This header must be configured serverside
    const length = +response.headers.get('content-length'); 

    // Declare received as 0 initially
    let received = 0; 
  // ...
  if (done) {
      // Finish loading
      loading = false;
    } else {
      // Push values to the chunk array
      chunks.push(value);

      // Add on to the received length
      received += value.length; 
    }
  }
Enter fullscreen mode Exit fullscreen mode

这样,我们就获得了所有相关的指标值。现在缺少的是如何将它们传递给调用函数。这可以通过使用 JavaScript 框架的响应式特性轻松实现,例如 React Hooks 或 Vue 的组合 API。不过,在本例中,我们将使用浏览器内置的 `.` 功能CustomEvent

通过事件提供获取进度信息

为了完善监控功能,我们来创建两个自定义事件:

  • 每当读取数据块时,事件fetch-progress
  • 一个用于获取请求完成的事件fetch-finished

这两个事件都将绑定到窗口对象。这样,它们就可以在http函数的作用域之外使用。

在循环内部_readBody(),按如下方式调整 while... 循环:

  const _readBody = async (response) => {
    // ...

    // Loop through the response stream and extract data chunks
    while (loading) {
      const { done, value } = await reader.read();
      const payload = { detail: { received, length, loading } }
      const onProgress = new CustomEvent('fetch-progress', payload);
      const onFinished = new CustomEvent('fetch-finished', payload)

      if (done) {
        // Finish loading
        loading = false;

        // Fired when reading the response body finishes
        window.dispatchEvent(onFinished)
      } else {
        // Push values to the chunk array
        chunks.push(value);
        received += value.length;

        // Fired on each .read() - progress tick
        window.dispatchEvent(onProgress); 
      }
    }
    // ... 
  }
Enter fullscreen mode Exit fullscreen mode

在用户界面中显示进度

最后一步是捕获自定义事件并相应地更改进度条的值。让我们打开文件main.js并按如下方式进行调整:

  • 获取一些相关的 DOM 元素
  • 添加事件监听器fetch-progress
  • 添加事件监听器fetch-finished
  • 然后我们可以通过解构属性来访问进度值e.detail,并调整进度条的值。
// Import the fetch client and initalize it
import http from './client.js';

// Grab the DOM elements
const progressbar = document.getElementById('progress-bar');
const progressbutton = document.getElementById('fetch-button');
const progresslabel = document.getElementById('progress-label');
const { json } = http('http://universities.hipolabs.com/');

const setProgressbarValue = (payload) => {
  const { received, length, loading } = payload;
  const value = ((received / length) * 100).toFixed(2);
  progresslabel.textContent = `Download progress: ${value}%`;
  progressbar.value = value;
};

// Bind the fetch function to the button's click event
progressbutton.addEventListener('click', async () => {
  const universities = await json('search?country=United+States');
  console.log(universities);
});

window.addEventListener('fetch-progress', (e) => {
  setProgressbarValue(e.detail);
});

window.addEventListener('fetch-finished', (e) => {
  setProgressbarValue(e.detail);
});
Enter fullscreen mode Exit fullscreen mode

好了,现在你可以监控你的获取请求的进度了。

不过,还有一些调整需要做出:

  • 重置作用域变量
  • 允许用户取消请求

如果你已经读到这里,请再耐心读几行。

重置作用域变量

这听起来很简单,但它却能给我们提供一个很好用的、可重复使用的函数。

_readBody()在文件中的 - 函数下方添加以下内容client.js

const _resetLocals = () => {
  loading = false;

  chunks = [];
  results = null;
  error = null;

  controller = new AbortController();
}
Enter fullscreen mode Exit fullscreen mode

记住,你必须先调用resetLocals()json()函数。

export default function http(rootUrl) {
  let loading = false;

  let chunks = [];
  let results = null;
  let error = null;

  let controller = null; // Make sure to uncomment this variable
  const json = async (path, options,) => {
    _resetLocals();
    loading = true
  // ... rest of the json function
  }
// ... rest of the http function
Enter fullscreen mode Exit fullscreen mode

通过上述函数,我们还引入了一个名为 `cut` 的新对象AbortController。顾名思义,我们可以用它来切断一个活动请求。

取消正在进行的请求

利用创建的 AbortController,我们现在可以创建一个信号。它充当控制器本身和传出的 HTTP 请求之间的通信接口。可以把它想象成一个内置的终止开关。

要进行设置,client.js请按如下方式修改您的文件:

  • 创建信号并将其传递给获取请求选项。
  • 创建一个新函数,该函数调用控制器的中止函数。
const json = async (path, options,) => {
  _resetLocals();
  let signal = controller.signal; 
  loading = true

  try {
    const response = await fetch(rootUrl + path, { signal, ...options });
  // ... rest of the trycatch function
  }
// ... rest of the json function
}

// Cancel an ongoing fetch request
const cancel = () => {
  _resetLocals();
  controller.abort();
};

// Make sure to export cancel
return { json, cancel }
Enter fullscreen mode Exit fullscreen mode

最后,我们跳转到main.js第二个按钮,并将事件绑定到该按钮上。

// ... other variable declarations
const abortbutton = document.getElementById('abort-button');
const { json, cancel } = http('http://universities.hipolabs.com/');

// ... other functions and event listeners
abortbutton.addEventListener('click', () => {
  cancel()
  alert('Request has been cancelled')
})
Enter fullscreen mode Exit fullscreen mode

如果您现在点击“获取”并立即取消 请求,您将看到一个警报,表明即使请求返回 HTTP 状态 200,也不会返回任何数据。

更新:Vue 3 fetch 的组合功能

我使用 Vue 3 的 Composition API 重现了此功能。如果您希望在 Vue 应用中实现监控和取消 fetch 请求,建议您查看此 Gist:

https://gist.github.com/tq-bit/79d6ab61727ebf29ed0ff9ddc4deedca

接下来呢?

遗憾的是,在我为本文进行研究时,我还没有找到一种常用的方法来监控上传进度。官方的 whatwg GitHub 代码库中有一个关于名为“上传进度监控”的功能的未解决问题FetchObserver。然而,看来我们需要耐心等待它的实现。或许,它也能使本文中提到的功能更加便捷。让我们拭目以待。

https://github.com/whatwg/fetch/issues/607

更新于 2022 年 12 月 1 日(感谢@nickchomey的提示):

Chrome 从 105 版本开始支持流媒体上传。具体规范请参见:https://chromestatus.com/feature/5274139738767360。后续文章将对此进行详细介绍。

本文最初发布于https://blog.q-bit.me/monitoring-and-canceling-a-javascript-fetch-request/
感谢阅读。如果您喜欢这篇文章,欢迎在 Twitter 上关注我们 🐤 @qbitme

文章来源:https://dev.to/tqbit/how-to-monitor-the-progress-of-a-javascript-fetch-request-and-cancel-it-on-demand-107f