Vue3 + Element Plus 实现大文件切片上传及进度条展示

文章目录

    • 1 前言
    • 2 功能实现
      • 2.1 添加 Element Plus 上传代码及进度条展示代码
      • 2.2 限制文件上传类型和大小
      • 2.3 判断文件大小,小文件直接上传
      • 2.4 大文件获取文件唯一标识
      • 2.5 计算切片数量
      • 2.6 上传切片
      • 2.7 取消上传
    • 3 完整代码
    • 4 拓展

1 前言

最近在做一个项目的重构,其中有大文件上传的功能,由于项目是几年前,代码没有前后分离,用的是 jQuery + webuploader 库做的,但实际上只是实现了大文件切片上传,并没有切片并发、秒传及断点续传功能,后端也不支持,且 webuploader 库已经不再维护了,故决定自己实现一个最简单的大文件切片上传功能。

2 功能实现

2.1 添加 Element Plus 上传代码及进度条展示代码


2.2 限制文件上传类型和大小

const beforeUpload = (file: File) => {
  const mimeTypes = ['audio/mpeg', 'audio/x-m4a', 'audio/aac', 'video/mp4', 'video/x-m4v']
  if (!mimeTypes.includes(file.type)) {
    ElMessage({
      type: 'error',
      message: '只能上传 MP3、M4A、AAC、MP4、M4V 格式的文件',
      duration: 6000
    })
    return false
  }
  if (file.size / 1024 / 1024 / 1024 > 1.5) {
    ElMessage.error('文件大小不能超过 1.5G')
    ElMessage({
      type: 'error',
      message: '文件大小不能超过 1.5G',
      duration: 6000
    })
    return false
  }
  return true
}

2.3 判断文件大小,小文件直接上传

const chunkSize = 1 * 1024 * 1024 // 切片大小
const upload = async (file: { file: File }) => {
  const fileObj = file.file
  const nameList = fileObj.name.split('.')
  fileData.value.name = fileObj.name
  fileData.value.size = fileObj.size
  fileData.value.type = fileObj.type
  fileData.value.suffix = nameList[nameList.length - 1]
  if (chunkSize > fileData.value.size) { // 文件大小小于切片大小,直接上传
    disabled.value = true
    axios
      .post('upload', fileObj) // 调用后端上传文件接口
      .then((res) => {
        ElMessageBox({ message: `${fileData.value.name}上传成功`, title: '提示' })
        updateUrl(res.data) // 调用后端保存上传文件路径接口
      })
      .catch(() => ElMessageBox({ message: `${fileData.value.name}上传失败`, title: '提示' })) // 上传失败弹框
      .finally(() => (disabled.value = false))
    return
  }
  batchUpload(fileObj) // 大文件切片上传
}

2.4 大文件获取文件唯一标识

// 重构项目没有断点续传等功能,故不需要做hash计算,只需要保证唯一即可,后端会拿这个值新建文件夹保存切片
let counter = 0
const getFileMd5 = () => {
  let guid = (+new Date()).toString(32)
  for (let i = 0; i < 5; i++) {
    guid += Math.floor(Math.random() * 65535).toString(32)
  }
  return 'wu_' + guid + (counter++).toString(32)
}

2.5 计算切片数量

const percentage = ref(0)
const dialogVisible = ref(false)
const cancelUpload = ref(false)
const batchUpload = async (fileObj: File) => {
  percentage.value = 0 // 每次上传文件前清空进度条
  dialogVisible.value = true // 显示上传进度
  cancelUpload.value = false // 每次上传文件前将取消上传标识置为 false
  const chunkCount = Math.ceil(fileData.value.size / chunkSize) // 切片数量
  fileData.value.md5 = getFileMd5() // 文件唯一标识
  for (let i = 0; i < chunkCount; i++) {
    if (cancelUpload.value) return // 若已经取消上传,则不再上传切片
    const res = await uploadChunkFile(i, fileObj) // 上传切片
    if (res.code !== 0) { // 切片上传失败
      dialogVisible.value = false
      ElMessageBox({ message: `${fileData.value.name}上传失败`, title: '提示' })
      return
    }
    if (i === chunkCount - 1) { // 最后一片切片上传成功
      setTimeout(() => { // 延迟关闭上传进度框用户体验会更好
        dialogVisible.value = false
        ElMessageBox({ message: `${fileData.value.name}上传成功`, title: '提示' })
        axios.post('mergeUpload', { folder: fileData.value.md5 }) // 调用后端合并切片接口,参数需要与后端对齐
          .then((res) => updateUrl(res.data)) // 调用后端保存上传文件路径接口
      }, 500)
    }
  }
}

2.6 上传切片

let controller: AbortController | null = null // 当前切片上传 AbortController
const uploadChunkFile = async (i: number, fileObj: File) => {
  const start = i * chunkSize // 切片开始位置
  const end = Math.min(fileData.value.size, start + chunkSize) // 切片结束位置
  const chunkFile = fileObj.slice(start, end) // 切片文件
  const formData = new FormData() // formData 参数需要与后端对齐
  formData.append('fileName', fileData.value.name)
  formData.append('folder', fileData.value.md5)
  formData.append('file', chunkFile, String(i + 1)) // 必传字段;若第三个参数不传,切片 filename 默认是 blob ,如果后端是以切片名称来做合并的,则第三个参数一定要传
  controller = new AbortController() // 每一次上传切片都要新生成一个 AbortController ,否则重新上传会失败
  return await axios
    .post('mergeUpload', formData, { // 调用后端上传切片接口
      onUploadProgress: (data) => { // 进度条展示
        percentage.value = Number(
          (
            (Math.min(fileData.value.size, start + data.loaded) / fileData.value.size) *
            100
          ).toFixed(2)
        )
      },
      signal: controller.signal // 取消上传
    })
    .then((res) => updateUrl(res.data))
}

2.7 取消上传

const cancel = () => {
  dialogVisible.value = false
  cancelUpload.value = true
  controller?.abort()
  axios.post('cancelUpload', { folder: fileData.value.md5 }) // 调用后端接口,删除已上传的切片
}

3 完整代码








4 拓展

  • hash 值作为文件唯一标识
    引入 js-spark-md5 库做文件 hash 计算
  • 切片并发上传
    需要控制好并发数量
  • 秒传
    上传文件前请求后端接口,通过文件 hash 值判断否存已经上传过该文件,存在则无需再上传该文件,直接返回上传成功,实现秒传
  • 断点续传
    上传切片前请求后端接口,通过切片 hash 值判断是否已经上传过该切片,存在则无需再上传该切片,从下一个切片开始上传,实现断点续传

你可能感兴趣的:(Vue3,Element,Plus,vue.js)