fetch: 取消请求、读取流、获取下载进度...

引言

Fetch API 提供了一个获取资源的接口(包括跨网络通信)。对于任何使用过 XMLHttpRequest 的开发者来说, 对于 Fetch 应该都能轻松上手, 而且新的 API 提供了更强大和灵活的功能集...

本文主要就是记录下, 在使用 Fetch 期间可能会碰到的几个小案例....

一、取消请求

在前端开发中, 取消请求是一个比较常见的需求了吧!!

下面举个场景, 比如我们要实现一个类似 google 一样的搜索提示下拉框, 下拉框内容是根据输入框内容查询出来的!! 当用户快速输入词条, 必然会高频的调用接口, 这时难免会出现先请求的接口响应速度比后请求的慢, 这样的话就有可能出现响应覆盖的问题!! 这里常规的解决办法有以下几种:

  1. 防抖: 用户快速地交互过程中, 只使用最后一次交互产生的数据, 然后再发起请求!
  2. 锁状态: 在上一个接口没有返回数据时, 交互状态一直处于 loading 的锁定状态
  3. 取消上一个请求: 在发起下一个请求前, 把之前的请求取消掉

防抖锁状态 虽然能解决问题, 但或多或少还是会影响用户的一个体验, 所以个人认为比较好的方案就是 取消上一个请求。下面将介绍如果在 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.");
}

五、参考

相关推荐
腾讯TNTWeb前端团队1 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰5 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪5 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy6 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom6 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom7 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试