vue3+element 分片上传 分片下载功能实现

思路:分片上传是把一个大文件切割若干等份,前端循环调用上传接口进行上传。分片下载也是一样的道理,前端调用接口拿到文件总大小,计算分割成多少份,循环调用下载接口获取每一段的文件流,获取全部文件片段,进行合并下载。

一、安装依赖

用于获取文件的唯一标识,后端会根据此标识判断是否传过这个文件,传过的话就直接返回文件路径,提示上传成功

npm install spark-md5

方法封装

import SparkMD5 from 'spark-md5'
// 获取文件的唯一MD5标识码
export function getFileMd5(file) { return new Promise((resolve, reject) => { const fileReader = new FileReader()
    const spark = new SparkMD5.ArrayBuffer()
    fileReader.readAsArrayBuffer(file)
    fileReader.onload = e => { spark.append(e.target.result)
      let md5 = spark.end()
      resolve(md5)
    }
  })
}
二、分片上传
  {{ modelObj.loading ? '上传中...' : buttonTitle_ }} 

上传的逻辑

import { getFileMd5 } from './method'
import { computed, reactive} from 'vue'
const modelObj = reactive({ fileList: {},
  loading: false,
  percentage: 0,
})
// 上传之后重新点击上传
const handleExceed = uploadFile => { modelObj.fileList = {}
  handleChange({ raw: uploadFile[0] })
}
//后端接口
const { upLoadBypiece, downLoadbyPiece } = api
// 文件上传 选择文件时触发(:on-change事件)
const handleChange = async (uploadFile, uploadFiles) => { modelObj.percentage = 0
  // 文件信息
  let fileRaw = uploadFile.raw
  modelObj.fileName = fileRaw.name
  console.log(fileRaw, 'fileRaw')
  modelObj.loading = true
  // 获取 文件的 MD5唯一标识码
  let fileMd5 = null
  try { fileMd5 = await getFileMd5(fileRaw)
  } catch (e) { console.error('[error]', e)
  }
  if (!fileMd5) return
  // 每片的大小为 5M 可调整
  const chunkSize = 5 * 1024 * 1024
  // 文件分片储存
  let chunkList = []
  function chunkPush(page = 1) { chunkList.push(fileRaw.slice((page - 1) * chunkSize, page * chunkSize))
    if (page * chunkSize < fileRaw.size) { chunkPush(page + 1)
    }
  }
  chunkPush()
  saveFileChunk(chunkList, fileMd5, fileRaw.name)
}
// 保存文件片段到后台
const saveFileChunk = async (chunkList, fileMd5, fileName) => { for (let i = 0; i < chunkList.length; i++) { let formData = new FormData()
    formData.append('filePath', props.filePath) // minio存储的路径
    formData.append('chunk', i) // 当前片段的索引
    formData.append('chunkSize', 5 * 1024 * 1024) // 切片的文件分片大小 (就是以多少字节进行分片的,这里是5M)
    formData.append('chunks', chunkList.length) // 共有多少分片
    formData.append('chunkFile', chunkList[i]) // 当前分片的文件流
    formData.append('md5', fileMd5) // 整个文件的MD5唯一标识码,不是分片
    formData.append('fileName', fileName) // 文件的名称
    formData.append('size', chunkList[i].size) // 当前切片的大小(最后一片不一定是5M)
    try { const data = await upLoadBypiece(formData)
      //计算当前上传进度百分比,展示进度条
      modelObj.percentage = Math.floor(((i + 1) / chunkList.length) * 100)
      //成功的时候接口会返回文件的相关信息,当有data.fileName,说明上传成功了
      if (data.fileName) { modelObj.percentage = 100
        modelObj.loading = false
        modelObj.fileList = data
        emit('getFile', modelObj.fileList)
        console.log(modelObj.fileList, 'modelObj.fileList')
        message.success(`上传成功`)
        return
      }
    } catch (e) { modelObj.loading = false
    }
  }
}

效果图如下

三、分片下载
(1)分片下载合并核心伪代码
 let fileBlob=[]
 for (let index = 0; index < 5; index++) { const params={}
      const config={}
      const data = await downLoadbyPiece(params, config)
      //存储每一片文件流
      fileBlob.push(data.data)
  }
  //合并
  const blob = new Blob(fileBlob, { type:fileBlob[0].type,
  })
  //下载
  const link = document.createElement('a')
  link.href = window.URL.createObjectURL(blob)
  link.download = fileName
  link.click()
  window.URL.revokeObjectURL(link.href)
