彻底搞懂大文件分片上传的实现
学习AbortController
AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。
可以通过这个来实现取消或者中断请求的功能。
axios.abort();
底层实现就是这个。
认识了
fse = require('fs-extra')
概述
实现文件分片上传的原理就是通过将文件的ArrayBlob形式,通过file(blob)的方法slice,来实现将文件拆成几个部分,然后排上顺序传到服务端,最后传完了调用服务端的merge合并接口,服务端将文件合并。
服务端实现原理,上传前创建文件夹,命名注意了,可以参考给到的代码。然后将获得的文件通过fse的写入方法,写入到我们创建的文件夹中,并且有相应的排序。最后我们通过合并方法将文件合并到一个文件。
详细学习
Client端
getChunkListAndFileMd5函数
这个函数用来创建分片数组,以及生成Hash签名。
创建分片列表数组,我们使用的方法是file.prototype.slice
当然这里我们为了解决兼容性问题,我们使用的代码是:
export function getBlobSlice() {
return (File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice);
}
创建分片:getBlobSlice.call(file,start,end)
。这里start、end分别是起始位置和终点位置,我们确认好分片大小就可以计算start和end。
好,那么我们开始了,我们定义size是 5M 代码如下
const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
我们定义一个对象保存默认配置:
const DEFAULT_OPTIONS = {
chunkSize: DEFAULT_CHUNK_SIZE,
};
我们来定义分片的编号,初始值肯定是 0。我们定义chunkSize来保存我们上边定义的常量分片大小。
let currentChunk = 0;
const chunkSize = this.fileUploaderClientOptions.chunkSize;
接下来我们需要计算需要多少个分片,很简单:文件总的大小/每个分片的大小,最终结果可能是个小数,但是为了保证分片的完整肯定是向上取整。
const chunks = Math.ceil(file.size / chunkSize);
定义一个函数来加载分片,代码如下:
function loadNextChunk() {
const start = currentChunk * chunkSize;
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
const chunk = blobSlice.call(file, start, end);
chunkList.push(chunk);
fileReader.readAsArrayBuffer(chunk);
}
拆解这个函数:
-
计算start位置,这个不难理解。
-
计算end位置,这里需要判断一下,主要是针对两种情况:
- 文件尺寸很小,小于分片大小,直接取文件的尺寸。
- 最后一个切片,大小可能没有切片尺寸大,我们直接取文件大小的位置。
3.调用我们上边说的函数来实现分片,并且获取当前的分片。 4.将当前的分片push到我们的数组中。 5.调用fileReader.readAsArrayBuffer方法来读取分片。(其实当前的分片是一个blob实例,我们通过这个方法可以读取到里边的内容)。
接下来我们来看看fileReader是怎么回事。
认识FileReader
不懂fileReader的可以先看看文档,了解下里边的方法。
FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
其中 File 对象可以是
- 来自用户在一个 元素上选择文件后返回的FileList对象
- 也可以来自拖放操作生成的 DataTransfer对象,
- 还可以是来自在一个HTMLCanvasElement上执行mozGetAsFile()方法后返回结果。
FileReader可以在Web Worker中使用。 (重要,可以很大成都解决性能问题)
这里因为用到了FileReader的方法,所以重点讲讲这个对象的方法。
- FileReader.abort(); 中止读取操作。
- FileReader.readAsArrayBuffer(); 开始读取指定的 Blob中的内容,一旦完成,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象。
- FileReader.readAsDataURL(); 开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL 格式的 Base64 字符串以表示所读取文件的内容。
- FileReader.readAsText();开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个字符串以表示所读取的文件内容。
我们目前需要读取分片的内容,并且需要ArrayBuffer的格式,所以我们创建一个FileReader对象实例,并且使用readAsArrayBuffer方法来读取文件,通过onload事件来监听,获取最终结果。代码如下:
const fileReader = new FileReader();
fileReader.onload = function (e) {
// 我们读取到的结果是 e.target.result
}
fileReader.onerror = function (e) {
// 这里表示读取失败
}
上边loadNextChunk函数中调用了这个方法。
fileReader.readAsArrayBuffer(chunk);
使用md5来实现签名
用到了 spark-md5 这个三方库。主要使用的方法是生成md5的编码。
npm install --save spark-md5
这个就是一个处理分片的一个库。给出了方法:
const spark = new SparkMD5.ArrayBuffer();
将分片 v 添加到对象中
spark.append(v)
最终结果返回
const result = spark.end()
uploadFile函数
这个函数用来上传文件的函数。
首先我们要获取上边我们函数得到的值 md5、chunkList。
const { md5, chunkList } = yield this.getChunkListAndFileMd5(file);
上传文件前,需要调用接口来初始化文件分片上传,这里我们其实就是调用初始化文件分片上传的接口requestOptions.initFilePartUploadFunc
。
yield requestOptions.initFilePartUploadFunc();
这个接口在后端的实现其实就是创建好一个文件夹用来保存分片,这里就不多说了,之后在学习Server代码的时候我们细讲。
接下来就是开始上传我们的分片了,上传方法requestOptions.uploadPartFileFunc
:
for (let index = 0; index < chunkList.length; index++) {
try {
yield requestOptions.uploadPartFileFunc(chunkList[index], index);
}
catch (e) {
console.warn(`${index} part upload failed`);
retryList.push(index);
}
}
注意了:我们不能保证全部顺利上传,如果中间出现问题中断了上传等问题,我们如何处理? 这里我们使用retryTimes来获取需要重新上传的列表。
for (let retry = 0; retry < requestOptions.retryTimes; retry++) {
if (retryList.length > 0) {
console.log(`retry start, times: ${retry}`);
for (let a = 0; a < retryList.length; a++) {
const blobIndex = retryList[a];
try {
yield requestOptions.uploadPartFileFunc(chunkList[blobIndex], blobIndex);
retryList.splice(a, 1);
}
catch (e) {
console.warn(`${blobIndex} part retry upload failed, times: ${retry}`);
}
}
}
}
最后我们调用上传结束的接口requestOptions.finishFilePartUploadFunc(md5)
,其实这个接口主要是通知服务端,分片都上传完了,服务端可以进行文件合并了,最终将分片合并成一个文件。
if (retryList.length === 0) {
return yield requestOptions.finishFilePartUploadFunc(md5);
}
else {
throw Error(`upload failed, some chunks upload failed: ${JSON.stringify(retryList)}`);
}
至此,客户端的操作完毕!
转载自:https://juejin.cn/post/7238826935529439289