gogoend / blog

blogs, ideas, etc.
MIT License
9 stars 2 forks source link

有关 HTML 表单的一些事儿 —— 到底要给后端传什么东西? #57

Open gogoend opened 4 years ago

gogoend commented 4 years ago

在日常前后端联调中,后端总会向前端提供一个接口文档,使得前端可以通过接口向后端拿数据。但前端到底要如何向后端传值才能够完成预期需求呢?本文就来简单介绍介绍。

从表单元素说起

首先我们来看一看HTML原生的表单元素 <form>。在以往AJAX还未诞生前,若要向服务器提交数据,必然要使用这一元素。 我们可以在为<form>标签设置一些属性:

提交目标

一个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方法来进行的)。

编码类型

编码类型指的是表单最终将会以什么格式提交到服务器端。HTML原生表单支持的编码类型也就是上文中所说的application/x-www-form-urlencodedmultipart/form-data。 值得注意的是,GET 和 POST 两种请求方法所能够使用的编码类型并不相同: - GET POST
可使用的编码类型 application/x-www-form-urlencoded application/x-www-form-urlencoded 、 multipart/form-data

那为何使用GET请求的表单不能够使用multipart/form-data来编码呢?详见下文。

application/x-www-form-urlencoded

这种编码方式实际上在我们网上冲浪时便在使用 —— 虽然有时候并不是专用于表单。例如打开Github,我们在左上角搜索框(其实就是一个使用GET方法的表单)中输入gogoend来查找笔者在GitHub的一些活动,按下回车提交后,在搜索结果页面的 URL 上可以看到这样的字符串:?q=gogoend,这就是application/x-www-form-urlencoded编码,可在右侧开发者工具中看到字符串被解析后的结果。

image

这种编码方式,以?分隔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 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-Typeapplication/x-www-form-urlencoded。如图所示: image

倘若我们将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>

经过测试,我们可以发现,此时只提交了该文件的文件名,文件内容并没有被提交…… 如图: image

那我们应该如何提交(上传)这个文件呢?这时,我们就需要把表单元素的enctype设置为multipart/form-data了。

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>

提交后发出的请求如图所示: image

同样我们在网络面板中找到提交请求,查看Form Data中提交内容的源码,可以看到这样一串字符: ------WebKitFormBoundaryZkwLEvTG1tHjXbhl

同时我们可以发现上方请求头中的Content-Typemultipart/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>

在表单中输入一些内容后,进行提交。下图右侧开发者工具中就展示了我们向服务器端提交的所有数据。 image

(略有尴尬……在Chrome浏览器中测试时,在表单提交后,并没有在开发者工具网络面板对应请求中找到Form Data —— 可能是因为笔者并没有使用XHR来提交,导致页面发生了跳转……但表单数据的确是提交成功的;为了方便截图,便使用了Firefox。)

来看看上文中所提及的问题:为何GET方法提交的表单不支持multipart/form-data编码? 参考自GET - HTTP | MDNRFC 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时代

Ajax时代到来后,网页的用户体验提高了许多 —— 不必再通过来跳转页面进行请求了,可直接在当前页面使用XMLHttpRequest发起请求,使得前后端发生了分离。同时在请求体中可传递的内容也变得更加丰富,例如可传递JSON字符串等等。 目前在前端技术中,十分流行对表单数据和JavaScript中的相关对象进行双向绑定,比较有代表性的框架便是Vue.js。事实上在笔者日常所做的业务中,对于HTML原生表单的使用很少,向后端传值几乎都是使用JSON字符串,原生表单的使用场景仅限于上传文件等极少数情况。 例如,下方的代码描述的是一个使用JSON来作为表单数据进行传递的例子:

    <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方法。

我们看一下提交后发送出去的请求,如图所示: image

提交给服务器后,服务器端便可以对提交的JSON字符串进行解析。PHP 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来构建,构造函数中可接受一个表单元素作为参数。我们对上方提交部分的代码稍作修改:

    <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实例在XHR实例发送数据时作为请求体传入send方法,即可将模拟表单中的数据传递给后端。如图: image

传输的FormData的源码和原生表单的multipart/form-data编码后的源码基本相同。有关FormData的更多内容,见有关 HTML 表单的一些事儿 —— FormData 篇

当然,这里仅仅是一个模拟场景,对于如何上传文件还是应当与后端进行联调。例如,后端可能此处会分为两个接口进行两步操作 —— 一个用于上传文件,另一个用于提交表单。

使用Postman调用接口

Postman是一款十分常用的接口调试工具。

调试GET接口

通常在 Params 选项卡中输入参数,最终参数将会被URL编码后拼接到URL后方。 image

调试POST接口

通常在 Body 选项卡中输入参数,最终参数将会被加入请求体。我们可以在 Body 选项卡下看到几个单选项目 —— form-data、x-www-form-urlencoded、raw、binary。 image

这里我们根据向接口传递的数据类型,选择具体的选项。

  1. form-data对应multipart/form-data
  2. x-www-form-urlencoded对应application/x-www-form-urlencoded
  3. raw对应其他类型的数据,例如文本字符串(比如这里可以是JSON字符串);
  4. binary对应二进制文件。