
今天我们来聊下关于HTML表单(form)中form-data的玩法。众所周知,处理表单应用,是一个很常用的功能,其实有很多插件可以使用,您也可以不用插件去处理,比如上篇文章,我们就分享过,如下:
但是我们今天不是讨论是否采用插件的话题,而是另一个主题:form-data。
前言
form表单在网页中是相当常见的应用,不只能够传输纯文字,也能够达到档案上传的功能。不过也因为form 的行为跟其他传输方式较为不同,有时候也产生疑惑与误解。
这篇文章试着从阅读规范理解来龙去脉后,深入理解form 背后到底做了哪些事情,以及 form-data 与其他传输方式的不同之处,最后再提及HTML 的 <form/> 标签背后做了哪些事情。
主要涵盖下列几个重点:
是什么以及为什么需要它
如何理解请求格式
知道form-data 解决了什么问题
为什么需要form-data?
资料的传递需要双方对资料格式有一定的认知。在网路的世界里,我们使用protocol 来规范资料传递的形式。透过HTTP 的 Content-Type 标头,我们可以知道这个请求的内容是什么,进而用对应的方式解读资料。
MIME Type定义了传输格式的种类:
Content-Type: application/json代表请求内容是JSONContent-Type: image/png代表请求内容是图片档
其中 multipart/form-data 就属于 Content-Type 的其中之一。
一般的 Content-Type 往往只能传送一种形式的资料,但在网页的应用当中我们还可能想要上传档案、图片、影片在表单里头,这样的需求促成了 multipart/form-data 规范的出现(RFC7578【https://tools.ietf.org/html/rfc7578】)
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 的作用
Content-Type: multipart/form-data; boundary=——WebKitFormBoundaryFYGn56LlBDLnAkfd
在Content-Type 当中,我们可以看到boundary 后面接着一坨奇怪的字串。这个boundary 的作用是什么呢?
前面有提到,multipart/form-data的目的在于让不同格式的资料可以透过同一个请求发送,所以要有一个方式判断每个资料的界限在哪里,以query parameter 为例:a=b&c=d的 & 就是一个分界点,让电脑有办法知道什么时候分割资料。每次电脑看到这个boundary 的时候就知道这个属性的资料已经读取完毕,可以开始读取下一个资料了。

在规范当中并没有完全限制boundary 的格式,但还是有定义长度跟允许的字元:
开头是两个hypen
总长度在70 以内(不包含hypen 本身)
只接受ASCII 7bit
因此像 helloworldboundary 这样的字串也是完全合法的boundary。
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 请求
知道了 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 观察封包内容是否有被正确解析:

可以看到Encapsulated multipart part 的部分,name=Kalan与档案内容的部分都有被正确解析。这说明了几件事:
multipart/form-data也是HTTP 请求的一种只要符合格式不用浏览器也可以发送请求
档案内容必须在伺服器端解析(请求只是将一坨binary data 传过去)
application/x-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人参与
发表评论: