0%

文件上传需求之大文件上传

写在前面

本文主要是讲了菜鸡作者(我)在面对文件上传的需求的时候的一些解决方法,如果有问题或者有更好的方法欢迎大家指出。

文件上传

事情的起因是项目中有个需求是上传文件,然后文件上传大家都知道直接丢formData里面给后端就好了

1
2
3
4
cosnt formData = new FormData()
formData.append('file', file)
// 发送文件
this.sendFile(formData)

二进制文件流上传

本以为这样就结束了,但是后来开始联调的时候才发现后端需要的是二进制文件。完蛋没有碰到过这种情况呀,二进制?是要把文件转换成二进制文件么,马上就去查了一下,了解到了上传文件时input会返回上传的文件的FileList对象,每一个文件是其中的一个File对象,然后有一个FileReader对象可以读取文件,然后他有readAsArrayBuffer()的方法可以把读到的内容转换成ArrayBuffer,其中FileReader读取文件的方式是异步的,在读取结束的时会触发onload事件通过这些就可以得到下面的代码

1
2
3
4
5
6
7
8
9
10
11
// 新建一个FileReader对象
const reader = new FileReader()
// 调用readAsArrayBuffer方法把File转换成ArrayBuffer
reader.readAsArrayBuffer(file)
// 监听转换完成
reader.onload = function () {
// binary为得到的二进制文件
const binary = this.result
// 发送文件
this.sendBinary(binary)
}

其实在过程中有个小插曲,发送binary给后端的时候一直给我返回格式不对的错误,然后我就试了各种方法,怀疑自己文件类型转换错了,最后才查出是后端的bug…

大文件上传

再次以为结束了,后来后端说在他那上传浏览器崩溃了。然后给了我文件,然后我上传了一下发现

WX20200329-000436.png

Chrome上传一个200M的文件就直接崩溃了,后来确认了一下需求需要支持大文件的上传,又开了一个新坑,开始研究大文件一般怎么上传,和后端沟通后,后端那可以提供一个分割文件上传的接口,我可以把文件拆分成2M大小的一个个小片段分开上传。然后通过研究得到前端这边的需要做的是,直接把之前的文件通过slice进行拆分,通过循环,一次一次的发送请求,告诉后端一个开始和结束的请求,和当前文件的编号,最后后端对拆分的文件进行拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 新建一个FileReader对象
const reader = new FileReader()
// 调用readAsArrayBuffer方法把File转换成ArrayBuffer
reader.readAsArrayBuffer(file)
// 监听转换完成
reader.onload = function () {
// binary为得到的二进制文件
const binary = this.result
// 这里定义拆分后的文件为2M一个
const blockSize = 2 * 1024 * 1024
// 如果文件小于2M就直接上传
if (file.size <= blockSize) {
// 上传文件
this.sendBinary(binary)
} else {
// 记录分割后的文件的顺序,后端按照顺序进行拼接
let num = 1
// 文件的标识,可以随机生产一个随机数,带有这个标识的是同一个文件
const id = new Date().getTime()
// 当前分割的文件的位置,初始为单个拆分文件的大小
let nextSize = blockSize
// 循环到分割文件的位置是文件的大小截止
while (file.size > nextSize) {
// 当前分割的文件的位置,因为在最后一个的时候不一定是2M大小的文件,最后一个位置是文件的大小
nextSize = Math.min(num * blockSize, file.size)
// 分割文件,上一次的位置到当前文件的位置
const slice = binary.slice((num - 1) * blockSize, nextSize)
// 开始上传
const param = new FormData()
// 传一个文件的序号
param.append('chunk', num)
// 传分割后的文件
param.append('slice', slice)
// 传文件的唯一标识
param.append('id', id)
// 如果是最后一次,传给后端一个结束的标志
if (num * blockSize >= file.size) {
param.append('status', 'end')
}
num++
// 发送请求
this.sendFileSlice(param)
}
}
}

后来发现还是有问题,看接口文档才知道,这回后端接受的分割后的类型是File类型,emmm没办法,我找到了File的构造器,可以通过ArrayBuffer创建一个File对象。然后去MDN找到了文档。

var myFile = new File(bits, name[, options]);

bits:ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的 Array — 或者任何这些对象的组合。这是 UTF-8 编码的文件内容。

name:USVString,表示文件名称,或者文件路径。

这不就把我得到的slice传过去就好了,然后我就

1
const data = new File(slice, 'test.txt')

然后报错

1
Uncaught TypeError: Failed to construct 'File': Iterator getter is not callable.

难道是MDN出错了,然后我仔细观察了一下他的示例,发现他传的参数是一个数组

1
2
3
var file = new File(["foo"], "foo.txt", {
type: "text/plain",
});

我就试了一下改成数组,没想到就可以了

1
const data = new File([slice], 'test.txt')

原来他对bits的描述非常容易让人误解,看英文版更容易理解,然后我把他的翻译更改了一下改成了

一个包含ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的 Array — 或者任何这些对象的组合。这是 UTF-8 编码的文件内容。

现在大家在MDN看到的应该是我更改后的版本了。
到此问题就算是解决了。直到写这篇文章的时候我才发现File对象从Blob接口继承了Blob.slice的方法,其实可以直接拿File来切割,我把他转成了ArrayBuffer然后切割然后转回成File,这么看来非常没必要。如果有同样需求的同学可以直接拿File切割。

写在最后

这次上传文件的需求折磨了我几天的时间,无论是去了解二进制文件的转换还是切割上传文件还是掉在后端的bug里都花费了很长的时间,其实现在写文章的时候看来,这个需求也不过如此,无非就是用一下关于文件的一些API,其实最开始主要就是没有研究过这方面的API,导致之前除了很多问题,所以感觉自己的知识储备还是需要加强的。

其次还有一个大问题是没有及时的和后端进行沟通,首先是没有和后端确认接口的格式,没有确定准确的需求,还有是出现问题只是自己埋头找答案没有去问问是不是后端那的问题,应该和后端沟通一下,这样会更快地找到问题的原因。

参考资料

File

File.File()

FileReader

readAsArrayBuffer

前端大文件上传