前言

上一篇 JavaScript – XMLHttpRequest 有提到 XMLHttpRequest 正在被 Fetch 取代,这篇就继续介绍 Fetch 吧。

参考

阮一峰 – Fetch API 教程

Simple Get Request & Response

const response = await fetch('https://192.168.1.152:44300/products');
console.log('status', response.status);
console.log('response', await response.text()); // 注:读取 response 数据是个异步过程哦,需要 await

对比 XMLHttpRequest 有几个点:

  1. Fetch 代码量比 XMLHttpRequest 少了很多
  2. Fetch 默认 request method 是 GET,所以不需要特别声明
  3. Fetch 用 Promise 方式取代了 XMLHttpRequest 的事件监听方式
  4. Fetch 的 response 有 Stream 的概念,读取数据是异步的,需要 await Promise (Stream 还有其它特色,下面会教)。
    另外,面对不同的 response type 需要使用不同的 response 方法做读取,比如读取 string 使用 response.text(),JSON 使用 response.json()。
    这类似于 XMLHttpRequest 设置 request.responseType 属性。

效果

Request with Query Parameters

Fetch 和 XMLHttpRequest 一样,都没有 built-in 对 Query Parameters 的处理。

我们需要借助 URLSearchParams。

const searchParams = new URLSearchParams({
key1: 'value1',
});
const queryString = '?' + searchParams.toString();
const response = await fetch('https://192.168.1.152:44300/products' + queryString);

然后把 Query String 拼接到 request URL 就可以了。

Request with Header

fetch 函数的第二参数可以设置 headers

const response = await fetch('https://192.168.1.152:44300/products', {
headers: {
Accept: 'application/json',
}, // 用 Array Array 类型也是可以
// headers: [
// ['Accept', 'application/json']
// ],
});

Request and Headers 对象

我们也可以先创建 Request 和 Headers 对象,之后再传入 fetch 函数。

// 创建 headers 对象
const headers = new Headers({
Accept: 'application/json',
}); // 创建 request 对象,并且传入 headers
const request = new Request('https://192.168.1.152:44300/products', {
headers,
}); // 注意,虽然创建 Request 时有传入 Headers 对象,但是它不是相同的引用哦
// 内部有 clone 的概念
console.log(request.headers === headers); // false // 再添加一个 header
request.headers.append('Custom-Header', 'value'); // 发送请求
const response = await fetch(request);

fetch 函数的参数和 Request 对象 constructor 是一致的,不管上层接口是什么,底层实现都是 Request + Headers + fetch 就对了。

Clone Request

Request 有一个 clone 方法,我们可以很简单的复制出一个请求

const request = new Request('https://192.168.1.152:44300/products', {
headers: {
Accept: 'application/json',
},
});
const request2 = request.clone();
// 注意 clone 是深拷贝
console.log(request.headers === request2.headers); // false

clone 是深拷贝哦。

Auto Parse JSON Response

当 XMLHttpRequest 设置 request.responseType = ‘json',response 数据会自动被 JSON.parse 放入 request.response。

Fetch 没有 request.responseType 属性,取而代之的是 Response 对象上有各种类型方法。

const response = await fetch('https://192.168.1.152:44300/products');
const products = await response.json(); // [{ id: 1, name: 'iPhone14' }, { id: 2, name: 'iPhone15' }]

Fetch 的 response.text() 相等于 XMLHttpRequest 的 request.responseType = 'text'。

Fetch 的 response.json() 相等于 XMLHttpRequest 的 request.responseType = 'json'。

Fetch 的 response.blob() 相等于 XMLHttpRequest 的 request.responseType = 'blob'。

以此类推...

Read Response Body Multiple Times

Fetch 的 Response 有 Stream 的概念,每一个 response 的 stream 只能被读取一次。

const response = await fetch('https://192.168.1.152:44300/products');
console.log(await response.json()); // ok
console.log(await response.json()); // error

读取第二次就会报错。解决方法也非常简单,clone response 后才读取,就可以了。

const response = await fetch('https://192.168.1.152:44300/products');
const response2 = response.clone(); console.log(await response.json()); // ok
console.log(await response2.json()); // ok

Response Header

从 response.headers 对象中获取

const response = await fetch('https://192.168.1.152:44300/products');
console.log(response.headers.get('Content-Type')); // 'application/json; charset=utf-8' // for loop all response headers
for (const [key, value] of response.headers.entries()) {
console.log([key, value]); // ['content-type', 'application/json; charset=utf-8']
}

如果遇到重复的 header 那么它会合并起来

key 是 'cutom',value 是 'a, b'

Cross-Origin 跨域请求携带 Cookie

