表单中 enctype 属性
HTML
中 <form/>
表单的提交,当使用 method
为 POST
时,可以指定属性 enctype
,可能的值为以下三个;文档同时指出:method
中的 GET
和 POST
分别代表 HTTP
协议中的 GET
与 POST
方法,而 enctype
三个值分别都是是 MINE TYPE
。
application/x-www-form-urlencoded
;multipart/form-data
;text/plain
。
提交表单及其 enctype 值与 HTTP 协议的基本关系
POST
在 HTTP
协议中可以理解为从客户端像服务端提交数据最简单、常用的方法,比如在网页上输入姓名电话信息交给服务器去做再次的发送、信息处理或者直接保存。
POST
方法起初是用来向服务器输入数据的。 实际上,通常会用它来支持HTML
的表单。表单中填好的数据通常会被送给服务器,然后由服务器将其发送到它要去的地方(比如,送到一个服务器网关程序中,然后由这个程序对其进行处理)。<span style="font-size: .9rem;" >参考《HTTP 权威指南》中的 《3.3.5 POST》</span>
enctype
的值在表单提交的时候一般会对应 HTTP
报文首部中的 Content-Type
值,但因为 HTTP
协议对请求主体的编码方式并没有规定,所以这里的值又不一定对应请求主体内容的编码方式。
HTTP
报文(在这里是请求报文)的组成分别是对报文进行描述的起始行 (start line
)、包含属性的首部(header
)块, 以及可选的、包含数据的主体(body
)部分。<span style="font-size: .9rem;" >参考《HTTP 权威指南》的 《3.2 报文的组成部分》</span>
Content-Type
的位置就位于 <headers>
部分,其本身属于「实体首部」范畴,用来描述有关实体及其内容的信息,从有关对象类型的信息,到能够对资源使用的各种有效的请求方法。
application/x-www-form-urlencoded
application/x-www-form-urlencoded
应该是最最常见的表单提交方式:
- 如果表单中的
enctype
不指定值,则表单默认采用的提交方式就是这种; - 如果使用 jQuery 的
$.ajax()
方法,请求报文中的Content-Type
默认也会采用这个值。
请求报文的首部:
请求报文主体:
不难看出这里的表单提交是将 <form/>
下所有表单控件的 name
和 value
进行了组合然后提交给服务端做后续处理。同时也很明显,之前准备的属于文件类型的 a.txt
、a.html
和 binary
的实际内容并没有被提交,被提交的只是一个文件名的字符串,需要使这些二进制文件得意提交,需要使用另一个 enctype
的值 multipart/form-data
,之后会讨论到。
对内容拼接和转译的规范
The control names/values are listed in the order they appear in the document. The name is separated from the value by ‘=’ and name/value pairs are separated from each other by ‘&’.1.application/x-www-form-urlencoded,《17.13.4 Form content types》
其 name
和 value
间使用 =
,多个 name=value
键值对间使用 &
连接的组合形式的由来,可以参考 w3.org 关于表单(forms)的文档中的 《17.13.3 Processing form data》 。
non-alphanumeric characters are replaced by `%HH’, a percent sign and two hexadecimal digits representing the ASCII code of the character。
除了 name
或者 value
中出现的所有空格必须使用 +
代替之外,还需要了对于内容中非数字、字母部分的转译,对应的浏览器 API 可以是 encodeURIComponent()
方法,使用这个方法对内容作转译处理,完成之后才能提交。
「空格需要被转义为 +」
w3 文档里有一句:Control names and values are escaped. Space characters are replaced by ‘+’ … 意思是「空格」需要被转移为 +
,但是通过 encodeURIComponent()
方法转译得到的「空格」实际上是 %20
。
读了几个问题和分别指向的参考文章,似乎原因是有一定历史遗留问题1,一个比较简单的答案在这,另外一个试图说清楚历史的答案在这。
大概可以理解为,比如说有这么一条链接:http://www.example.com/some/path/to/resource?param1=value1
,那么 ?
之前的「空格」必须被转译成 %20
,而问号之后的部分则可以转译为两者之一:%20
或者 +
,而如果的的确确需要一个 +
,则使用 %2B
代替。
前端中的数据提交
对前端来说一定非常熟悉 jQuery,其 .serialize()
方法其中就包含了这一部分的处理(具体实现参考源码的这里),单独处理的话可以参考以下这段伪代码,和一个叫做 form-serialize 的库。
multipart/form-data
表单提交的部分请求首部
表单请求的主体内容
请求主体中则完全不同于之前的 x-www-form-urlencoded
生成的以 &
连接的键值对字符串。这就不得不牵扯出了一个问题:multiplart/form-data
是什么?
multipart/form-data 是什么?
首先,名称 multipart/form-data
本身是 MIME 的一种。
In the case of multipart entities, in which one or more different sets of data are combined in a single body, a “multipart” media type field must appear in the entity’s header.
The body must then contain one or more body parts, each preceded by a boundary delimiter line, and the last one followed by a closing boundary delimiter line.
After its boundary delimiter line, each body part then consists of a header area, a blank line, and a body area.
其最早的由来可以追溯到 《RFC 1867: Form-based File Upload in HTML》 这个文档,1995 年的时候第一次为 HTML 中上传文件做的实验性(Experimental)规范,似乎一经发布就被广泛采用沿用至今。
Since file-upload is a feature that will benefit many applications, this proposes an extension to HTML to allow information providers to express file upload requests uniformly, and a MIME compatible representation for file upload responses.
同时从这个文档的标题就可以看出,multipart/form-data
是用来在 HTML 文档中上传二进制文件的。如其名 multipart/form-data
的意思,指传输的数据本身有很多部分,不管是多个文件的上传,还是既有字符串、又包含二进制文件的表单提交。
但这个文档中对 multipart/form-data
的具体格式并没有写的非常详细,只在第六部分的《Examples》当中给了一个很基本的范例,所以 1998 年又有了一份新的《RFC 2388:Returning Values from Forms: multipart/form-data
》来阐明 multipart/form-data
中的各个部分的具体格式。
后来,最近到了 2015 年 6 月,又被更加详细的 《RFC 7578 Returning Values from Forms: multipart/form-data》 代替。
multipart/form-data 的组成
以文档中提到最简单的例子来对 multipart/form-data 的组成格式来做一个基本的梳理
范例
关于组成各部分内容的一般说明
(对应范例)
boundary
:首先在请求首部中,Content-Type
指定为multipart/form-data
之外,需要制定一个额外的值:boundary
。boundary
的内容就是一串自定义字符串,可以直接写成FormDataBoundary
或者随便什么。在上面的示例中,boundary
就是AaB03x
;separator
:定义了boundary
之后,具体作为分隔符(separator
)的是--
+boundary
的格式;CRLF
:\r\n
;Content-disposition
:form-data; name="field_name"
2:multipart/form-data
传输的内容中有很多个部分,每个部分的开头都必须(must)要申明content-disposition: form-data
,- 并且指定当前的部分的 name3;
Content-Type
和Charset
,针对每个部分可选(optional)的首部信息:CRLF
:\r\n
;CRLF
:\r\n
(连续两个 newline);- 这里就到了当前部分的内容本身。
- 内容结束之后这一部分结束,以一个
separator
(--
+boundary
)结束; - 下一部分重新开始。
- …
- 整个
multipart/form-data
结束,最后需要一个结束标识:-- + boundary + --
。
multipart/form-data 的手动拼接
伪代码实现
知道了其格式的组成方式,就可以用代码手动来实现。虽然不多,在项目中也遇到过需要手动来发送 multipart/form-data
格式作为 ajax
发送的主题内容。以下就是一个最基本的伪代码实现,不过需要注意这里只是针对一个 field
中键值对的实现,实际中,更多时候需要对一个表单中的所有控件作循环处理。
multipart/form-data 和 HTML5 中的 FromData 对象
HTML5 中提供的 FormData
对象实际上就浏览器提供将页面内的 Form
打包成 multipart/form-data
格式的 API。
The
FormData
object lets you compile a set of key/value pairs to send usingXMLHttpRequest
.It is primarily intended for use in sending form data, but can be used independently from forms in order to transmit keyed data.
The transmitted data is in the same format that the form’s
submit()
method would use to send the data if the form’s encoding type were set tomultipart/form-data
.
抛开浏览器兼容性,FormData
的使用,绝大部分时候都是和 ajax
一起:
提一下使用 jQuery 中 $.ajax()
方法提交 FormData
需要注意的两点–需要声明的两个参数:
contentType: false
:上面提到 jQuery 会默认将$.ajax
提交表单的请求首部中Content-Type
值设定为application/x-www-form-urlencoded
,这个参数就是告诉 jQuery 不需要那样设置;processData: false
:和上面其实同理,jQuery 的ajax
默认认为传入的data
是一个键值对对象,需要转换成urlencoded
以&
连接并且转译,传入FormData
对象时候的格式就有别于此,不需要对FormData
对象进行拼接和转译。
multipart/form-data 在后端的解析(使用 Nodejs)
可以拼接自然就可以解析,原理上相同:拼接的时候首先在报文头里指定 boundary
,在报文主体中对以指定的 boundary
对内容做拼接,解析的时候自然就是反过来,从报文头里读取指定好了的 boundary
,配合结构中 \r\n
等固定元素对整个流进行拆分。
不过说起来容易、做起来坑多:首先,获取到的文件是流,所以接受的时候,chunk
是在 req.on('data')
的时候不断拼接,单个文件可能非常巨大,所以在一个 chunk
中可能不会得到完整的文件进行存储。假设已经有一个针对 multipart/form-data
的解析方法,那么在这个流的不断接收的过程中需要对其中的字节流不断的读取,同时读取回来的内容需要放在一个暂存的位置6。
一直好奇为啥对于 formdata
的处理不被包含在标准库的功能中,不过还好由社区的力量。社区中常用的应该是 node-formiable 和 multer 两个包来处理请求中的 formdata
数据,前者可以配合 Express 框架使用也可以单独使用,相对灵活;后者则完全是一个 Express 的 middleware,调用的方法主要是 app.use(new mutler(option)) 这样。
node-formiable
multer
上面说的历史遗留中的历史可以参考以下两个文档:RFC 1738 - Uniform Resource Locators (URL) 和 w3.org 的 URI spec 其中 Recommendations 下的 Query Stirng 部分。 ↩︎
《RFC 7585》 中的 《4.3 Multiple Files for One Form Field》,特别针对
<input type="file" multiple/>
,也就是一个name
对应多个文件的情况作了说明:To match widely deployed implementations, multiple files MUST be sent by supplying each file in a separate part but all with the same “name” parameter. 即使用多个部分,和同一个name
来对应一个input
上传的多个文件。 ↩︎这里有一篇记录了遇到类似问题时比较详细的解决思路《Node.js如何解析Form上传? 》,可以参考。 ↩︎
技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。
BEST
Lien(A.K.A 胡椒)