【JS】--一文了解文件上传全过程
#前言
平常在写业务的时候常常会用的到的是 GET
, POST
请求去请求接口,GET
相关的接口会比较容易基本不会出错,而对于 POST
中常用的 表单提交,JSON
提交也比较容易,但是对于文件上传呢?大家可能对这个步骤会比较害怕,因为可能大家对它并不是怎么熟悉,而浏览器Network
对它也没有详细的进行记录,因此它成为了我们心中的一根刺,我们老是无法确定,关于文件上传到底是我写的有问题呢?还是后端有问题,当然,我们一般都比较谦虚, 总是会在自己身上找原因,可是往往实事呢?可能就出在后端身上,可能是他接受写的有问题,导致你换了各种请求库去尝试,axios
,request
,fetch
等等。那么我们如何避免这种情况呢?我们自身要对这一块够熟悉,才能不以猜的方式去写代码。如果你觉得我以上说的你有同感,那么你阅读完这篇文章你将收获自信,你将不会质疑自己,不会以猜的方式去写代码。
本文比较长可能需要花点时间去看,需要有耐心,我采用自顶向下的方式,所有示例会先展现出你熟悉的方式,再一层层往下, 先从请求端是怎么发送文件的,再到接收端是怎么解析文件的。以下是讲解的大纲,我们先从浏览器端上传文件,再到服务端上传文件,然后我们再来解析文件是如何被解析的。
#前置知识
#什么是 multipart/form-data?
multipart/form-data
最初由 《RFC 1867: Form-based File Upload in HTML》文档提出。
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.
由于文件上传功能将使许多应用程序受益,因此建议对HTML进行扩展,以允许信息提供者统一表达文件上传请求,并提供文件上传响应的MIME兼容表示。
总结就是原先的规范不满足啦,我要扩充规范了。
#文件上传为什么要用 multipart/form-data?
The encoding type application/x-www-form-urlencoded is inefficient for sending large quantities of binary data or text containing non-ASCII characters. Thus, a new media type,multipart/form-data, is proposed as a way of efficiently sending the values associated with a filled-out form from client to server.
1867文档中也写了为什么要新增一个类型,而不使用旧有的application/x-www-form-urlencoded
:因为此类型不适合用于传输大型二进制数据或者包含非ASCII字符的数据。平常我们使用这个类型都是把表单数据使用url编码后传送给后端,二进制文件当然没办法一起编码进去了。所以multipart/form-data
就诞生了,专门用于有效的传输文件。
也许你有疑问?那可以用 application/json
吗?
其实我认为,无论你用什么都可以传,只不过会要综合考虑一些因素的话,multipart/form-data
更好。例如我们知道了文件是以二进制的形式存在,application/json
是以文本形式进行传输,那么某种意义上我们确实可以将文件转成例如文本形式的 Base64
形式。但是呢,你转成这样的形式,后端也需要按照你这样传输的形式,做特殊的解析。并且文本在传输过程中是相比二进制效率低的,那么对于我们动辄几十M几百M的文件来说是速度是更慢的。
以上为什么文件传输要用multipart/form-data
我还可以举个例子,例如你在中国,你想要去美洲,我们的multipart/form-data
相当于是选择飞机,而application/json
相当于高铁,但是呢?中国和美洲之间没有高铁啊,你执意要坐高铁去,你可以花昂贵的代价(后端额外解析你的文本)造高铁去美洲,但是你有更加廉价的方式坐飞机(使用multipart/form-data
)去美洲(去传输文件)。你图啥?(如果你有钱有时间,抱歉,打扰了,老子给你道歉)
#multipart/form-data规范是什么?
摘自 《RFC 1867: Form-based File Upload in HTML》 6.Example
Content-type: multipart/form-data, boundary=AaB03x
--AaB03x
content-disposition: form-data; name="field1"
Joe Blow
--AaB03x
content-disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
可以简单解释一些,首先是请求类型,然后是一个 boundary (分割符),这个东西是干啥的呢?其实看名字就知道,分隔符,当时分割作用,因为可能有多文件多字段,每个字段文件之间,我们无法准确地去判断这个文件哪里到哪里为截止状态。因此需要有分隔符来进行划分。然后再接下来就是声明内容的描述是 form-data 类型,字段名字是啥,如果是文件的话,得知道文件名是啥,还有这个文件的类型是啥,这个也很好理解,我上传一个文件,我总得告诉后端,我传的是个啥,是图片?还是一个txt文本?这些信息肯定得告诉人家,别人才好去进行判断,后面我们也会讲到如果这些没有声明的时候,会发生什么?
好了讲完了这些前置知识,我们接下来要进入我们的主题了。面对File, formData,Blob,Base64,ArrayBuffer,到底怎么做?还有文件上传不仅仅是前端的事。服务端也可以文件上传(例如我们利用某云,把静态资源上传到 OSS 对象存储)。服务端和客户端也有各种类型,Buffer,Stream,Base64....头秃,怎么搞?不急,就是因为上传文件不单单是前端的事,所以我将以下上传文件的一方称为请求端,接受文件一方称为接收方。我会以请求端各种上传方式,接收端是怎么解析我们的文件以及我们最终的杀手锏调试工具-wireshark来进行讲解。
#请求端
#浏览端
#File
首先我们先写下最简单的一个表单提交方式。
<form action="http://localhost:7787/files" method="POST">
<input name="file" type="file" id="file">
<input type="submit" value="提交">
</form>
我们选择文件后上传,发现后端返回了文件不存在。
不用着急,熟悉的同学可能立马知道是啥原因了。嘘,知道了也听我慢慢叨叨。
我们打开控制台,由于表单提交会进行网页跳转,因此我们勾选preserve log
来进行日志追踪。
我们可以发现其实 FormData
中 file
字段显示的是文件名,并没有将真正的内容进行传输。再看请求头。
发现是请求头和预期不符,也印证了 application/x-www-form-urlencoded
无法进行文件上传。
我们加上请求头,再次请求。
<form action="http://localhost:7787/files" enctype="multipart/form-data" method="POST">
<input name="file" type="file" id="file">
<input type="submit" value="提交">
</form>
发现文件上传成功,简单的表单上传就是像以上一样简单。但是你得熟记文件上传的格式以及类型。
#FormData
formData 的方式我随便写了以下几种方式。
<input type="file" id="file">
<button id="submit">上传</button>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
submit.onclick = () => {
const file = document.getElementById('file').files[0];
var form = new FormData();
form.append('file', file);
// type 1
axios.post('http://localhost:7787/files', form).then(res => {
console.log(res.data);
})
// type 2
fetch('http://localhost:7787/files', {
method: 'POST',
body: form
}).then(res => res.json()).tehn(res => {console.log(res)});
// type3
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:7787/files', true);
xhr.onload = function () {
console.log(xhr.responseText);
};
xhr.send(form);
}
</script>
以上几种方式都是可以的。但是呢,请求库这么多,我随便在 npm 上一搜就有几百个请求相关的库。
因此,掌握请求库的写法并不是我们的目标,目标只有一个还是掌握文件上传的请求头和请求内容。
#Blob
Blob
对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File
接口基于Blob
,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
因此如果我们遇到 Blob 方式的文件上方式不用害怕,可以用以下两种方式:
1.直接使用 blob 上传
const json = { hello: "world" };
const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' });
const form = new FormData();
form.append('file', blob, '1.json');
axios.post('http://localhost:7787/files', form);
2.使用 File 对象,再进行一次包装(File 兼容性可能会差一些 https://caniuse.com/#search=File)
const json = { hello: "world" };
const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' });
const file = new File([blob], '1.json');
form.append('file', file);
axios.post('http://localhost:7787/files', form)
#ArrayBuffer
ArrayBuffer
对象用来表示通用的、固定长度的原始二进制数据缓冲区。
虽然它用的比较少,但是他是最贴近文件流的方式了。
在浏览器中,他每个字节以十进制的方式存在。我提前准备了一张图片。
const bufferArrary = [137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,1,3,0,0,0,37,219,86,202,0,0,0,6,80,76,84,69,0,0,255,128,128,128,76,108,191,213,0,0,0,9,112,72,89,115,0,0,14,196,0,0,14,196,1,149,43,14,27,0,0,0,10,73,68,65,84,8,153,99,96,0,0,0,2,0,1,244,113,100,166,0,0,0,0,73,69,78,68,174,66,96,130];
const array = Uint8Array.from(bufferArrary);
const blob = new Blob([array], {type: 'image/png'});
const form = new FormData()