theme: smartblue
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
比 XHR 好用的 Fetch
在说 Fetch 和 XHR 之前,不得不先提一提 Axios
,前端同学天天用它发送请求。所谓 Axios 其实是基于 Promise 封装的 HTTP 库。本质也是对原生 XMLHttpRequest
的封装。而原生的 XHR 由于编写过于麻烦,已被 axios 取代了,所以作为 XHR 的升级版,Fetch 应运而生,而它也确实提供了更强大和灵活的功能集。
Fetch 的使用
Fetch 天生就支持 Promise,这让它写起来非常方便,不用像 XHR 一样去判断 readyState
的各种状态:
fetch('http://www.xxx.com/')
.then(...)
.then(...)
.catch(...);
当然,使用 async
和 try...catch
也是很香的。
返回的结果
注意,Fetch 会返回一个 Response 对象,以此呈现对一次请求的响应数据。以下罗列出一些常用的:
属性 | 类型 | 说明 |
---|---|---|
ok | boolean | true 表示成功(HTTP 状态码的范围200-299) |
body | ReadableStream | 可读取的二进制内容,里面放着后端返回的数据 |
status | number | 状态码 |
statusText | string | 与该 Response 状态码一致的状态信息 |
url | string | 请求地址 |
headers | Headers | 请求头信息(看不到,通过 get 读取) |
其中,需要注意的是:只有当网络故障时或请求被阻止时,才会标记为 rejected。也就是说,当接收到 404 或 500 这种表示错误的 HTTP 状态码时,返回的 Promise 也是正常的 fulfilled 状态 ,只不过此时 ok
属性为 false。
读取 Response
Response 提供了很多读取 ReadableStream 的方法:arraybuffer()
、blob()
、formData()
、json()
text()
等,具体请自行参考文档。这里我们使用真实的例子,来介绍下 json()
的使用:
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
.then(resp => {
console.log('response: ', resp)
const data = resp.json()
console.log('data: ', data)
return data
})
.then(data => alert(data[0].sha));
可以看到,json()
最终读取并返回一个 JSON
格式的 Promise 对象。
请求参数的设置
fetch()
接受第二个可选参数,一个可以控制不同配置的 init
对象,可参见 fetch() 查看所有可选的配置和更多描述。 常用属性如下:
属性 | 类型 | 说明 |
---|---|---|
method | string | 请求方法,如 GET、POST |
headers | Headers | 请求头,Headers 对象 |
body | Blob/FormData/BufferSource 等 | 请求的 body 信息 |
mode | string | 请求模式 cors、no-cors、same-origin |
cache | string | 请求的缓存模式,default、no-store、reload、no-cache、force-cache、only-if-cached |
credentials | string | 请求的凭证,如 omit、same-origin、include |
其中,需要注意的是 credentials
属性,它的默认值是 same-origin
,即只有同源才发送 cookie。也就是说,fetch 不会自动发送跨域 cookie,所以为了在当前域名内自动发送跨域 cookie,必须配置 credentials: 'include'
。
Post 请求
以 Content-Type
为例,这里我们介绍 application/json
和 mutipart-formdata
两种形式的 post 请求。
上传 json 数据
const data = { username: 'example' };
fetch('https://example.com/profile', {
method: 'POST', // or 'PUT'
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
})
.catch((error) => {
console.error('Error:', error);
});
上传文件
const fileField = document.querySelector('input[type="file"]');
const formData = new FormData();
formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);
fetch('https://example.com/profile/avatar', {
method: 'POST',
body: formData,
credentials: 'include'
// 注意:这里并没有额外设置 Content-Type: 'multipart/form-data'
})
.then(response => response.json())
.then(result => {
console.log('Success:', result);
})
.catch(error => {
console.error('Error:', error);
});
上传文件时遇到的坑
一、multipart/form-data
必须的 boundary
属性
在实际开发中,我发现:如果上传文件时,设置了 multipart/form-data
,请求会失败,让后端同学帮忙定位了一下问题,说是请求首部字段 Content-Type 中缺少了boundary
。
于是翻阅了 MDN 文档,找到了对于 boundary 的如下说明:
对于多部分实体,boundary 是必需的,其包括来自一组字符的 1 到 70 个字符,已知通过电子邮件网关是非常健壮的,而不是以空白结尾。它用于封装消息的多个部分的边界。
但是,文档并没有说明如何设置 boundary 属性。随后,我又找到一篇关于 如何在 multipart/form-data 中设置 boundary 的文章:
At this moment there is no way to set up boundary for FormData.
Just remove: ‘Content-Type’: ‘multipart/form-data; boundary=——some-random-characters’ – it will cause the Content-Type will be set according to body type.
意思目前还没有办法给 FormData 设置 boundary,唯一的方法就是不要加!…… o(︶︿︶)o
二、总结fetch 请求中的 Content-Type
配置:
- 如果请求的 body 是字符串,则
Content-Type
会默认设置为'text/plain;charset=UTF-8'
; - 当发送 JSON 时,我们会配置
Content-Type: 'application/json'
,这是 JSON 编码的数据所需的正确的 Content-Type ; - 但是,在上传文件时(比如这里是 FormData),我们没有手动设置
Content-Type: 'multipart/form-data'
,因为上传二进制文件时,浏览器会自动设置; - 当然,也可以直接上传二进制数据,同样无需设置 Content-Type,将
Blob
或arrayBuffer
数据放在body
属性里,因为 Blob 对象具有内建的类型。对于 Blob 对象,这个类型就变成了 Content-Type 的值。
封装 Fetch
贴一下封装的 Fetch 请求,供大家参考和修改:
import { message } from 'antd'
// 公司基准地址
const baseUrl = 'http://127.0.0.1:8989'
// 初始options
const initalOptions = {
method: 'get',
body: null,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json;charset=UTF-8'
},
credentials: 'include'
}
// 处理 url 的 query 参数
export const encodeURIparam = (obj) => {
let pairs = []
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
let k = encodeURIComponent(prop)
let v = encodeURIComponent(
typeof obj[prop] !== 'undefined' ? obj[prop] : ''
)
pairs.push(k + '=' + v)
}
}
return pairs.join('&')
}
// 拼接 url
export const handleParams = (url, params) => {
url = baseUrl + url
if (params && Object.keys(params).length) {
url += '?' + encodeURIparam(params)
}
return url
}
// 处理 fetch 返回的 Response
const handleFetchResponse = (response, responseType) => {
const { ok, status, statusText } = response
let data // 是 promise
switch (responseType) {
case 'TEXT':
data = response.text()
break
case 'JSON':
data = response.json()
break
case 'BLOB':
data = response.blob()
break
case 'ARRAYBUFFER':
data = response.arrayBuffer()
break
}
// 成功直接返回 data (ok: 状态码200-299)
if (ok) return data
// 失败返回错误信息以及处理后的 data
if (data) {
data.then((errorInfo) => {
return Promise.reject({
message: statusText,
code: status,
data: errorInfo
})
})
}
}
const http = async (
url,
{ params, noMessage = false, responseType = 'JSON', ...config } = {}
) => {
const options = {
...initalOptions,
...config
}
// step1. 拼接 url
url = handleParams(url, params)
// step2. post/put 移除 Content-Type
responseType = responseType.toUpperCase()
const method = config.method.toUpperCase()
if (['POST', 'PUT'].includes(method)) {
delete options.headers['Content-Type']
}
// step3. 发送请求
try {
const response = await fetch(url, options)
// step4. 处理 Response
return await handleFetchResponse(response, responseType)
} catch (error) {
// step5. 处理错误
// 这里有两种错误:1. 因网络故障或被阻止的请求 2. 错误状态码
const msg = error.message || 'Internal Server Error.'
const status = error.code || 500
// 报错(可选)并 reject 错误信息
if (!noMessage) message.error(`${status} ${msg}`, 5)
return Promise.reject(error)
}
}
export default http
总结
Fetch 是比 XHR 更好用的请求方式,天生自带 Promise,在上传文件时也无需手动配置 Content-Type 头部。当然,在实际项目开发中,还是要基于现有要求做一些封装。所以除了 axios,原生Fetch 现在也是一个不错的选择。
暂无评论内容