关于大文件上传的实现细节,网上已经有很多讲的细致清楚的教程以及代码可供参考。正所谓实践出真知,因此,本文的目的仅为了记录笔者开发工作中功能的逐渐实现和解决其中遇到的一些问题。如有错误,请大佬们不吝赐教!
文件分片、合并、删除及断点续传
对文件分片即是调用File对象的slice
方法。在下图1上进行切片,笔者切片时文件对象如下图2,因此是对File的raw属性进行切片并得到Blob数据,然后调用后端接口进行上传。上传之后将这些切片进行合并删除切片(调用合并切口若返回数组说明有切片未上传成功,需要重新上传)。
话不多说,直接上代码:
const state = reactive({
hash: '',
})
const chunkSize = 1024 * 1024 * 5; // 每块切片5MB
const fileChunks = [];
for (let i = 0; i < files.size; i += chunkSize) {
const chunk = files.raw.slice(i, i + chunkSize); // files.raw是我们要切片的原始数据
fileChunks.push(chunk);
}
const calculateFileHash = (fileChunks) => { // 使用第三方库spsrk-md5计算文件的hash值
return new Promise((resolve) => {
// 之前使用new SparkMD5()时有小bug:同一个文件内容变化后计算得到的hash值不变
const spark = new SparkMD5.ArrayBuffer(); // 解决办法
function _read(i) {
if (i >= fileChunks.length) return;
const reader = new FileReader();
reader.readAsArrayBuffer(fileChunks[i]);
reader.onload = (e) => {
spark.append(e.target.result);
i < fileChunks.length - 1 ? _read(i + 1) : resolve(spark.end());
};
}
_read(0);
});
};
let resSet = []; // 初始化需要重传的切片索引
const mapper = async (chunk, index) => { // mapper函数支持两个参数
if(!state.hash) {
// 由于是并发请求,因此第一批次的若干个请求都会进行hash的计算(即下述p-map控制并发数为3,这里就会进行3次文件hash的计算),此后请求不会计算
state.hash = await calculateFileHash(fileChunks);
}
const chunkHash = resSet.length ? `${state.hash}_${resSet.splice(0, 1)[0]}` : `${state.hash}_${index+1}`;
const fd = new FormData();
fd.append('file', chunk);
fd.append('chunkHash', chunkHash);
fd.append('mark', 1); // 后端接口参数标识,用来进行分片上传的不同场景
fd.append('markVal', xxx); // 即向id为xxx的数据集中传入文件
const result = await uploadReqeust(fd);
return result;
};
const merge = async (fileHash, fileName, filesPath, chunkNums) => { // 合并/删除切片
const res = await mergeFiles(fileHash, fileName, filesPath, chunkNums); // 调用合并接口
if(res.filePath) {
state.uploading = false;
const dir = res.filePath.slice(res.filePath.indexOf(arr[0]),res.filePath.length)
ctx.emit('uploadSuccess', dir);
deleteSliceFile(fileHash, filesPath)// 删除切片文件
.then((res) => state.hash = '')
.catch((err) => console.log(err))
return dir;
}
return res.unUploadFiles;
}
// 使用p-map(或p-limit等相关第三方库)控制上传并发数
// stopOnError: 默认为true,取false时不会因为一个请求失败而影响其它请求的进行
return pMap(fileChunks, mapper, { concurrency: 3, stopOnError: false })
.then(async (res) => {
const result = await merge(state.hash, files.name, res.filePath, chunkNums)
if(Array.isArray(result)) { // 断点续传
resSet = result;
const reUploadList = [];
for (const item of resSet) {
reUploadList.push(fileChunks[item-1]);
}
pMap(reUploadList, mapper, { concurrency: 3, stopOnError: false })
.then(async (res) => {
const result2 = await merge(state.hash, files.name, res.filePath, chunkNums)
})
}
})
.catch(errFn);