大文件上传
大文件使用一次请求发送的话,那么这个请求的时间就会非常的长。一旦请求的过程中出现一些问题,比如网络断开了,那么你就不得不把整个文件重新上传一遍。这样的代价是用户接受不了的,也非常的浪费资源。所以我们在做大文件上传的时候,我们往往会对文件进行分片。
分片原理
在客户端,首先把整个的大文件数据,分成一个一个的数据小块,你可以把每一块想象成为单独的一个小文件,然后利用单文件上传把这些小文件依次传到服务器,当最后把文件全部传输完成之后在服务器端使用程序把整个文件的小数据组装起来,形成一个完成的文件。这个组装由后端完成!
在这个过程中前端要做到最核心的就是要把文件进行分片,分成一个一个的小块。所以大文件的上传的第一个核心技术点就是文件如何来分片。
举例分析
// splitFile.html
<html>
<head> </head>
<body>
<input type="file"/>
<script>
const inp = document.querySelector('input');
inp.onchange = (e) => {
const file = inp.files[0];
if(!file) return;
console.log(file);
}
</script>
</body>
</html>
在浏览器中打开这个例子,这里有一个input
元素,然后监听它的onchange
事件,拿到它选择的文件。选择一个文件,我们得到的是一个File
对象
接下来我们对这个File对象进行切片,这个对象里面有一个函数叫做slice
,它跟数组的slice一个用法,它的起始和结束就表示说从这个文件第多少个字节开始取,取到第多少个字节。file.slice(0,100)
表示取该文件的0-99个字节,得到一个文件的切片数据。修改onchange方法如下:
const file = inp.files[0];
if(!file) return;
// 0-99个字节
const piece = file.slice(0,100);
console.log(piece);
打印结果我们发现分片后的数据是一个Blob
类型,我们知道Blob
类型它也是表示文件数据的,也就是说用ajax请求的时候可以直接把它发送到服务器。它的用法和File
对象的用法是一样的。
有了slice
函数我们就可以非常轻松的完成切片,我们写一个工具函数,并在拿到File对象后使用该函数进行分割如下:
<html>
<head></head>
<body>
<input type="file"/>
<script>
const inp = document.querySelector('input');
inp.onchange = (e) => {
const file = inp.files[0];
if(!file) return;
//这里一次分割50k,获取到分片结果
const chunks = createChunks(file, 50 *1024);
console.log(chunks);
}
/*
file:File对象,
chunkSize: 切片的大小
*/
function createChunks(file, chunkSize) {
const result = [];
for (let i = 0; i < file.size; i+=chunkSize) {
result.push(file.slice(i, i + chunkSize));
}
return result;
}
</script>
</body>
</html>
打印一下,我们已经获取到了分片的结果是一个Blob
数组。分片是很快的,原因是File
或Blob
对象其实只是保存了文件的基本信息,比如说File对象文件有多大、文件是什么类型、文件的名字和位置等这些基本信息。它并没有保存文件的数据。Blob也是一样的,它保存了数据的大小和类型。所以分片其实就是一个简单的数学运算。
到这里分片就结束了,我们真正读取数据的时候需要利用FileReader
才能真正的把它们的数据读出来。
断点续传
有这样一个场景,我们在分片上传的时候网络不好或断网了,我们下一次要接着上传之前上传过的分片,之前上传过的分片就不用再上传了。这种文件秒传是怎么操作的呢?看一下它的基本原理:
它其实就是跟服务器的一次对话,重新上传同一个文件时会先访问一下服务器,告诉服务器我要给你上传一个文件,你告诉我一下这个文件我上传过了没有,还有哪些分片还没有上传。服务器会返回对应的结果,告诉客户端之前已经上传过了哪些分片了,还有哪些编号的分片还需要传递。
通过一次Ajax请求,客户端就能够知道这个文件我该如何处理,那么在这个交互过程中客户端必须要告诉服务器一个关键的信息,一个能够唯一代表这个文件的东西就是文件的hash值。hash是一种算法,它可以把任何数据换算成一个固定长度的字符串,这个字符串是不可逆的。
所以在上传文件的时候需要生成一个hash值,并在服务器记录一下,在下一次重传的时候我再告诉你这个hash值之前有没有传过,还有哪些分片需要上传,通过这个hash值就能够代表整个的文件内容。这里我们使用md5作为它的hash算法,接下来的关键点就是我们如何在客户端计算出这个文件的hash值,我们可以使用一个第三方库spark-md5
来做这个事。
<html>
<head> </head>
<body>
<input type="file"/>
<script src="./spark-md5.js"></script>
<script>
const inp = document.querySelector('input');
inp.onchange = (e) => {
const file = inp.files[0];
if(!file) return;
//这里一次分割50k,获取到分片结果
const chunks = createChunks(file, 50 *1024);
hash(chunks);
}
/*
生成文件hsah值,
为了防止文件过大,采用分块增量算法
*/
function hash(chunks) {
const spark = new SparkMD5();
// 递归函数
function _read(i) {
if(i >= chunks.length) {
console.log(spark.end());
return; //读取完成
}
const blob = chunks[i];
const reader = new FileReader();
reader.onload = e => {
// 读取到的字节数组
const bytes = e.target.result;
spark.append(bytes);
_read(i+1);
}
reader.readAsArrayBuffer(blob);
}
_read(0);
}
/*
file:File对象,
chunkSize: 切片的大小
*/
function createChunks(file, chunkSize) {
const result = [];
for (let i = 0; i < file.size; i+=chunkSize) {
result.push(file.slice(i, i + chunkSize));
}
return result;
}
</script>
</body>
</html>
上面代码中,为了防止文件过大,我们采用分块增量算法来获取hash值。修改代码选择文件,我们就可以打印这个文件的hash值了。如果文件过大计算量也将变大,就没切片时那么的快了。
接下来我们把hash
函数封装成一个异步函数,让它返回一个Promise
:
<html>
<head></head>
<body>
<input type="file" />
<script src="./spark-md5.js"></script>
<script>
const inp = document.querySelector('input');
inp.onchange = async (e) => {
const file = inp.files[0];
if (!file) return;
//这里一次分割50k,获取到分片结果
const chunks = createChunks(file, 50 * 1024);
const result = await hash(chunks);
console.log(result);
}
/*
生成文件hsah值,
为了防止文件过大,采用分块增量算法
*/
function hash(chunks) {
return new Promise((resolve) => {
const spark = new SparkMD5();
// 递归函数
function _read(i) {
if (i >= chunks.length) {
resolve(spark.end());
return; //读取完成
}
const blob = chunks[i];
const reader = new FileReader();
reader.onload = e => {
// 读取到的字节数组
const bytes = e.target.result;
spark.append(bytes);
_read(i + 1);
}
reader.readAsArrayBuffer(blob);
}
_read(0);
})
}
/*
file:File对象,
chunkSize: 切片的大小
*/
function createChunks(file, chunkSize) {
const result = [];
for (let i = 0; i < file.size; i += chunkSize) {
// slice越界则取数据最大长度
result.push(file.slice(i, i + chunkSize));
}
return result;
}
</script>
</body>
</html>
为了防止主线程卡死,我们一般不会放到主线程里的,我们可以利用web worker单独去开一个线程。因为这个操作时CPU密集型任务,如果放到单独线程还是卡顿,可能是特别大的文件造成的,这个时候可以先粗略的对这个文件分成一些大块,单独去计算每个大块,比如每个大块有300M的数据量,将这个大块再分小块,这样计算它的hash是非常快的。当有空闲的时候再去慢慢计算后边的hash值,因为后边的hash值还不着急用。