文件上传

概述

浏览器中的文件上传方式,基本有两种方案,一种是使用 HTML 的表单上传,一种是通过 JS 的 ajax 技术上传。
下面对这两种方式进行讨论

一、表单上传

表单上传是最简单的上传文件方式,只需要简单的配置 from 元素的指定参数,不需要编程人员做额外的工作,浏览器会自动帮你做好很多复杂的事情

这里先提前讲表单上传的基本三要素,给大家留个印象:一是 method 必须设置成 post,二是 enctype 必须设置成 multipart/form-data ,三是必须得有类型为 file的 input 元素

正所谓 talk is cheap, show me the code,直接在例子中讲解。

1.1 简单例子

这里有个基本的简单的上传例子,我会对属性 name 为 "info" 的输入框填入 "test",而文件我选择了一个文本文件 test.txt,该文件的内容是一段字符串:"hello word"

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="text" name="info">
    <input type="file" name="upload">
    <button type="submit">上传</button>
</form>

这里只关注下 form 元素的关键属性 enctype (编码方式,也就是编码成 MIME 类型),它决定了浏览器将以什么方式对表单的数据进行编码。
在表单元素中,enctype只有有三个值可选:

1、"application/x-www-form-urlencoded",enctype默认值。故名思义,它的作用就是对 "form" 进行 "urlencoded",也就是对表单的数据进行 URL 编码
2、"multipart/form-data",能极大程度保留表单每项数据的详细信息,每项数据都能有单独的 MIME 类型。注意这种编码方式是将数据放在 HTTP 报文的 body 部分的,所以 GET 请求时,这种编码将会失效
3、"text/plain",这个值很少使用。它的作用是将表单数据整合成一段文本(也就是字符串)。所以服务器收到这个数据将无法解析,只当作文本处理。所以 nodejs 的 body-parser,java 的 springboot,这些库或者框架默认就不支持,除非你自己手动解析这个文本

对此时这个表单点击上传按钮的时候,浏览器会整理表单数据,以 "multipart/form-data" 格式编码数据形成一个 HTTP 请求报文,然后以 post 方式发送到服务器的 /upload 路由。

