JavaScript – Fetch
前言
上一篇 JavaScript – XMLHttpRequest 有提到 XMLHttpRequest 正在被 Fetch 取代,这篇就继续介绍 Fetch 吧。
参考
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 有几个点:
- Fetch 代码量比 XMLHttpRequest 少了很多
- Fetch 默认 request method 是 GET,所以不需要特别声明
- Fetch 用 Promise 方式取代了 XMLHttpRequest 的事件监听方式
- 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');
类似这样。