文章正文

浅析FormData

2022-01-24 发布于 · 阅读量:815

image-20211226182534221.png

前因

在日常开发中都是使用公司内部封装好的 request,一直没太注意请求参数类型,源于一次常规需求, 服务端提出:之前的请求参数有问题,需要调整,经过排查后发现之前的 Request HeadersContent-Type 字段值为 application/json ,与服务端解码规则不同,可见这篇文章《SpringBoot 是如何解析参数的》,需要更改为 multipart/form-data,配合改完后,问题解决,也顺便总结一下。

简单介绍 RESTful

我们现在常用的互联网软件架构 RESTful ,有一些规则和约束,比如:协议、域名、版本、路径、HTTP 动词、状态码等,本文主要总结 HTTP 动词 的部分内容,也就是 HTTP 请求方法,我们常用的请求方法有 GETPOSTPUT 等,GET 请求大家应该比较熟悉,一般是用于获取资源,客户端 通过 URL 传参,但由于请求 URL 的长度限制,参数比较少的时候可以使用,比如一些简单的列表页等。而 POST 就稍稍复杂一点了,一般是用于提交数据,客户端是通过 Request Body 传参,该请求方式在实际业务场景(特别是在中后台系统中)应用广泛,下面我们就以常见的 POST 请求为例简单介绍 FormData 的使用场景。

引入 FormData

很多时候,在 post 提交数据时我们常采用 application/jsonapplication/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 就可用上了。

image-20211226224815407.png

定义

FormData 这种方式相信很多同学都比较熟悉,它提供了一种表示表单数据的键值对 key/value 的构造方式,由名称和定义就知道 FormData 是专门为表单量身定做的类型,但其实其功能要比 application/json 强得多,比如文件上传的问题,用 FormData 传参能很好的解决,window 上也直接挂载了 FormData 对象,很方便我们直接使用。

我们在控制台实例化一个 FormData 对象,然后打印,如下

image-20211226223413812.png

使用

可以看到其原型上有很多的方法,个人感觉这个 FormDataMap 有点像,仔细观察可以知道都有 setgetvalueshas 等方法,我们平常开发主要的使用也就是 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 上面写的很清楚,appendkey 存在,就会附加到已有值集合的后面,而 set 会使用新值覆盖已有的值,所以选择使用哪一种取决于你的需求。

image-20211226224338614.png

那么文章开头就说了 FormData 在文件上传这一块比较有优势,那么它是怎么处理的呢?FormData 对象能够设置三种类型的值,stringBlobFile,所以我们不需要转换格式,可以直接传文件,当我们传递 FileformatData 层,会直接被 appendFormData 对象里,且可以通过 get 获取到值,然后发送请求到服务端,我们能从浏览器入参中清晰的看到 de 参数的类型是 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>
}

image-20211226224815407.png

image-20211226232858147.png

可以看到 每一个参数之间都有一个 ------WebKitFormBoundary *** 区分开,这实际上是 FormData 的规范标志,后面的字符串是浏览器帮我们自动创建的,以 ------WebKitFormBoundary *** 作为分隔符,也作为开始和结尾,其内容主要有 Content-DispositionContent-Type 等,其中 Content-Disposition 是必选项, name 属性代表着表单元素的 keyfilename 则是上传文件的名称,也可以使用 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

❉ 作者介绍 ❉