根据表单请求方式的不同、编码的不同,浏览器会生成不同的 HTTP 请求报文。
这里对各种情况下,对浏览器生成的报文的异同进行讨论。(这里以谷歌浏览器为基准,对于生成的 HTTP 请求报文,为了方便阅读我已经删除了多余的 header 信息,只保留了重点的一行Content-Type

1.2 POST 请求生成的报文

1.2.1 multipart/form-data 编码的报文

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary51R26gaG9hSSCrHF

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

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

hello word
----WebKitFormBoundary51R26gaG9hSSCrHF-

观察该报文的 header 部分,其中的 Content-Type 由两个部分组成,前面的 "multipart/form-data" 是类型信息,后面的 "boundary=----WebKitFormBoundary51R26gaG9hSSCrHF" ,是搭配 "multipart/form-data" 的固定写法,用处就是对该报文的 body 部分的数据充当分界线的作用。
观察该报文的 body 部分,发现它们的都是由一个固定字符串(也就是 header 中 Content-Type 的 boundary 值)作为分界线,而作为文件的那部分数据,有自己单独的 Content-Type,也就是 MIME 类型。

服务器收到 HTTP 请求报文后,根据其请求头的 Content-Type,决定如何之进行解析处理。这里服务器发现其为 "multipart/form-data" 类型,就会根据后面的 boundary 解析该报文的 body 部分,获取数据。

1.2.2 application/x-www-form-urlencoded 编码的报文

POST /upload HTTP/1.1
Content-Type: application/x-www-form-urlencoded

info=test&upload=test.txt

URL 编码,实际上只能处理字符串或者数字,注意到test.txt文件只有其路径,而没有具体值,值被丢弃了。因为 URL 编码,实际上只能处理字符串或者数字,在谷歌浏览器中对文件进行 URL 编码,只会保存该文件的名称。

1.2.3 text/plain 编码的报文

body 部分仅仅只是一段文本:

POST /upload HTTP/1.1
Content-Type: text/plain

info=test
upload=test.txt

1.3 GET 请求生成的报文

我们得知道 HTTP 请求报文中的 Content-Type ,只是用来指示服务器处理 HTTP 请求报文的body,所以表单为 GET 请求的时候,无论对表单的enctype设置了什么类型,浏览器都会对其忽略处理,而采用 URL 进行编码(也就是类似于 "application/x-www-form-urlencoded" 的情况),然后拼接到请求路径后面:

GET /upload?info=test&upload=test.txt HTTP/1.1

注意到 Content-Type 直接不显示了

二、ajax上传

XHR2定义了全新的 FormData 对象,能够进行 "multipart/form-data" 编码的http请求 ,因此,这种方式上传文件,相当于使用表单上传文件。 FormData 操作很简单,这里不仔细讲解操作。
FormData 默认编码是 "multipart/form-data",且 FormData 本身并没有 api 设定编码。当你使用 ajax 上传 FormData 的时候(这个也必须是 POST 方式上传),浏览器就会像表单一样,帮你默默做好一切,生成的报文和 "multipart/form-data" 编码的表单一样
但是,某些行为会打断浏览器的默认行为,导致出现某些问题,这里讲几个主要的注意事项

2.1 修改 HTTP 报文的影响

使用 ajax 发送 FormData 前,修改 HTTP 报文的某些内容。比如设定自己指定的 ContentType,这会让浏览器停止对数据的编码那么就需要你自己的实现。再比如,你自己对数据进行一次编码,那么数据将会被浏览器再次编码,导致服务器无法解析 HTTP 请求报文的请求参数信息。

HTTP 报文的修改,依赖于 XMLHttpRequest,而 ajax 的底层核心就是 XMLHttpRequest。编程人员通常很少自己使用 XMLHttpRequest 来编写 ajax(越是底层操作,就越繁琐),都是采用的 ajax 库来上传。常见的 ajax 库有 axios、jQuery 以及浏览器现在自带的 fetch。

这里主要讲讲使用最广泛的 jQuery 如何避免影响到FormData,其他库其实也大差不差:

  var formData = new FormData()
  // 这里省略添加键值对的操作
  // ...
  $.ajax({
    method: 'POST',
    url: '/upload',
    data: formData,

    // processData 默认值为 true,这里 jQuery 会默认帮你序列化 data,也就是将其进行 URL 编码
    // 因为浏览器会编码,那么这个数据就会被编码两次,服务器得到数据后就不能正确解析请求参数,导致错误
    // 所以设置成 false,交给浏览器处理就好
    processData: false, 

    // 值为 false,那么 jQuery 就不会修改 contentType 属性
    // ajax 发送后,报文会传递给浏览器,浏览器会自动设置
    // 如果设置了,那么浏览器就认定你已经自己完成了编码,不会干预你的操作,从而不会对数据进行编码。
    // 所以要么自己编码、自己设定 contentType,要么浏览器帮你。
    // "multipart/form-data" 类型自己编写将会很繁琐,不推荐自己写。所以这里最好设置成 false
    contentType: false, 

    // 这里省略其他的 ajax 设置
    // ...
  })

2.2 初始化参数 form 的影响

初始化 FormData 时,可以指定一个 form 元素,该元素的 enctype 会影响 FormData 的编码,此时 FormData 的编码就会变成 enctype 指定的值(不支持 "text/plain",而表单的 enctype 只有三种。所以除去 "text/plain",也就意味着 FormData 只有最多两种默认编码)。
注意 FormData 不支持 "text/plain" 编码(当表单设置 "text/plain" 编码时,FormData 的编码还是原来的默认值 "multipart/form-data"),但是支持 "application/x-www-form-urlencoded"。当 FormData 的编码变成 "application/x-www-form-urlencoded" 的时候,如同刚才讨论表单上传一样,文件将不能被传输,只会传输其文件名称

2.3 其他的影响

这是一种极端的情况,也是我偶然发现的:
在调查资料的时候,我发现有一位同行的博客,他说他在苹果系统下,编写钉钉的小程序的时候,上传文件使用了 FormData ,上传的编码为 "application/x-www-form-urlencoded",造成了bug。他也没贴代码什么的,就这么说了一句然后带过,然后他自己写了 "multipart/form-data" 编码。
我不知道这是什么问题,猜测最有可能就是 钉钉 软件自带的小程序的 js 引擎,关于 FormData 的默认编码出了问题。也有可能是这个同行自己操作错误没有发现。

三、总结

这就是文件上传方式的全部了,当然你也可以另辟蹊径自己写编码过程,这里 MDN 已经写好了

GET请求能上传文件吗?

答案是可以的:

  1. 放在请求路径后面,在各种浏览器中针对于 GET 请求,都做出了相应的限制,你可以把小型的文件转换成字符串,然后在服务器解码成文件
  2. 放在请求报文的 body 中,这种应该是最稳妥的了,但是就和 POST 请求就没啥区别了。但是你得用js编码完成许多额外的工作,比如设置请求报文的请求头的Content-Type,自己完成对请求体的编码。但是还要考虑有些浏览器、服务器的限制,它们会主动过滤掉 GET 请求的 body 部分。不只是前端需要编写,后端也得编写相应的解码代码,简直是麻烦的不行。

对了,在stackoverflow上也早有对这个能否通过 GET 请求上传的探讨,链接在这:
File uploading using GET Method:As we all know, file uploading is most often accomplished using POST method. So, why can't the GET method be used for file uploads instead? Is there a specific prohibition against HTTP GET uploads?

posted @ 2022-05-07 16:17  Sebastian·S·Pan  阅读(323)  评论(0编辑  收藏  举报