项目中有涉及到素材上传的功能,小的文件上传已经不能满足当前的需求。可能会存在1-5GB的文件,假如依旧采取原方式,接口和服务端会出现超时情况。那么此时就可以考虑采用分片上传的方式了,当然也建议你通过第三方上传以生成URL的方式解决功能要求。
实现思路
- 调用后端的分片初始化接口,后端根据文件名生成唯一标识
uploadId(用于后续合并文件); - 前端分片处理文件,并调用后端分片接口,把分好的文件通过索引(
partNumber)+标识(uploadId)+切片文件(file)的方式给到后端; - 最终调用后端合并接口,以最终返回的合并文件路径为准;
技术实现
- 前端:vue2,element-ui
- 后端:java,阿里云oss分片上传
- 前端项目地址
文件分片
- 获取文件需要分片的总数
CHUNK_SIZE: 5 * 1024 * 1024, // 5MB一片
context.chunkCount = Math.ceil(file.size / this.CHUNK_SIZE) // 总片数
- 基于获得的
chunkCount总片数和chunkSize每片大小,进行for循环,切割文件
// 获取当前chunk分片数据
getChunkInfo(context, index) {
const { file, chunkSize } = context
const start = index * chunkSize
const end = Math.min(file.size, start + chunkSize)
const chunk = file.slice(start, end)
return chunk
}
多请求并发
假设文件分为了100片,同时请求100个接口肯定会影响性能,因此这里限制了最大并发数量为6,通过Promise建立执行队列,超出这个数量时会在队列中等待执行。
MAX_REQUEST: 6 // 最大并发请求数量,超出的请求会被放入队列中
// 临时存放并发请求的列表
const requestsList = [...context.chunkList]
// 通过 while 解决最大并发请求问题
// 根据第一次选择的fileList的文件数量来平分MAX_REQUEST
while (requestsList.length) {
const requests = requestsList.splice(0, Math.ceil(this.MAX_REQUEST / this.uploadFilesList.length))
await Promise.all(requests.map((item) => this.uploadChunk(context, item.index)))
}
上传进度
这里还是通过axios提供的前端上传进度监听onUploadProgress的方式实现,即便这种方式还是要依赖于最终后端服务的返回,但却能更简单的解决我当下问题。
你也可以采取其它方案。(例如:根据后端接口实际成功响应,分段展示上传的百分比进度)
// 上传分片
async uploadChunk(context, index) {
...
this.$http({
url: `xxxx?uploadId=${uploadId}&partNumber=${partNumber}`,
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' },
cancelToken: cancelTokenSource.token, // 取消令牌
onUploadProgress: (e) => {
// 记录每个分片的上传进度
context.chunkList[index].loaded = e.loaded
// 计算总进度 汇总每个分片的上传进度/总大小
e.totalProgress = (context.chunkList.reduce((prev, cur) => prev + cur.loaded, 0) / totalSize) * 100
// 限制最大进度为99.9,最终上传成功以后端接口返回为准
e.totalProgress = Math.min(e.totalProgress, 99.9)
this.onProgressUpload(e, file)
}
})
...
},
取消上传
借助axios的cancelTokenSource取消令牌,来及时阻断请求的上传,避免资源浪费。
首次会初始化,后面基于当前文件,都会放在uploadChunk每个分片上传中,以便接口异常或手动取消上传时,统一取消。
context.cancelTokenSource = axios.CancelToken.source() // 创建一个axios的cancelTokenSource(取消令牌)
...
catch (error) {
cancelTokenSource.cancel('上传失败')
console.error('uploadByPieces error', e)
}
// 用户手动删除取消
handleDel(index) {
this.$confirm('是否删除该文件?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.uploadFilesList[index]?.cancelTokenSource.cancel('取消上传')
this.uploadFilesList.splice(index, 1)
})
}