(2)思路:

1、前端第一次调用接口,请求头默认传 Range:bytes=0-chunkSize ,第一段的文件大小。接口会在响应头返回Content-Range: bytes 0-5242880/534107865,534107865即为总文件大小,需要根据这个总文件大小以及每片大小chunkSize ,计算分为多少段下载。

后端参考代码:https://www.jianshu.com/p/e8dee3dbc409

请求头

第一次Range: bytes=0-5242880,第二次请求Range: bytes=5242880-10485760,依次递增

响应头

Content-Disposition用于获取文件名称;Content-Range获取总文件大小

  • 如果network有Content-Disposition,Content-Range;但是前端取不到值,参考 vue

    axios无法获取响应头Content-Disposition字段

    2、根据总文件大小以及每片大小chunkSize计算合并成数组uploadRange。格式如下

    3、第一次调用已经传了Range: bytes=0-5242880,所以循环是从数组第2位开始调用下载接口

    (3)代码
     {{ downloadObj.downloading ? '下载中...' : '下载文件' }}  下载进度({{ downloadObj.percentage }}%)
    //下载逻辑
    const downloadObj = reactive({ fileName: '',
      downloading: false,
      range: 0,
      fileBlob: [],
      percentage: 0,
    })
    const download = async () => { downloadObj.fileBlob = []//存接口返回的每一段的文件流
      downloadObj.downloading = true
      downloadObj.range = 0 //文件总大小
      downloadObj.percentage = 0 //下载进度
      const params = { md5: '73333a4795dfdfgv266454bbbgfdge41f',
      }
      const chunkSize = 5 * 1024 * 1024
      //第一次调接口获取到响应头的content-range,文件总大小,用于计算下载切割
      const config = { headers: { Range: `bytes=0-${chunkSize}`,
        },
      }
      const data = await downLoadbyPiece(params, config)
      //获取文件总大小
      const arr = data.headers['content-range'].split('/')
      downloadObj.range = Number(arr[1])
      //存储每片文件流
      downloadObj.fileBlob.push(data.data)
      //获取文件名称
      let fileName = ''
      let cd = data.headers['content-disposition']
      if (cd) { let index = cd.lastIndexOf('=')
        fileName = decodeURI(cd.substring(index + 1, cd.length))
      }
      await chunkUpload(params, fileName, chunkSize)
    }
    //拿到文件总大小downloadObj.range,计算分为多少都段下载
    const chunkUpload = async (params, fileName, chunkSize) => { //获取分段下载的数组
      let chunkList = []
      function chunkPush(page = 1) { chunkList.push((page - 1) * chunkSize)
        if (page * chunkSize < downloadObj.range) { chunkPush(page + 1)
        }
      }
      chunkPush()
      chunkList.push(downloadObj.range)
      console.log(chunkList, 'chunkList')
      //分段组合传参格式处理 0-1024 1024-2048
      let uploadRange = []
      chunkList.forEach((item, i) => { if (i == chunkList.length - 1) return
        uploadRange.push(`${chunkList[i]}-${chunkList[i + 1]}`)
      })
      console.log(uploadRange, 'uploadRang')
      for (let index = 0; index < uploadRange.length; index++) { if (index > 0) { const config = { headers: { Range: `bytes=${uploadRange[index]}`,
            },
          }
          const data = await downLoadbyPiece(params, config)
          //计算下载进度
          downloadObj.percentage = Math.floor(((index + 1) / uploadRange.length) * 100)
          emit('getDownloadpercent', downloadObj.percentage)
          //存储每一片文件流
          downloadObj.fileBlob.push(data.data)
        }
      }
      //合并
      const blob = new Blob(downloadObj.fileBlob, { type: downloadObj.fileBlob[0].type,
      })
      downloadObj.downloading = false
      //下载
      const link = document.createElement('a')
      link.href = window.URL.createObjectURL(blob)
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(link.href)
    }
    
    四、上传下载完整代码

    参考文章:

    https://blog.csdn.net/m0_51431448/article/details/127953473

    https://www.jianshu.com/p/64694675ca95