引言
Fetch API 提供了一个获取资源的接口(包括跨网络通信)。对于任何使用过 XMLHttpRequest
的开发者来说, 对于 Fetch
应该都能轻松上手, 而且新的 API
提供了更强大和灵活的功能集...
本文主要就是记录下, 在使用 Fetch
期间可能会碰到的几个小案例....
一、取消请求
在前端开发中, 取消请求是一个比较常见的需求了吧!!
下面举个场景, 比如我们要实现一个类似 google
一样的搜索提示下拉框, 下拉框内容是根据输入框内容查询出来的!! 当用户快速输入词条, 必然会高频的调用接口, 这时难免会出现先请求的接口响应速度比后请求的慢, 这样的话就有可能出现响应覆盖的问题!! 这里常规的解决办法有以下几种:
- 防抖: 用户快速地交互过程中, 只使用最后一次交互产生的数据, 然后再发起请求!
- 锁状态: 在上一个接口没有返回数据时, 交互状态一直处于
loading
的锁定状态 - 取消上一个请求: 在发起下一个请求前, 把之前的请求取消掉
防抖
和 锁状态
虽然能解决问题, 但或多或少还是会影响用户的一个体验, 所以个人认为比较好的方案就是 取消上一个请求
。下面将介绍如果在 Fetch
中如何取消请求!
在 Fetch
中如果需要中止未完成请求, 可使用 AbortController, AbortController
接口表示一个控制器对象, 允许你根据需要中止一个或多个 Web
请求
具体使用见下面代码:
- 先创建了一个
AbortController
实例对象controller
- 在
fetch
发起请求过程中, 设置signal
参数, 让对应fetch
请求和AbortController
控制器进行一个绑定 - 通过定时器, 在
1
秒后通过控制器的abort()
方法来取消所有和控制器相互关联的请求 - 最后我们可以在
.catch
中通过判断Error
对象的name
属性, 来检测到请求取消的行为
js
const controller = new AbortController();
fetch(
"/user/12345",
{ signal: controller.signal }
).then(response => {
console.log('请求成功');
}).catch(err => {
if(err.name === "AbortError") {
// 请求被手动取消
} else {
// 处理正常错误
}
});
// 1S 后手动取消请求
setTimeout(() => {
controller.abort();
}, 1000)
二、读取流数据
想必你应该听说过 Server-Sent Events(SSE)
, 如果还不清楚可以看看我的这篇文章 《在 Koa 中基于 gpt-3.5 模型实现一个最基本的流式问答 DEMO》!!
本质上其实就是后端接口和前端建立了一个单向长连接, 然后后端不断的向前端推送流数据, 前端可通过 SSE
的方式来接收流数据!!
但实际上, 在 Fetch
中其实也是支持实时读取流数据的, 我们可以使用 response.body
属性。它是 ReadableStream
特殊对象, 它允许接口逐块(chunk
)为前端提供 body
。在 Streams API 规范中有对 ReadableStream
的详细描述!
如下代码所示:
await reader.read()
: 调用的结果是一个具有两个属性的对象done
: 当读取完成时为true
, 否则为false
value
: 字节的类型化数组:Uint8Array
js
const handle = async () => {
// 1. 请求接口
const response = await fetch('http://127.0.0.1:4000/demo');
const reader = response.body.getReader(); // 获取reader
const decoder = new TextDecoder(); // 文本解码器
// 2. 循环取值
while (true) {
// 取值, value 是后端返回流信息, done 表示后端结束流的输出
const { value, done } = await reader.read();
if (done) break;
// 打印值: 对 value 进行解码
console.log('推送数据', decoder.decode(value));
}
};
handle();
三、获取下载文件长度
上文提到 Fetch
可以分片读取流数据, 那么如果我们能够知道要获取的资源大小或者长度, 那么我们就能够通过资源大小以及获取到的数据大小来计算出请求的进度
所以这里其实还需要后端配合, 需要将响应头 Content-Length
设置为资源的一个完整的长度, 这样前端就可以直接通过响应头 Content-Length
来拿到资源的完整长度, 从而计算出当前下载进度
js
// 1. 启动 fetch, 并获得一个 reader
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100');
const reader = response.body.getReader();
// 2. 获得总长度(length)
const contentLength = +response.headers.get('Content-Length');
// 3. 读取数据
let receivedLength = 0; // 当前接收到了这么多字节
let chunks = []; // 接收到的二进制块的数组(包括 body)
while(true) {
const {done, value} = await reader.read();
if (done) {
break;
}
chunks.push(value);
receivedLength += value.length;
console.log(`Received ${receivedLength} of ${contentLength}`)
}
// 4. 将块连接到单个 Uint8Array
let chunksAll = new Uint8Array(receivedLength); // (4.1)
let position = 0;
for(let chunk of chunks) {
chunksAll.set(chunk, position); // (4.2)
position += chunk.length;
}
// 5. 解码成字符串
let result = new TextDecoder("utf-8").decode(chunksAll);
// 我们完成啦!
let commits = JSON.parse(result);
alert(commits[0].author.login);
四、上传文件进度条
到目前为止, fetch
方法无法跟踪 上传
进度。相关功能可以使用 XMLHttpRequest
来实现
使用 XMLHttpRequest
对象, 它提供了一个 progress
事件, 该事件在 上传数据时
会不断被触发, 并且在回调函数中我们可以获取当当前上传的一个进度, 具体参考 《MDN - 使用 XMLHttpRequest》
js
var oReq = new XMLHttpRequest();
oReq.addEventListener("progress", updateProgress);
oReq.addEventListener("load", transferComplete);
oReq.addEventListener("error", transferFailed);
oReq.addEventListener("abort", transferCanceled);
oReq.open();
// ...
// 服务端到客户端的传输进程(下载)
function updateProgress(oEvent) {
if (oEvent.lengthComputable) {
var percentComplete = (oEvent.loaded / oEvent.total) * 100;
// ...
} else {
// 总大小未知时不能计算进程信息
}
}
function transferComplete(evt) {
console.log("The transfer is complete.");
}
function transferFailed(evt) {
console.log("An error occurred while transferring the file.");
}
function transferCanceled(evt) {
console.log("The transfer has been canceled by the user.");
}