👆 这是第 132 篇不掺水的原创
本文首发于政采云前端团队博客:浅析 FormData https://www.zoo.team/article/formdata
在日常开发中都是使用公司内部封装好的 request
,一直没太注意请求参数类型,源于一次常规需求, 服务端提出:之前的请求参数有问题,需要调整,经过排查后发现之前的 Request Headers
的 Content-Type
字段值为 application/json
,与服务端解码规则不同,可见这篇文章《SpringBoot 是如何解析参数的 (https://juejin.cn/post/6844903841775747079)》,需要更改为 multipart/form-data
,配合改完后,问题解决,也顺便总结一下。
我们现在常用的互联网软件架构 RESTful (https://www.ruanyifeng.com/blog/2011/09/restful.html) ,有一些规则和约束,比如:协议、域名、版本、路径、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 (https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)支持程度比较高,但是 JSON.stringify
在转换某一些数据结构时会出问题,比如 会丢失 function 类型的参数、循环引用时会报错、Blob
/File
对象会被转化成 {}
等等,,可以参考 为何不推荐使用 JSON.stringify 做深拷贝 (https://segmentfault.com/a/1190000023595021),不过 JSON.stringify
还有第三个参数,有兴趣的同学可以去了解下,这是其一,其二,有同学要说了,如果要是图片那可以转换成 base64
格式进行上传解决,这种方式虽然可行,但是转换成 base64
格式需要很多字符,占用很多资源,而且很长,不便于阅读,另外就是服务端接收到这个参数还得解析,很麻烦,此时,FormData
就可用上了。
FormData
这种方式相信很多同学都比较熟悉,它提供了一种表示表单数据的键值对 key/value
的构造方式,由名称和定义就知道 FormData
是专门为表单量身定做的类型,但其实其功能要比 application/json
强得多,比如文件上传的问题,用 FormData
传参能很好的解决,window
上也直接挂载了 FormData (https://developer.mozilla.org/zh-CN/docs/Web/API/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
}
append
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
类型,固定传递。
在我们日常开发中,现有的几种都能够满足我们的使用需求,只是在一些特殊的场景中可能会有一些偏差,具体如何使用还是要看场景,以及和服务端的约定,约定优于配置。
https://www.ruanyifeng.com/blog/2011/09/restful.html
https://zhuanlan.zhihu.com/p/122912935
https://juejin.cn/post/6885726248039874573