跨域主要是服务端的职责,不熟悉的可以看这篇 ASP.NET Core – CORS (Cross-Origin Resource Sharing)

客户端和跨域有关的是 Cookie。

同跨 (same-origin) 请求,Fetch 会自动携带 Cookie。

跨域 (cross-origin) 请求,Fetch 默认不携带 Cookie,如果我们希望它携带 Cookie 可以设置 credentials。

const response = await fetch('https://192.168.1.152:44300/products', {
credentials: 'include',
});

如果不希望 same-origin 携带 Cookie,可以这样设置 credentials = 'omit'。

const response = await fetch('https://192.168.1.152:44300/products', {
credentials: 'omit',
});

注:Fetch 这一点比 XMLHttpRequest 强,XMLHttpRequest 的 withCredentials 无法控制 same-origin 是否携带 Cookie。

Fetch 默认 credentials 是 'same-site',意思是只有 same-origin 请求会携带 Cookie,cross-origin 请求不携带 Cookie。

Request Error

Request Error 指的是请求失败,通常是 offline 造成的。

status 400-5xx 这些可不算 Request Error 哦,因为这些已经是有 response 成功了,只是在 status 被区分是有问题的。

要处理 Request Error,可以使用 try catch。

try {
const response = await fetch('https://192.168.1.152:44300/products');
} catch (error) {
if(error instanceof TypeError) {
console.log(error.name); // TypeError
console.log(error.message); // Failed to fetch
}
}

其实它就是处理 Promise Rejection,使用 .then().catch 或 .then(() => {}, () => {}) 都可以。

Abort Request

我们可以在任意时间终止一个 request。

创建 abort controller

const abortController = new AbortController();

监听 abort 事件

abortController.signal.addEventListener('abort', () => {
console.log('The request has been aborted, reason: ' + abortController.signal.reason);
});

signal.reason 是 abort 的原因,这点比 XMLHttpRequest 更好,XMLHttpRequest 无法表述原因。

把 AbortController.signal 设置到 fetch request

await fetch('https://192.168.1.152:44300/products', {
signal: abortController.signal,
});

两秒钟后执行 abortController.abort 方法。

setTimeout(() => {
abortController.abort('reason of abort'); // abort 时可以声明原因。
}, 2000);

fetch 内部会监听 abort 事件,当 abort 时会终止请求,并且通知服务端,请求被终止了。

ASP.NET Core 有一个 cancellationToken,可以随时查看是否客户端已经终止请求, 如果已经终止,那就不需要继续执行了。

Abort 会导致 fetch reject promise

try {
const response = await fetch('https://192.168.1.152:44300/products', {
signal: abortController.signal,
});
} catch (error: unknown) {
if (error instanceof DOMException && error.name === 'AbortError') {
console.log(error.message); // The user aborted a request.
console.log(abortController.signal.reason); // reason of abort
}
}

通过 catch + error instanceof DOMException,我们可以识别出 error 来自 abort。

Request Timeout

XMLHttpRequest 可以通过 request.timeout 设置超时限制,Fetch 没有类似的 built-in 设置。

我们只可以通过 Abort Request 来实现超时限制。

const timeoutAbortController = new AbortController();

const timeoutNumber = setTimeout(() => {
timeoutAbortController.abort('Timeout, the request is taking more than five seconds.');
}, 5000); try {
await fetch('https://192.168.1.152:44300/products', {
signal: timeoutAbortController.signal,
});
clearTimeout(timeoutNumber);
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError' && (timeoutAbortController.signal.reason as string).startsWith('Timeout')
) {
console.log(timeoutAbortController.signal.reason);
}
}

Download File

除了请求 JSON 数据,偶尔我们也会需要下载文件。

download txt file

const response = await fetch('https://192.168.1.152:44300/data.txt');
const memoryStream = await response.arrayBuffer();
const bytes = new Uint8Array(memoryStream);
const textDecoder = new TextDecoder();
const text = textDecoder.decode(bytes);
console.log(text); // 'Hello World'

关键是 response.arrayBuffer 方法,它会返回 ArrayBuffer,再通过 Uint8Array 和 TextDecoder 从 ArrayBuffer 读取 data.txt 的内容。

Download Video

Video 通常 size 比较大,用 ArrayBuffer 怕内存会不够,所以用 Blob 会比较合适。

const response = await fetch('https://192.168.1.152:44300/video.mp4');
const blob = await response.blob();
console.log(blob.size / 1024); // 124,645 kb
console.log(blob.type); // video/mp4

download progress

文件大下载慢,最好可以显示进度条

