51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

前端开发:一起了解下HTML表单(form)中form-data的玩法

500.jpg

今天我们来聊下关于HTML表单(form)中form-data的玩法。众所周知,处理表单应用,是一个很常用的功能,其实有很多插件可以使用,您也可以不用插件去处理,比如上篇文章,我们就分享过,如下:

前端开发:收藏表单验证JS验证的应用【非插件】

但是我们今天不是讨论是否采用插件的话题,而是另一个主题:form-data。

前言 {#前言}

form表单在网页中是相当常见的应用,不只能够传输纯文字,也能够达到档案上传的功能。不过也因为form 的行为跟其他传输方式较为不同,有时候也产生疑惑与误解。

这篇文章试着从阅读规范理解来龙去脉后,深入理解form 背后到底做了哪些事情,以及 form-data 与其他传输方式的不同之处,最后再提及HTML 的 <form/> 标签背后做了哪些事情。

主要涵盖下列几个重点:

  • 是什么以及为什么需要它

  • 如何理解请求格式

  • 知道form-data 解决了什么问题

为什么需要form-data?

资料的传递需要双方对资料格式有一定的认知。在网路的世界里,我们使用protocol 来规范资料传递的形式。透过HTTP 的 Content-Type 标头,我们可以知道这个请求的内容是什么,进而用对应的方式解读资料。

MIME Type定义了传输格式的种类:

  • Content-Type: application/json 代表请求内容是JSON

  • Content-Type: image/png代表请求内容是图片档

其中 multipart/form-data 就属于 Content-Type 的其中之一。

一般的 Content-Type 往往只能传送一种形式的资料,但在网页的应用当中我们还可能想要上传档案、图片、影片在表单里头,这样的需求促成了 multipart/form-data 规范的出现(RFC7578【https://tools.ietf.org/html/rfc7578】)

form-data 请求解析 {#form-data-請求解析}

multipart/form-data最大的用处在于使用者可以把复数个资料格式一次传送(一个请求)出去,主要用在HTML 的表单里头,或是在实作档案上传功能时使用到。

接下来我们来观察一下一个 multipart/form-data 的格式长怎么样。要发送一个Content Type 为 multipart/form-data 的请求,可以用HTML 的form 标签达成(或是使用JavaScript 的FormData):

<form enctype="multipart/form-data" action="/upload" method="POST">
  <input type="text" name="name" />
  <input type="file" name="file" />
  <button>Submit</button>
</form>

当点击Submit 按钮时,浏览器会发送一个POST 请求:

POST /upload HTTP/1.1
Host: localhost:3000

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFYGn56LlBDLnAkfd
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36

------WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="name"

Test
------WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain

Hello World
------WebKitFormBoundaryFYGn56LlBDLnAkfd--

因为网页上的请求都是基于HTTP,所以 multipart/form-data 也会是一个HTTP 请求,格式被规范在RFC 当中。

要理解一个 multipart/form-data 请求有两个重点:

  • 知道boundary 的作用

  • 知道每个格式的意义

boundary 的作用 {#boundary-的作用}

Content-Type: multipart/form-data; boundary=------WebKitFormBoundaryFYGn56LlBDLnAkfd

在Content-Type 当中,我们可以看到boundary 后面接着一坨奇怪的字串。这个boundary 的作用是什么呢?

前面有提到,multipart/form-data的目的在于让不同格式的资料可以透过同一个请求发送,所以要有一个方式判断每个资料的界限在哪里,以query parameter 为例:a=b&c=d& 就是一个分界点,让电脑有办法知道什么时候分割资料。每次电脑看到这个boundary 的时候就知道这个属性的资料已经读取完毕,可以开始读取下一个资料了。

image.png

在规范当中并没有完全限制boundary 的格式,但还是有定义长度跟允许的字元:

  • 开头是两个hypen

  • 总长度在70 以内(不包含hypen 本身)

  • 只接受ASCII 7bit

因此像 helloworldboundary 这样的字串也是完全合法的boundary。

Content-Disposition {#content-disposition}

multipart/form-data 里面,Content-Disposition 的作用在于描述这个资料的格式。

Content-Disposition: form-data; name="name"

明了这是 form-data 里面一个field,名字为name

如果是档案的话后面还会额外加上filename,并且在下一行加入 Content-Type 来描述档案的类型:

Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain

空一行之后接着才是资料内容:

------WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="name"

Test
------WebKitFormBoundaryFYGn56LlBDLnAkfd
Content-Disposition: form-data; name="file"; filename="text.txt"
Content-Type: text/plain

Hello World
------WebKitFormBoundaryFYGn56LlBDLnAkfd--

范例当中我使用纯文字档上传,如果用图片档或是其他格式档案的话则会以binary 显示。

Content-Disposition: form-data; name="file"; filename="image.png"
Content-Type: image/png

PNG


IHDR¤@¬
ÃiCCPICC ProfileHTSÙϽétBoô*%ôÐ{³@B!!ØPGp,¨2 cd,(¶A±a :l¨¼<ÂÌ{ë½·Þ¿ÖY÷»;ûì½ÏYçܵÏ
(省略)

实作一个 multipart/form-data 请求 {#實作一個-multipartform-data-請求}

知道了 multipart/form-data 的请求格式之后,就可以自己写一个来观察看看了。在这边使用 node.js 当作范例:

const http = require('http');
const fs = require('fs');
const content = fs.readFileSync('./text.txt');
const formData = {
  name: 'Kalan',
  file: content,
};
let payload = '';
const boundary = 'helloworld';
Object.keys(formData).forEach((k) => {
  let content;
  if (k === 'file') {
    content = [
      `\r\n--${boundary}`,
      `\r\nContent-Disposition: multipart/form-data; name=${k}; filename="text.txt"`,
      `\r\nContent-Type: text/plain`,
      `\r\n`,
      `\r\n${formData[k]}`,
    ].join('');
  } else {
    content = [
      `\r\n--${boundary}`,
      `\r\nContent-Disposition: multipart/form-data; name=${k}`,
      `\r\n`,
      `\r\n${formData[k]}`,
    ].join('');
  }
  payload += content;
});
payload += `\r\n--${boundary}--`;
const options = {
  host: 'localhost',
  port: '3000',
  path: '/upload',
  protocol: 'http:',
  method: 'POST',
  headers: {
    'Content-Type': 'multipart/form-data; boundary=helloworld',
    'Content-Length': Buffer.byteLength(payload),
  },
};
const req = http.request(options, (res) => {});
req.write(payload);
req.end();

实作上很简单,就只是将规范定义的格式填入request body 而已,比较要注意的地方在于每个boundary 都会以两个hypen 开头,最后一个boundary 则会再以两个hypen 当作结尾。

之后我们透过Wireshark 观察封包内容是否有被正确解析:

image.png

可以看到Encapsulated multipart part 的部分,name=Kalan与档案内容的部分都有被正确解析。这说明了几件事:

  • multipart/form-data也是HTTP 请求的一种

  • 只要符合格式不用浏览器也可以发送请求

  • 档案内容必须在伺服器端解析(请求只是将一坨binary data 传过去)

application/x-www-form-urlencoded {#applicationx-www-form-urlencoded}

如果在表单当中使用GET 方法送出,那么所有表单的内容都以url encoded 的方式被传送。举例来说,以下的HTML 点击Submit 按钮后会变成/upload?name=Kalan&file=filename,就算 enctype 指定 multipart/form-data 还是会以 application/x-www-form-urlencoded 的形式送出。

<form enctype="multipart/form-data" action="/upload" method="GET">
  <input type="text" name="name" />
  <input type="file" name="file" />
  <button>Submit</button>
</form>

总结 {#總結}

这篇文章试着从规范理解multipart/form-data,一起探讨form-data 解决了哪些问题,并试着自己建立一个符合规范的 multipart/form-data 请求,进而对这个构造比较特别的HTTP 请求有更深入的了解。

multipart/form-data对于网页应用来说有几个好处:

  • 不同格式的资料可以透过一个请求发送

  • 可以达到使用者传送档案的需求

  • 浏览器有统一的规范可以实作

对开发者来说,理解 multipart/form-data 有几个目的:

  • 知道在网页上达成档案上传的原理

  • 基于HTTP 请求如何规范不同格式资料传输

  • 对于原理的掌握加快开发速度

下一篇文章会以 <form> 这个标签为主题,探讨form 标签与FormData 的应用,以及身为开发者的我们应该注意哪些事情。请持续关注Web前端之家动态吧!

赞(0)
未经允许不得转载:工具盒子 » 前端开发:一起了解下HTML表单(form)中form-data的玩法