在项目中,我们平常会遇到这样的需求场景:测试:你这个上传文件怎么传不呀?整个页面卡住没反应!!! 遇到这样的问题,是上传了大文件,前后端没有做大文件切片处理。下面来说一下大文件切片上传,断点续传具体实现步骤!
流程图
第一步: 在自定义上传组件中,对上传的文件进行MD5计算
- 安装下载 npm i spark-md5 -S 库
- 组件:文件上传组件,进度条组件。 采用上传文件组件对文件进行处理,进度条参数 percent 表示一个文件上传的进度。
<a-upload
:file-list="fileList"
:customRequest="handleUpload"
:remove="handleRemove"
>
<a-button> <a-icon type="upload" />上传文件</a-button>
</a-upload>
<a-progress :percent="percent" status="active" />
- 将hanndleUpload的回调文件file,进行MD5加密处理,默认切片chunkSize大小为20M。计算MD5文件大小也需要花费时间,文件越大计算时间越长。这里我设置占进度条的50%
function md5(file, chunkSize = 20 * 1024 * 1024) {
const _this = this;
return new Promise((resolve, reject) => {
const startMs = new Date().getTime();
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer(); // 追加数组缓冲区。
const fileReader = new FileReader(); // 读取文件
fileReader.onload = function (e) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
const md5 = spark.end(); // 完成md5的计算,返回十六进制结果。
console.log('文件md5计算结束,总耗时:', (new Date().getTime() - startMs) / 1000, 's');
resolve(md5);
}
};
fileReader.onerror = function (e) {
reject(e);
};
function loadNext() {
console.log('当前part number:', currentChunk, '总块数:', chunks);
_this.percent = Math.ceil((50 / chunks) * (currentChunk + 1)); //进度条
const start = currentChunk * chunkSize;
let end = start + chunkSize;
end > file.size && (end = file.size);
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
},
第二步:调用接口校验文件的MD5,查询是否存在名称为 md5 的文件夹,如果存在,就直接返回已上传的信息,通过后端返回的finished判断是否已经上传完成,这里就包括如有一部分切片上传失败,则返回给前端已经上传成功的切片name,前根据返回的切片,计算出未上传成功的剩余切片,然后把剩余的切片继续上传,即可实现"断点续传"。 不存在就调接口初始化一个上传任务。
// 2.校验文件的MD5,后台查询是否存在名称为 md5 的文件夹
// 初始化上传任务
async getTaskInfo(file) {
// 1.生成文件md5值
const identifier = await this.md5(file);
// 2.校验文件的MD5,后台查询是否存在名称为 md5 的文件夹
const res = await taskInfo(identifier);
if (!res) {
// 如果不存在,则上传文件任务
const initTaskData = {
identifier,
fileName: file.name,
totalSize: file.size,
chunkSize: 20 * 1024 * 1024,
};
const res = await creatSlice(initTaskData);
if (!res) {
this.$message.error('文件上传错误');
return null;
}
return res;
}
return res;
// 得到上传的结果
},
第三步: 查询取每个分片的预签名上传地址。对返回的签名地址,通过PUT文件流的方式对文件切片上传
handleUploadFile(file, taskRecord, options) {
let lastUploadedSize = 0; // 上次断点续传时上传的总大小
let uploadedSize = 0; // 已上传的大小
const totalSize = file.size || 0; // 文件总大小
const startMs = new Date().getTime(); // 开始上传的时间
const { exitPartList, chunkSize, chunkNum, fileIdentifier } = taskRecord;
// 获取从开始上传到现在的平均速度(byte/s)
const getSpeed = () => {
// 已上传的总大小 - 上次上传的总大小(断点续传)= 本次上传的总大小(byte)
const intervalSize = uploadedSize - lastUploadedSize;
const nowMs = new Date().getTime();
// 时间间隔(s)
const intervalTime = (nowMs - startMs) / 1000;
return intervalSize / intervalTime;
};
const uploadNext = async (partNumber) => {
const start = Number(chunkSize) * (partNumber - 1);
const end = start + Number(chunkSize);
const blob = file.slice(start, end);
const res = await getPartUrl({ identifier: fileIdentifier, partNumber: partNumber });
if (res) {
await axios.request({
url: res,
method: 'PUT',
data: blob,
headers: { 'Content-Type': 'application/octet-stream' },
});
return Promise.resolve({ partNumber: partNumber, uploadedSize: blob.size });
}
return this.$message.error(`分片${partNumber}, 获取上传地址失败`);
};
第四步:更新上传进度条,并对多个上次接口请求进行处理。这里我采用的是npm i promise-queue-plus -S 对切片请求进行promise队列等待处理,依次按照切片上传顺序执行。
* 更新上传进度
* @param increment 为已上传的进度增加的字节量
*/
const updateProcess = (increment) => {
increment = Number(increment);
const { onProgress } = options;
const factor = 1000; // 每次增加1000 byte
let from = 0;
// 通过循环一点一点的增加进度
while (from <= increment) {
from += factor;
uploadedSize += factor;
const percent = Math.round((uploadedSize / totalSize) * 100);
this.percent = Math.ceil(percent / 2 + 50); // 算入md5计算时间
onProgress({ percent: percent });
}
const speed = getSpeed();
const remainingTime = speed !== 0 ? Math.ceil((totalSize - uploadedSize) / speed) + 's' : '未知';
console.log('剩余大小:', (totalSize - uploadedSize) / 1024 / 1024, 'mb');
console.log('当前速度:', (speed / 1024 / 1024).toFixed(2), 'mbps');
console.log('预计完成:', remainingTime);
};
return new Promise((resolve) => {
const failArr = [];
const queue = Queue(5, {
retry: 3,
retryIsJump: false,
workReject: function (reason, queue) {
failArr.push(reason);
},
queueEnd: function (queue) {
resolve(failArr);
},
});
this.fileUploadChunkQueue[file.uid] = queue;
for (let partNumber = 1; partNumber <= chunkNum; partNumber++) {
const exitPart = (exitPartList || []).find((exitPart) => exitPart.partNumber === partNumber);
if (exitPart) {
// 分片已上传完成,累计到上传完成的总额中,同时记录一下上次断点上传的大小,用于计算上传速度
lastUploadedSize += exitPart.size;
updateProcess(exitPart.size);
} else {
queue.push(() =>
uploadNext(partNumber).then((res) => {
// 单片文件上传完成再更新上传进度
console.log(res);
updateProcess(res.uploadedSize);
}),
);
}
}
if (queue.getLength() === 0) {
// 所有分片都上传完,但未合并,直接return出去,进行合并操作
resolve(failArr);
return;
}
queue.start();
});
第五步:全部切片上传完毕,调用接口,通知后端进行切片合并操作。并拿到文件上传完成的地址
const res = await mergeSlice(identifier); // identifier MD5值