前端实现大文件分片上传

629 阅读2分钟

项目中有涉及到素材上传的功能,小的文件上传已经不能满足当前的需求。可能会存在1-5GB的文件,假如依旧采取原方式,接口和服务端会出现超时情况。那么此时就可以考虑采用分片上传的方式了,当然也建议你通过第三方上传以生成URL的方式解决功能要求。

fenpian1.gif

实现思路

企业微信截图_17316376868031.png

  1. 调用后端的分片初始化接口,后端根据文件名生成唯一标识uploadId(用于后续合并文件);
  2. 前端分片处理文件,并调用后端分片接口,把分好的文件通过索引(partNumber)+标识(uploadId)+切片文件(file)的方式给到后端;
  3. 最终调用后端合并接口,以最终返回的合并文件路径为准;

技术实现

文件分片

  1. 获取文件需要分片的总数
CHUNK_SIZE: 5 * 1024 * 1024, // 5MB一片
context.chunkCount = Math.ceil(file.size / this.CHUNK_SIZE) // 总片数
  1. 基于获得的 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)))
}

fenpian2.gif

上传进度

这里还是通过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)
          }
        })
      ...
    },

取消上传

借助axioscancelTokenSource取消令牌,来及时阻断请求的上传,避免资源浪费。

首次会初始化,后面基于当前文件,都会放在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)
 })
}

image.png

可能对你有帮助