大文件上传解决方案

需求描述

大数量文件上传

  • 支持文件信息列表展示
  • 每个文件校验格式、大小等信息
  • 每个文件计算上传百分比、速度、剩余时间

大容量文件上传

  • 支持断点续传
  • 支持秒传
  • 支持取消

准备工作

上传文件夹

大数量的文件上传都是通过上传文件夹的形式上传的,应该不会有人手动去选上万个文件的吧~
so,首先需要用webkitdirectory来启动选择文件夹

1
<input type="file" webkitdirectory>

HTML5 File API

File API 提供了前端处理本地文件的能力,让图片预览、分块上传、拖拽上传等等神奇的操作变为可能。简单介绍一下我们会用到的部分,详细介绍看官方文档

  • FileList — input file标签获取到的值,是一个类数组,每个元素就是一个File对象。
  • File — FileList中的一个对象,包含文件的名称、大小、类别、修改时间等等基本信息。
  • FileReader — 文件读取的API,将文件读取到内存中,可以执行预览图片、计算MD5等等操作。
  • Blob — File对象就继承自Blob对象,二进制数据,提供操作接口,比如我们会用到的slice方法可以实现文件分块。

问题一:同时处理大量文件,浏览器直接崩溃?卡得要死?

由于某些原因我们项目采用的单个文件依次上传的方式,上传部分就很简单了,只需要控制上传请求的数量就可以了,难点主要是对文件格式、大小的校验。

获取到文件列表并逐一处理

如果我们直接用for循环取出每个文件进行处理,上万个文件同时加载可想而知,浏览器瞬间就投降了,这个时候就需要我们的Web Worker登场了,JavaScript主线程通过创建Worker线程,将一些计算密集型或高延迟型的任务交给Worker线程,充分发挥多核CPU的性能,避免主线程的阻塞和卡顿,下面简单介绍下如何使用,详细用法看Web Worker 使用教程 — 阮一峰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let worker = new Worker('work.js');
// 发送消息到work.js
worker.postMessage('Hello World');
// 接收work.js传出的消息
worker.onmessage = (e) => {
console.log(`Message:${e.data}`);
// do something...
};
// 捕获work.js中抛出的错误
worker.onerror = (error) => {
console.log(`Error:${error}`);
};
// 从主线程关闭worker
worker.terminate();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// worker.js
// 接收主线程传递的消息
self.onmessage = (e) => {
console.log(`Data:${e.data}`);
// 这是个例子,具体怎么处理自由发挥
switch (e.data.cmd) {
case 'start':
// 发送消息到主线程
self.postMessage('worker start');
// do something...
break;
case 'stop':
self.postMessage('worker stop');
// 从子线程关闭worker
self.close();
break;
default:
self.postMessage('Unknown command:');
}
};

Web Worker可以理解为一个数据处理程序,传递一些消息或者数据给它,它会根据你设定的条件在需要的时候发送一个消息给你,你只需要监听对应的事件然后进行处理就好了。引入Web Worker后同时处理成千上万个文件的校验就不会引起浏览器的卡顿或者崩溃了,如果操作比较复杂,还可以多启动几个Worker来处理。、

问题二:大文读取MD5导致浏览器崩溃?

我们项目最早使用的是JavaScript MD5(github传送门),用于一些小文件的md5计算还可以,但是用于几百M的大文件就会出现浏览器卡顿、崩溃等问题,而且耗时较长。各种google之后发现了SparkMD5(github传送门),SparkMD5是利用File API对文件进行分块之后进行的md5计算,效率很高,秒级运算而且不会引起浏览器的异常。示例代码:

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
// 校验MD5
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
let spark = new SparkMD5();
let fileReader = new FileReader();
const loadNext = () => {
let start = currentChunk * chunkSize;
let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
};
// fileReader.readAsBinaryString(file);
fileReader.onload = function(e) {
// console.log('read chunk nr', currentChunk + 1, 'of', chunks);
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
// console.log('finished loading');
// console.info('computed hash', spark.end());
// spark.end()返回的就是文件的md5值了
// 可以发送到服务器进行校验,从而实现秒传、续传等功能
}
};
fileReader.onerror = function() {
console.warn('oops, something went wrong.');
};

loadNext();

问题三:如何实现分块上传、续传、秒传?

有了md5以后这几个需求就很好实现了,md5相同的文件就可以认为是同一个文件,每个文件上传之前先发送文件的md5到服务器进行校验,服务器返回这个文件还有哪些分块缺失,前端再发送需要的分块到服务器即可。当然,前后端必须要提前约定分块大小才能保证最后能在后端拼凑成一个完整的文件。

  • 服务器返回缺失部分分块就是续传了
  • 服务器返回不缺失分块,就表示服务器上已经存在相同文件 ,前端显示秒传即可。

分块上传示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let chunkNum = 0; // 分块编号
let chunkSize = 10 * 1024 * 1024; // 分块大小10M
let chunks = Math.ceil(file.size / chunkSize); // 总分块数
const uploadChunk = (chunkNum) => {
if (chunkNum < chunks) {
let start = chunkNum * chunkSize;
let stop = Math.min((start + chunkSize), file.size);
let fileTip = file.slice(start, stop);
upload(fileTip).then(() => {
chunkNum++;
uploadChunk(chunkNum);
}).catch({
// 错误处理
});
} else {
// 上传完成
}
};
uploadChunk(chunkNum);
0%