问题分析
文件过大一次性上传如果中断,只能重来,体验极差。
如果已经上传过,再次上传,浪费存储空间,浪费用户时间。
如此,我们需要做一个支持断点续传,同时支持秒传的方法。
实现思路
后端使用md5标记每一个上传文件,每次上传文件时,前端计算文件md5,然后计算文件分片数,请求后端,如果存在,则秒传。
如果不存则返回当前md5文件上传所有分片的上传地址列表(url带一次性上传权限,防止有人利用恶意利用上传文件接口占用空间),以及如果文件上传过,没上传完,则返回应该从第几个分片继续上传。
然后开启分片上传的并发控制,上传完毕,进行文件合并。
代码参考
选择文件
<input @change='addFile' type="file" >
let chunkSize=6*1024*1024//大于6MB的分片上传
addFile(event) {
let file = event.target.files[0]
let size = file.size;
this.loading = true
if(file.size>chunkSize){
//如果大于基本分片单位大小,分片上传
this.uploadFile(file,event,(str)=>{
this.uploadMessage=str
})
}else{
//小于基本分片单位大小,直接上传即可,也无需合并
let param = new FormData();
param.append('file', file);
utilsApi.postMinio(param).then(res => { //上传成功
this.loading = false
}).catch(e => {
this.loading=false
})
}
}
MD5计算
这里使用的依赖:"spark-md5": "^3.0.1"
/**
* 获取文件MD5
* @param file
* @returns {Promise<unknown>}
*/
import SparkMD5 from 'spark-md5'
getFileMd5(file){
let fileReader = new FileReader()
fileReader.readAsBinaryString(file)
let spark = new SparkMD5()
return new Promise((resolve) => {
fileReader.onload = (e) => {
spark.appendBinary(e.target.result)
resolve(spark.end())
}
})
}
分片和判断上传类型
uploadFile(file,callback){
//文件大小
const fileSize = file.size
//计算当前选择文件需要的分片数量
let chunkCount = Math.ceil(fileSize / chunkSize)-1
chunkCount=chunkCount>0?chunkCount:1
// console.log("文件大小:",(file.size / 1024 / 1024) + "Mb","分片数:",chunkCount)
let that=this
//获取文件md5
this.getFileMd5(file).then(fileMd5=>{
// console.log("文件md5:",fileMd5)
// console.log("向后端请求本次分片上传初始化")
//向后端请求本次分片上传初始化
const initUploadParams = JSON.stringify({chunkCount: chunkCount,fileMd5: fileMd5})
utilsApi.sliceUpload(initUploadParams).then(res=>{
this.count=0
if (res.data == null) {
// console.log("当前文件上传情况:所有分片已在之前上传完成,仅需合并")
this.composeFile(fileMd5,file.name,event)
return;
}
if (res.msg == "上传完成") {
console.log("file url:"+res.data) // console.log("当前文件上传情况:秒传")
this.loading=false
return
}
const chunkUploadUrls = res.data
//正常开始并发上传
this.multiRequest(chunkUploadUrls,5,file,fileMd5).then(val=>{
// 上传完毕合并分片
this.composeFile(fileMd5,file.name,event)
}).catch(err=>{ that.loading=false
})
}).catch(val=>{
that.loading=false
})
}).catch(err=>{
that.loading=false
})
},
并发请求控制
//并发控制
multiRequest(chunkUploadUrls = [], maxNum,file,fileMd5) {
// 请求总数量
const len = chunkUploadUrls.length;
// 根据请求数量创建一个数组来保存请求的结果
const result = new Array(len).fill(false);
// 当前完成的数量
let count = 0;
// console.log("开始promise")
let that=this
let finish=0//完成数量
return new Promise((resolve, reject) => {
// 请求maxNum个
while (count < maxNum) {
next();
}
//递归和promise控制并发请求数量,启动maxNum个请求,每完成一个启动一个新的
//直到所有的请求完毕,然后请求合并接口
function next(){
try{
let ind = count++;
// 处理边界条件
if (ind >= len ) {
// console.log("完成了")
// 请求全部完成就将promise置为成功状态, 然后将result作为promise值返回
!result.includes(false) && resolve(result);
return;
}
// console.log(`开始 ${ind}`, new Date().toLocaleString());
let item=chunkUploadUrls[ind]
//分片开始位置
let start = (item.partNumber - 1) * chunkSize
//分片结束位置
let end = 0
if(ind==chunkUploadUrls.length-1){
end=file.size
}else{
end =start + chunkSize
}
//取文件指定范围内的byte,从而得到分片数据
let _chunkFile = file.slice(start, end)
// console.log("end-start:",end-start)
let last=0
let param = new FormData(); //创建form对象
let url=item.url
param.append('key',fileMd5+"-"+(ind+1)+".chunk");
param.append('file', _chunkFile);
utilsApi.postMinio({file:param,url:url }).then(res=>{
finish++
// console.log("第" + that.count+ "个分片上传完成")
// that.uploadMessage="上传"+ (that.count/ chunkUploadUrls.length * 100 | 0) + '%'
result[ind] = true;
// console.log(`完成 ${ind}`, new Date().toLocaleString());
// 请求没有全部完成, 就递归
if (ind < len) {
next();
}
}).catch(err=>{
reject("上传中断")
})
}catch(err){
//异常中断
reject(err)
}
}
});