const response = await fetch('https://192.168.1.152:44300/video.mp4');
const reader = response.body!.getReader();
const totalBytes = +response.headers.get('Content-Length')!;
let loadedBytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
loadedBytes += value.length;
const percentage = ((loadedBytes / totalBytes) * 100).toFixed() + '%';
console.log(percentage);
}

XMLHttpRequest 有 progress 事件,Fetch 没有,所以要获取进度会比较麻烦。

首先需要通过 header Content-Length 获取 total。

接着利用 response body (类型是 ReadableStream) reader 分段式读取 response 内容。

然后拿每一次的内容长度做累加,最终计算出进度。

partial data on downloading

XMLHttpRequest 几乎无法做到分段式读取 response 内容 (即便做到也需要很多前提条件),但 Fetch 很容易,方案也很完整。

上面计算进度的例子中,我们就用到了分段式读取。

const response = await fetch('https://192.168.1.152:44300/video.mp4');
const reader = response.body!.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value); // value 的类型是 Uint8Array
}

POST Request

POST 和 GET 大同小异

POST JSON Data

const product = { name: 'iPhone12' };
const productJson = JSON.stringify(product); const response = await fetch('https://192.168.1.152:44300/products', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: productJson,
}); console.log(response.status); // 201
console.log(await response.json()); // { id: 1, name: 'iPhone12' }

JSON 格式需要添加 request header 'Content-Type',然后 body 放要发送的 JSON 数据就可以了。

POST FormData or FormUrlEncoded (multipart/form-data or application/x-www-form-urlencoded)

POST FormData or FormUrlEncoded 和 POST JSON data 大同小异

// POST multipart/form-data
const productFormData = new FormData();
productFormData.set('name', 'iPhone12'); // POST application/x-www-form-urlencoded
const productFormUrlEncoded = new URLSearchParams({
name: 'iPhone12',
}); const response = await fetch('https://192.168.1.152:44300/products', {
method: 'POST',
headers: {
Accept: 'application/json',
},
body: productFormData, // POST multipart/form-data
// body: productFormUrlEncoded, // POST application/x-www-form-urlencoded
});

只是把 send 的数据从 JSON 换成 FormData or URLSearchParams 就可以了。

另外,FormData or FormUrlEncoded 不需要设置 request header 'Content-Type',游览器会依据发送的数据类型自动添加,JSON 之所以需要是因为游览器把 JSON 视为 text/plain。

POST Binary (Blob)

FormData 支持 Blob 类型的 value,所以我们可以使用 FormData 上传二进制文件。

const productFormData = new FormData();
productFormData.set('name', 'iPhone12');
const productDocument = 'Product Detail';
const textEncoder = new TextEncoder();
const bytes = textEncoder.encode(productDocument);
const blob = new Blob([bytes], {
type: 'text/plain',
});
productFormData.set('document', blob); const response = await fetch('https://192.168.1.152:44300/products', {
method: 'POST',
headers: {
Accept: 'application/json',
},
body: productFormData,
});

或者直接发送 Blob 也是可以的。

const productDocument = 'Product Detail';
const textEncoder = new TextEncoder();
const bytes = textEncoder.encode(productDocument);
const blob = new Blob([bytes], {
type: 'text/plain', // 如果二进制没有明确类型,type 就放 application/octet-stream
}); const response = await fetch('https://192.168.1.152:44300/products', {
method: 'POST',
headers: {
Accept: 'application/json',
},
body: blob,
});

upload progress

非常遗憾,Fetch 完全不支持 upload progress。这也是为什么 XMLHttpRequest 还没有完全被取代的原因。

Chromium 105 推出了 Streaming requests with the fetch API 可以解决这个问题,但是其它游览器支持度不高。

Request ReadyState

XMLHttpRequest 有 request.readyState 和 readystatechange 事件可以获知 Request 的不同阶段。

Fetch 没有这些,我们需要自己写

const response = await fetch('https://192.168.1.152:44300/video.mp4');
console.log('readystatechange', '2. headers received');
const total = +response.headers.get('Content-Length')!;
const reader = response.body!.getReader();
console.log('readystatechange', '3. loading');
let loaded = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
loaded += value!.length;
console.log('progress', [total, loaded]);
}
console.log('readystatechange', '4. done');

类似这样。

