前因
在日常开发中都是使用公司内部封装好的 request,一直没太注意请求参数类型,源于一次常规需求, 服务端提出:之前的请求参数有问题,需要调整,经过排查后发现之前的 Request Headers 的 Content-Type 字段值为 application/json ,与服务端解码规则不同,可见这篇文章《SpringBoot 是如何解析参数的》,需要更改为 multipart/form-data,配合改完后,问题解决,也顺便总结一下。
简单介绍 RESTful
我们现在常用的互联网软件架构 RESTful ,有一些规则和约束,比如:协议、域名、版本、路径、HTTP 动词、状态码等,本文主要总结 HTTP 动词 的部分内容,也就是 HTTP 请求方法,我们常用的请求方法有 GET、POST、PUT 等,GET 请求大家应该比较熟悉,一般是用于获取资源,客户端 通过 URL 传参,但由于请求 URL 的长度限制,参数比较少的时候可以使用,比如一些简单的列表页等。而 POST 就稍稍复杂一点了,一般是用于提交数据,客户端是通过 Request Body 传参,该请求方式在实际业务场景(特别是在中后台系统中)应用广泛,下面我们就以常见的 POST 请求为例简单介绍 FormData 的使用场景。
引入 FormData
很多时候,在 post 提交数据时我们常采用 application/json、application/x-www-form-urlencoded 等类型,也确实能够覆盖到大部分的场景,但是有一些场景下,比如文件上传的时候,就不算是好的解决方案了,application/json 作为请求头 Content-Type 字段值时,表示告知服务端参数是序列化后的 JSON 字符串,所以一般在传参时都会用 JSON.stringify 序列化一下,且浏览器对 JSON.stringify API支持程度比较高,但是 JSON.stringify 在转换某一些数据结构时会出问题,比如 会丢失 function 类型的参数、循环引用时会报错、Blob /File 对象会被转化成 {} 等等,,可以参考 为何不推荐使用 JSON.stringify 做深拷贝,不过 JSON.stringify 还有第三个参数,有兴趣的同学可以去了解下,这是其一,其二,有同学要说了,如果要是图片那可以转换成 base64 格式进行上传解决,这种方式虽然可行,但是转换成 base64 格式需要很多字符,占用很多资源,而且很长,不便于阅读,另外就是服务端接收到这个参数还得解析,很麻烦,此时,FormData 就可用上了。
定义
FormData 这种方式相信很多同学都比较熟悉,它提供了一种表示表单数据的键值对 key/value 的构造方式,由名称和定义就知道 FormData 是专门为表单量身定做的类型,但其实其功能要比 application/json 强得多,比如文件上传的问题,用 FormData 传参能很好的解决,window 上也直接挂载了 FormData 对象,很方便我们直接使用。
我们在控制台实例化一个 FormData 对象,然后打印,如下
使用
可以看到其原型上有很多的方法,个人感觉这个 FormData 跟 Map 有点像,仔细观察可以知道都有 set、get、values、has 等方法,我们平常开发主要的使用也就是 append 方法了,一般都会封装一层 request,调用层只需要传入参数的对象集合就可以。
const specialFileType = ['Blob', 'File'];
function formatData (_data) {
const data = new window.FormData()
for (const key in _data) {
let value = _data[key]
if (_data[key] instanceof Object && !specialFileType.includes(_data[key].constructor.name)) {
value = JSON.stringify(_data[key])
}
data.append(key, value)
}
return data
}
ppend or set
这就有同学要问了,为啥不用 set 方法, MDN 上面写的很清楚,append 的 key 存在,就会附加到已有值集合的后面,而 set 会使用新值覆盖已有的值,所以选择使用哪一种取决于你的需求。
那么文章开头就说了 FormData 在文件上传这一块比较有优势,那么它是怎么处理的呢?FormData 对象能够设置三种类型的值,string、Blob、File,所以我们不需要转换格式,可以直接传文件,当我们传递 File 到 formatData 层,会直接被 append 到 FormData 对象里,且可以通过 get 获取到值,然后发送请求到服务端,我们能从浏览器入参中清晰的看到 d 、e 参数的类型是 binary,因为就是二进制的文件类型,这样服务端接到值之后很方便获取。
cosnt View = () => {
const [fileA, setFileA] = useState(null);
const [fileB, setFileB] = useState(null);
const handleClick = () => {
console.log('fileA:', fileA)
console.log('fileB:', fileB)
const p = {
a: { a1: 11, a2: 22 },
b: [1,2,3],
c: 123,
d: fileA[0],
e: fileB[0],
}
const data = formatData(p);
axios({
method: 'POST',
url: '/aa',
data,
// headers: {
// 'content-type': 'multipart/formdata'
// },
})
}
return <div>
<div onClick={handleClick}>发送请求</div>
<input
type='file'
onChange={(a) => {
const v = a.target.files;
setFileA(v);
}}
/>
<input
type='file'
onChange={(a) => {
const v = a.target.files;
setFileB(v);
}}
/>
</div>
}
可以看到 每一个参数之间都有一个 ———WebKitFormBoundary 区分开,这实际上是 FormData 的规范标志,后面的字符串是浏览器帮我们自动创建的,以 ———WebKitFormBoundary 作为分隔符,也作为开始和结尾,其内容主要有 Content-Disposition、Content-Type 等,其中 Content-Disposition 是必选项, name 属性代表着表单元素的 key,filename 则是上传文件的名称,也可以使用 FormData 第三个参数更改 ,另外,我在发送请求时,并没有更改请求头里面的 Content-Type,但实际上我们看到的是正确的 multipart/form-data,这是因为现在的浏览器比较智能,当客户端未设置请求头的 Content-Type 时,请求参数为对象时,某一些浏览器会自动帮我们在 请求头中添加 Content-Type: text/plain,如果传输的数据是 FormData,也会自动帮我们加上 Content-Type: multipart/form-data 等,可能不同浏览器表现行为不一样,但是最好的方式就是客户端与服务端约定好 Content-Type 类型,固定传递。
总结
在我们日常开发中,现有的几种都能够满足我们的使用需求,只是在一些特殊的场景中可能会有一些偏差,具体如何使用还是要看场景,以及和服务端的约定,约定优于配置。