如何监控 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 }
}
接下来,让我们将此函数导入到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);
});
点击“获取”按钮,即可将请求的大学信息打印到我们的控制台:
重新构建 .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 }
}
接下来,我们response.json()按如下方式替换:
// results = response.json();
// return results;
results = await _readBody(response)
return JSON.parse(results)
浏览器中的响应仍然与之前相同——一个解码后的 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;
}
}
这样,我们就获得了所有相关的指标值。现在缺少的是如何将它们传递给调用函数。这可以通过使用 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);
}
}
// ...
}
在用户界面中显示进度
最后一步是捕获自定义事件并相应地更改进度条的值。让我们打开文件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);
});
好了,现在你可以监控你的获取请求的进度了。
不过,还有一些调整需要做出:
- 重置作用域变量
- 允许用户取消请求
如果你已经读到这里,请再耐心读几行。
重置作用域变量
这听起来很简单,但它却能给我们提供一个很好用的、可重复使用的函数。
_readBody()在文件中的 - 函数下方添加以下内容client.js:
const _resetLocals = () => {
loading = false;
chunks = [];
results = null;
error = null;
controller = new AbortController();
}
记住,你必须先调用
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
通过上述函数,我们还引入了一个名为 `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 }
最后,我们跳转到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')
})
如果您现在点击“获取”并立即取消 请求,您将看到一个警报,表明即使请求返回 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://dev.to/tqbit/how-to-monitor-the-progress-of-a-javascript-fetch-request-and-cancel-it-on-demand-107f本文最初发布于https://blog.q-bit.me/monitoring-and-canceling-a-javascript-fetch-request/
感谢阅读。如果您喜欢这篇文章,欢迎在 Twitter 上关注我们 🐤 @qbitme


