需求
1、解决项目开发的时候,遇到超大附件上传,需要上传的时间很久,网络抖动时,上传的到一半被迫终止,需要重新上传。
2、超大文件上传,整个文件上传需要很久的时间,把文件切片,整理成小文件上传,从而节约时间。
实现思路
1、第一步结合后端,获取当前附件的标识。(次步鄹调用后端接口,后端返回标识即可)。
2、获取到标识之后,开始分割文件,放在一个数组里面。
3、调用接口传递向后端传递单片文件(需要注意并发请求,和单片文件失败之后的处理方式,如果后端需要,还要计算单片文件的MD5)。
4、调用后端合并的接口,文件上传成功。
本次的重点在于第3步,如何处理并发请求,失败重发,控制并发请求接口的个数,控制上传失败之后的逻辑。
重点步鄹
前期准备
js
需要确定的变量
chunksize: 5* 1024* 1024 //单片文件的大小
errorNumber: 5 // 文件上传失败重试的次数
actionNumber: 5 // 并发请求接口数
2 分割文件
js
把整个文件切割成为单片文件,因为是文件流不可以使用forEach,要使用原生的for循环
/****
data: 附件信息
res: 第一步后端的标识
fileChunkdeList: 切片的数组
****/
fileSlice(deta, res, fileChunkdeList) {
if(!data) return
// 获取分片的个数,注意:向上取整
data.total = Math.ceil(data.file.size/ this.chunkSize)
for(let i =0; i < data.total; i++){
let tmp
if(i === 0){
tmp = data.file.slice(i, Math.min(i + this.chunkSize, data.file.size))
} else {
tmp = data.file.slice(i * this.chunkSize, Math.min(i * this.chunkSize + this.chunkSize, data.file.size))
}
let obj = {
file: tmp,
filesIndex: i + 1
}
fileChunkdeList.push(obj)
}
//这一步是给合并的接口准备参数
data.comPleteMultipartUploadParms = this.getcomPleteMultipartUploadParms(data, res)
// 切片成功之后,调用单片文件上传的接口 第3步操作的函数
this.fileSliceAction(data, fileChunkdeList, res).then(res => {
if(上传失败的条件){
整理失败的逻辑: 是否要出现重启按钮啊,页面展示啊,页面提示啊等...
}
})
}
}
疑惑:
既然是把大文件切片分割成为小文件上传,为什么不在切割文件的时候,就直接调用上传单个文件的接口呢?
直接调用上传单个文件的接口就已经可以实现大附件切割小附件,已经可以起到加速上传文件的效果了。
解答: 直接调用接口会造成一个问题,不能控制并发请求接口数量,会造成两个极端。
1、并发请求全部的数据,就是有几个单片文件,就同时发送几个请求,这肯定不行。
2、我可以添加 async 把接口请求设置成为同步的请求,这样就是一个请求结束,在发送下一个请求,可以请求上一个问题,但是这个方法并不是最优解。
3、并发请求,失败重发
js
/***
arr : 切片之后的数组fileChunkdeList,
callback: 所有单片文件上传成功之后的文件合并接口回调,
data:附件信息,
res :第一步后端返回的附件标识 ,
num : 记录失败重试的次数,为后续的失败重发,断点续传做准备
****/
异步并发控制,同一时间点有5个任务在同时请求
sendRequest(arr, callback, data, res, num){
let fetchArr = [] // 正在请求的数据
let toFetch = () => {
// 如果异步任务都已经开始执行,剩下最后一组,则结束并发控制
if(!arr.length){
return Promise.resolve()
}
const chunItem = arr.shift()
// 执行异步任务
let it = this.fileSliceOnlyAction(chunItem, data, res, a)
// 添加异步事件的完成处理
it.then((res) => {
// 接口上传成功
if(res.code === 200){
// 有一次上传成功,就把失败的计数重置为0
num = 0
fetchArr.splice(fetchArr,indexOf(it), 1) // 成功从任务列队中删除
} else {
arr.unshift(chunItem) // 失败之后重新放回总队列中
}
}).catch(e => {
arr.unshift(chunItem) // 失败之后重新放回总队列中
})
// 添加新的任务
fetchArr.push(it)
let p = Promise.resolve()
if(fetchArr.length >= this.actionNumber) { // 如果并发达到最大数,则等其中的一个异步任务完成再添加
p = Promise.race(fetchArr)
}
return p.then(() => toFetch()) // 执行递归
}
toFetch().then(() => {
Promise.all(fetchArr).then((e) => {
// 最后一组全部执行完在执行回调函数,只是当前的5片数据全部上传完成之后的回调 就会进行合并接口
callback(data, res) // 调用合并接口
}).catch((e) => { // 断网失败 或者接口失败
// 可以记录失败的次数,判断是否到达上传失败重试次数 errorNumber, 是否重新执行调用上传函数
})
}).catch((e) => { // 断网失败 或者接口失败
// 可以记录失败的次数,判断是否到达上传失败重试次数 errorNumber, 是否重新执行调用上传函数
})
}
单片上传的接口
js
fileSliceOnlyAction(){
// 准备参数
// 参数里面可能会有 hash 计算
// 调用后端接口
//callback 封装在 axios里面 获取文件上传的进度
return postFile (this.url.part, 参数,callback, true, data, a*chunkSize)
}
// hash 计算方法
computeMD5(data){
return new Promise((resolve, reject) => {
let fileReader = new FileReader()
let spark = new SparkMD5() // SparkMD5 是一个插件 spark-md5
fileReader.readAsBinaryString(data)
fileReader.onload = e => {
spark.appendBinary(e.target.result, 16)
const md5 = spark.end()
resolve(md5)
}
})
}