Open gogoend opened 4 years ago
在日常前后端联调中,后端总会向前端提供一个接口文档,使得前端可以通过接口向后端拿数据。但前端到底要如何向后端传值才能够完成预期需求呢?本文就来简单介绍介绍。
首先我们来看一看HTML原生的表单元素 <form>。在以往AJAX还未诞生前,若要向服务器提交数据,必然要使用这一元素。 我们可以在为<form>标签设置一些属性:
<form>
application/x-www-form-urlencoded
multipart/form-data
一个URL,表示一个用于处理表单提交内容的接口,由后端提供。在该接口可对前端传递的内容进行各种操作,例如插入数据到数据库、存储文件到服务器的文件系统等等。本文实例使用一个PHP脚本来作为接口,该文件用于展示前端向服务器提交了什么内容,相关代码大致如下:
<?php // 请求中的所有参数 var_dump( $_REQUEST ); // GET请求中的所有参数 var_dump( $_GET ); // POST请求中的所有参数 var_dump( $_POST ); // 服务器端接收到的文件 var_dump( $_FILES ); // 服务器端接收到的JSON内容 var_dump( json_decode(file_get_contents('php://input')) );
HTML原生的请求方法包括GET和POST两种(DELETE、PUT、PATCH、OPTIONS等其他请求方法本文暂不展开),具体区别详见HTTP 方法:GET 对比 POST。 一般来说,GET方法用于获取数据(例如在日常通过浏览器访问网页的时候,就是通过GET方法拿到网页文件的),POST方法用于改变数据(例如文件的上传就是通过POST方法来进行的)。
那为何使用GET请求的表单不能够使用multipart/form-data来编码呢?详见下文。
这种编码方式实际上在我们网上冲浪时便在使用 —— 虽然有时候并不是专用于表单。例如打开Github,我们在左上角搜索框(其实就是一个使用GET方法的表单)中输入gogoend来查找笔者在GitHub的一些活动,按下回车提交后,在搜索结果页面的 URL 上可以看到这样的字符串:?q=gogoend,这就是application/x-www-form-urlencoded编码,可在右侧开发者工具中看到字符串被解析后的结果。
gogoend
?q=gogoend
这种编码方式,以?分隔URL和参数,参数形如key=value,多个参数之间以&进行分割,表单中的空格替换为+,表单中的非ASCII字符及特殊字符则被替换为URL编码 —— 所谓urlencoded,可以认为这种编码方式是把表单内容转换为符合URL参数规范的字符串。
?
key=value
&
+
urlencoded
例如,使用搜索框搜索gogoend 杰杰大帅帅,搜索结果页面URL中的搜索参数变成了:?q=gogoend+%E6%9D%B0%E6%9D%B0%E5%A4%A7%E5%B8%85%E5%B8%85。
gogoend 杰杰大帅帅
?q=gogoend+%E6%9D%B0%E6%9D%B0%E5%A4%A7%E5%B8%85%E5%B8%85
事实上,这种编码类型是<form>默认的表单编码类型,无论该表单的请求方法是POST还是GET。 例如有如下表单,请求方式是POST,我们没有为它设置enctype属性:
enctype
<form action="../network/show_post_data.php" method="POST"> <div>表单1</div> <label>f1字段</label><input name="f1" /><br /> <button type="submit">提交</button> </form>
提交后,在开发者工具网络面板下,找到提交发起的对应请求,在右侧窗格最下方Form Data即可看到我们提交的内容,点击view source即可看到提交内容的源码。同时可以看到上方请求头中的Content-Type为application/x-www-form-urlencoded。如图所示:
Content-Type
倘若我们将f1字段改为接受一个文件,此时再提交又会发生什么呢?
f1字段
<form action="../network/show_post_data.php" method="POST"> <div>表单1</div> <label>f1字段</label><input name="f1" type="file" /><br /> <button type="submit">提交</button> </form>
经过测试,我们可以发现,此时只提交了该文件的文件名,文件内容并没有被提交…… 如图:
那我们应该如何提交(上传)这个文件呢?这时,我们就需要把表单元素的enctype设置为multipart/form-data了。
从字面意思理解,这里的表单数据是由各个部分组成的 —— 实际上也确实是这样。这种表单的编码类型中可以接受各种类型的字段,例如文本、二进制内容等等,无需像application/x-www-form-urlencoded那样经过URL编码,只需将内容原样插入,即可提交到服务器端。
我们修改一下上文中做的一个表单的代码,将表单enctype属性强制设为multipart/form-data,然后对表单数据进行提交。代码如下:
<form action="../network/show_post_data.php" method="POST" enctype="multipart/form-data" > <div>表单1</div> <label>f1字段</label><input name="f1" /><br /> <button type="submit">提交</button> </form>
提交后发出的请求如图所示:
同样我们在网络面板中找到提交请求,查看Form Data中提交内容的源码,可以看到这样一串字符: ------WebKitFormBoundaryZkwLEvTG1tHjXbhl
------WebKitFormBoundaryZkwLEvTG1tHjXbhl
同时我们可以发现上方请求头中的Content-Type为 multipart/form-data; boundary=----WebKitFormBoundaryZkwLEvTG1tHjXbhl
multipart/form-data; boundary=----WebKitFormBoundaryZkwLEvTG1tHjXbhl
在这里,这一串字符表示的是表单数据的分隔符,可以看出是随机产生的一个字符串,该字符串的内容写在Content-Type中,该字符串使得服务器端程序能够将表单中各个部分进行区分。
我们可以多加入几个不同类型的字段来进行测试。
<form action="../network/show_post_data.php" method="POST" enctype="multipart/form-data" > <div>表单1</div> <label>f1字段</label><input name="f1" /><br /> <label>f2字段</label><input name="f2" type="file" multiple /><br /> <label>f3字段</label><input name="f3" type="hidden" value="f3字段" /><br /> <label>f4字段</label><input name="f4" /><br /> <button type="submit">提交</button> </form>
在表单中输入一些内容后,进行提交。下图右侧开发者工具中就展示了我们向服务器端提交的所有数据。
(略有尴尬……在Chrome浏览器中测试时,在表单提交后,并没有在开发者工具网络面板对应请求中找到Form Data —— 可能是因为笔者并没有使用XHR来提交,导致页面发生了跳转……但表单数据的确是提交成功的;为了方便截图,便使用了Firefox。)
来看看上文中所提及的问题:为何GET方法提交的表单不支持multipart/form-data编码? 参考自GET - HTTP | MDN、RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content,GET没有请求体。
HTML中规范中有提到:
A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request.
大意是:
GET请求中的负载语义不明,在GET请求中发送负载可能会导致请求被拒绝。
在进行GET请求时,所有的请求参数都包含在URL中,且使用了URL编码;multipart/form-data中可以接受文本、二进制等内容,但multipart/form-data仅可以通过请求体来传输。没有请求体的情况下,这些内容无法传输(当然或许也可以在URL中传输经过URL编码的二进制内容 —— 如果二进制内容很大,URL长度不敢想象)
通过上文,我们了解了通过
对于PHP来说,无论表单采用何种编码类型,如果表单数据是使用POST方法传递的,则通过$_POST对象来取值,若表单数据包含文件,则在$_FILES中取值;如果表单数据是使用GET方法来传递的,则通过$_GET来取值。
Ajax时代到来后,网页的用户体验提高了许多 —— 不必再通过
<form id="form" ref="form" v-on:submit="post" > <div>表单1</div> <label>f1字段</label><input name="f1" v-model="f1" /><br /> <label>f2字段</label><input name="f2" type="hidden" :value="f2"/><br /> <label>f3字段</label><input name="f3" v-model="f3"/><br /> <button type="submit">提交</button> </form> <script> let app = new Vue({ el: '#form', data:{ f1:'', f2:'f2f2f2', f3:'' }, methods: { post(e) { e.preventDefault() let formData = JSON.stringify(this.$data) let xhr = new XMLHttpRequest() xhr.open('POST', '../network/show_post_data.php') xhr.send(formData) } } }) </script>
在这里,<form>基本已经失去了它原先的作用,原生的特性只使用了提交事件的监听,而且我们还阻止了它的默认提交行为。在这里使用Vue实例上的data对象来收集数据,提交时将这一对象转换为JSON字符串,并将该字符串在XHR实例发送数据时作为请求体传入send方法。
data
我们看一下提交后发送出去的请求,如图所示:
提交给服务器后,服务器端便可以对提交的JSON字符串进行解析。PHP var_dump()输出的内容如下:
var_dump()
object(stdClass)#1 (4) { ["f1"]=> string(6) "f1f1f1" ["f2"]=> object(stdClass)#2 (0) { } ["f3"]=> string(6) "f3f3f3" ["f4"]=> string(6) "f4f4f4" }
在表单中,我们选择了一个文件来进行上传;无奈在提交前对表单数据进行JSON.stringify()操作时,我们选择好的文件变成了一个空对象。其他数据都成功传递到了服务器,我们选择的文件并没有上传到服务器。因此,虽然目前十分流行使用JSON来对数据进行传输,但对于文件上传便显得有些无能为力了。或许可以在JSON中内嵌文件的base64编码来上传文件,但经过base64编码文件体积会膨胀……这时候,我们就只好请来我们的老朋友 —— multipart/form-data。在这里,如果使用原生的上传方式,在上传结束后页面会发生跳转,这样来看用户体验不是很好,那如何通过Ajax来传递multipart/form-data? 事实上,浏览器中提供了一个名为FormData的类来用于模拟一个使用multipart/form-data编码的表单。在这里我们需要把之前的表单数据转换为FormData。FormData实例通过new来构建,构造函数中可接受一个表单元素作为参数。我们对上方提交部分的代码稍作修改:
JSON.stringify()
<script> let app = new Vue({ el: '#form', data:{ f1:'', f2:'', f3:'f3f3f3', f4:'' }, methods: { post(e) { e.preventDefault() let form = this.$refs.form let formData = new FormData(form) let xhr = new XMLHttpRequest() xhr.open('POST', '../network/show_post_data.php') xhr.send(formData) } } }) </script>
这里的FormData实例中的数据来自于
传输的FormData的源码和原生表单的multipart/form-data编码后的源码基本相同。有关FormData的更多内容,见有关 HTML 表单的一些事儿 —— FormData 篇。
当然,这里仅仅是一个模拟场景,对于如何上传文件还是应当与后端进行联调。例如,后端可能此处会分为两个接口进行两步操作 —— 一个用于上传文件,另一个用于提交表单。
Postman是一款十分常用的接口调试工具。
通常在 Params 选项卡中输入参数,最终参数将会被URL编码后拼接到URL后方。
通常在 Body 选项卡中输入参数,最终参数将会被加入请求体。我们可以在 Body 选项卡下看到几个单选项目 —— form-data、x-www-form-urlencoded、raw、binary。
这里我们根据向接口传递的数据类型,选择具体的选项。
在日常前后端联调中,后端总会向前端提供一个接口文档,使得前端可以通过接口向后端拿数据。但前端到底要如何向后端传值才能够完成预期需求呢?本文就来简单介绍介绍。
从表单元素说起
首先我们来看一看HTML原生的表单元素
<form>
。在以往AJAX还未诞生前,若要向服务器提交数据,必然要使用这一元素。 我们可以在为<form>
标签设置一些属性:application/x-www-form-urlencoded
和multipart/form-data
提交目标
一个URL,表示一个用于处理表单提交内容的接口,由后端提供。在该接口可对前端传递的内容进行各种操作,例如插入数据到数据库、存储文件到服务器的文件系统等等。本文实例使用一个PHP脚本来作为接口,该文件用于展示前端向服务器提交了什么内容,相关代码大致如下:
请求方法
HTML原生的请求方法包括GET和POST两种(DELETE、PUT、PATCH、OPTIONS等其他请求方法本文暂不展开),具体区别详见HTTP 方法:GET 对比 POST。 一般来说,GET方法用于获取数据(例如在日常通过浏览器访问网页的时候,就是通过GET方法拿到网页文件的),POST方法用于改变数据(例如文件的上传就是通过POST方法来进行的)。
编码类型
application/x-www-form-urlencoded
和multipart/form-data
。 值得注意的是,GET 和 POST 两种请求方法所能够使用的编码类型并不相同:那为何使用GET请求的表单不能够使用
multipart/form-data
来编码呢?详见下文。application/x-www-form-urlencoded
这种编码方式实际上在我们网上冲浪时便在使用 —— 虽然有时候并不是专用于表单。例如打开Github,我们在左上角搜索框(其实就是一个使用GET方法的表单)中输入
gogoend
来查找笔者在GitHub的一些活动,按下回车提交后,在搜索结果页面的 URL 上可以看到这样的字符串:?q=gogoend
,这就是application/x-www-form-urlencoded
编码,可在右侧开发者工具中看到字符串被解析后的结果。这种编码方式,以
?
分隔URL和参数,参数形如key=value
,多个参数之间以&
进行分割,表单中的空格替换为+
,表单中的非ASCII字符及特殊字符则被替换为URL编码 —— 所谓urlencoded
,可以认为这种编码方式是把表单内容转换为符合URL参数规范的字符串。例如,使用搜索框搜索
gogoend 杰杰大帅帅
,搜索结果页面URL中的搜索参数变成了:?q=gogoend+%E6%9D%B0%E6%9D%B0%E5%A4%A7%E5%B8%85%E5%B8%85
。事实上,这种编码类型是
<form>
默认的表单编码类型,无论该表单的请求方法是POST还是GET。 例如有如下表单,请求方式是POST,我们没有为它设置enctype
属性:提交后,在开发者工具网络面板下,找到提交发起的对应请求,在右侧窗格最下方Form Data即可看到我们提交的内容,点击view source即可看到提交内容的源码。同时可以看到上方请求头中的
Content-Type
为application/x-www-form-urlencoded
。如图所示:倘若我们将
f1字段
改为接受一个文件,此时再提交又会发生什么呢?经过测试,我们可以发现,此时只提交了该文件的文件名,文件内容并没有被提交…… 如图:
那我们应该如何提交(上传)这个文件呢?这时,我们就需要把表单元素的enctype设置为
multipart/form-data
了。multipart/form-data
从字面意思理解,这里的表单数据是由各个部分组成的 —— 实际上也确实是这样。这种表单的编码类型中可以接受各种类型的字段,例如文本、二进制内容等等,无需像
application/x-www-form-urlencoded
那样经过URL编码,只需将内容原样插入,即可提交到服务器端。我们修改一下上文中做的一个表单的代码,将表单enctype属性强制设为
multipart/form-data
,然后对表单数据进行提交。代码如下:提交后发出的请求如图所示:
同样我们在网络面板中找到提交请求,查看Form Data中提交内容的源码,可以看到这样一串字符:
------WebKitFormBoundaryZkwLEvTG1tHjXbhl
同时我们可以发现上方请求头中的
Content-Type
为multipart/form-data; boundary=----WebKitFormBoundaryZkwLEvTG1tHjXbhl
在这里,这一串字符表示的是表单数据的分隔符,可以看出是随机产生的一个字符串,该字符串的内容写在
Content-Type
中,该字符串使得服务器端程序能够将表单中各个部分进行区分。我们可以多加入几个不同类型的字段来进行测试。
在表单中输入一些内容后,进行提交。下图右侧开发者工具中就展示了我们向服务器端提交的所有数据。
(略有尴尬……在Chrome浏览器中测试时,在表单提交后,并没有在开发者工具网络面板对应请求中找到Form Data —— 可能是因为笔者并没有使用XHR来提交,导致页面发生了跳转……但表单数据的确是提交成功的;为了方便截图,便使用了Firefox。)
来看看上文中所提及的问题:为何GET方法提交的表单不支持
multipart/form-data
编码? 参考自GET - HTTP | MDN、RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content,GET没有请求体。HTML中规范中有提到:
大意是:
在进行GET请求时,所有的请求参数都包含在URL中,且使用了URL编码;
multipart/form-data
中可以接受文本、二进制等内容,但multipart/form-data
仅可以通过请求体来传输。没有请求体的情况下,这些内容无法传输(当然或许也可以在URL中传输经过URL编码的二进制内容 —— 如果二进制内容很大,URL长度不敢想象)通过上文,我们了解了通过