JavaScript – Fetch的更多相关文章

  1. JavaScript fetch接口

    JavaScript fetch接口 如果看网上的fetch教程,会首先对比XMLHttpRequest和fetch的优劣,然后引出一堆看了很快会忘记的内容(本人记性不好).因此,我写一篇关于fetc ...

  2. javascript fetch 跨域请求时 session失效问题

    javascript 使用fetch进行跨域请求时默认是不带cookie的,所以会造成 session失效. fetch(url, { method: 'POST', credentials: 'in ...

  3. [Javascript] Fetch API

    fetch() does the same thing as XHR, but fetch return a promise. fetch('password.txt', { 'method': 'P ...

  4. JavaScript 中如何拦截全局 Fetch API 的请求和响应?

    本文翻译自 Intercepting JavaScript Fetch API requests and responses 拦截器是可用于预处理或后处理 HTTP 请求的代码块,有助于全局错误处理. ...

  5. 05 . Vue前端交互,fetch,axios,以asyncawait方式调用接口使用及案例

    目标 /* 1. 说出什么是前后端交互模式 2. 说出Promise的相关概念和用法 3. 使用fetch进行接口调用 4. 使用axios进行接口调用 5. 使用asynnc/await方式调用接口 ...

  6. Javescript——API连接 && json数据处理(待续)

    原文链接1:How to Connect to an API with JavaScript 原文链接2:How to Use the JavaScript Fetch API to Get JSON ...

  7. vue再学习

    day01 Vue 是什么? Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架 vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合 使用Vu ...

  8. Vue基础(2)

    fetch与axios请求数据 fetch基本语法: fetch(url,{parmas}).then(res=> res.json()  //返回promise对象 ).then(data=& ...

  9. Portswigger web security academy:Reflected XSS

    Portswigger web security academy:Reflected XSS 目录 Portswigger web security academy:Reflected XSS Ref ...

  10. Vue之前后端交互

    Vue之前后端交互 一.前后端交互模式 接口调用方式 原生ajax 基于jQuery的ajax fetch axios 异步 JavaScript的执行环境是「单线程」 所谓单线程,是指JS引擎中负责 ...

随机推荐

  1. Arctic开源!网易数帆×华泰证券,推动湖仓一体落地

    数字化转型趋势下,各行业对数据生产力的探索与追求逐步进入深水区.现实的问题是,企业数据仓库存储.数据湖多种技术并存的局面将长期存在,如何才能摆脱技术协同的内耗,让大数据直通生产力的彼岸? 8月11日下 ...

  2. PAT-1002 写出这个数 (20分) JavaScript(node)

    读入一个正整数 n,计算其各位数字之和,用汉语拼音写出和的每一位数字. 输入格式: 每个测试输入包含 1 个测试用例,即给出自然数 n 的值.这里保证 n 小于 10100​​ . 输出格式: 在一行 ...

  3. [oeasy]python0068_ 字体样式_正常_加亮_变暗_控制序列

    字体样式 回忆上次内容 上次了解了一个新的转义模式 \33 逃逸控制字符 esc esc 让输出 退出标准输出流 进行控制信息的设置 可以清屏 也可以设置光标输出的位置 还能做什么呢? 可以设置字符的 ...

  4. oeasy教您玩转vim - 89 - # 高亮细节Highlight

    ​ 高亮细节 highight 回忆 这个自动命令 autocmd 还是很方便的 打开时.保存时就会有自动执行的操作 自动命令有这么几大元素 {event} 触发事件 {pattern} 文件模式 { ...

  5. AT_agc022_a 题解

    洛谷链接&Atcoder 链接 本篇题解为此题较简单做法及较少码量,并且码风优良,请放心阅读. 题目简述 给定字符串 \(S\) , 仅包含互不相同的小写字母, 你需要找到仅包含互不相同的小写 ...

  6. MySQL 跨服务器关联查询

    如果您需要在 MySQL 中关联查询位于不同服务器的表(跨服务器关联查询),您可以考虑使用 MySQL 的联机查询(Federated MySQL).联机查询允许您在一个服务器上访问和查询另一个服务器 ...

  7. python网络通信:IP/端口基础知识

    1.学习网络编程的目的 将多个设备通过网络连接在一起,进行数据共享 2.IP地址 作用:在逻辑上标记一台电脑 特点:没有重复的 3.通过收发数据理解IP地址的作用 dest ip 表示目的ip/src ...

  8. Miniconda 切换python版本

    要在 Miniconda 中切换 Python 版本,可以按照以下步骤进行操作: 打开命令提示符或者 Anaconda Prompt(如果已经安装了). 输入 conda info --envs 查看 ...

  9. 认识netty的基本组件

    Java NIO VS Netty 有了 Java NIO,而且 Netty 也是基于 Java NIO 实现,那么为什么不能直接用 Java NIO 来实现网络通信模块呢? 接下来我就给大家解释一下 ...

  10. 【H5】10 嵌入技术

    到目前为止,您应该掌握了将图像.视频和音频嵌入到网页上的诀窍了. 此刻,让我们继续深入学习,来看一些能让您在网页中嵌入各种内容类型的元素: <iframe>, <embed>  ...