大文件分片上传

vue 版本 v3.2.26

fileSpliceUpload.js

import { ElMessage } from 'element-plus'
import { ref } from 'vue'
import {
  isExistAssignmentFile,
  initFileData,
  uploadMaxFile,
  mergeSliceFile,
} from '@/api/common.js'
import lodash from 'lodash'

import { ElLoading } from 'element-plus'

// 文件名称
let fileName = ''
// 分片总数
let total = ''

// 总文件md5值
let contentHash = ''

let worker = ''
let hashPercentage = ''

// 记录当前已成功上传的数量
let successNum = 0

// 分片的大小
const size = 1024 * 1024 * 5

// 用来记录当前上传的进度(父级传过来的)
let progressCofig = ''

// 是否需要切片
let isSlice = false

// 文件上传的参数
let fileParam = {}

// 切片上传失败的请求集合
const faildRequestList = []

// 文件切片的数据
const uploadOption = ref([])

// 获取响应数据
function getResData(res) {
  if (!res) return
  return lodash.get(res, 'data.data')
}

// 分片上传后,发送分片合并请求
async function mergeRequest() {
  const { businessId, md5, totalIndex } = fileParam
  return new Promise((res, rej) => {
    return mergeSliceFile({
      businessId,
      md5,
      totalIndex,
      originalFilename: fileName,
    })
      .catch(() => {
        rej()
        progressCofig.show = false
        // ElMessage.error('上传失败!')
      })
      .then(() => {
        res()
      })
  })
}

/**
 * 生成文件切片
 * @param {*} selectedFile
 */
function createFileChunk(selectedFile) {
  successNum = 0
  const fileChunkList = []
  let cur = 0

  while (cur < selectedFile.size) {
    fileChunkList.push({
      file: selectedFile.slice(cur, cur + size),
    })
    cur += size
  }
  return fileChunkList
}

// 计算md5
function calculateHash(fileChunkList) {
  return new Promise(resolve => {
    // 添加 worker 属性
    worker = new Worker('/hash.js')
    worker.postMessage({ fileChunkList })
    worker.onmessage = e => {
      const { percentage, hash } = e.data
      hashPercentage = percentage
      progressCofig.setAnProcentage(parseInt(hashPercentage))
      // console.log('hashPercentage', hashPercentage)
      if (hash) {
        resolve(hash)
      }
    }
  })
}
/**
 * 网络请求并发控制
 * @param {*} requestList 请求列表
 * @param {*} max 同时请求最大数
 * @returns
 */
function sendRequest(requestList, max = 6) {
  return new Promise(resolve => {
    const len = requestList.length
    let idx = 0
    let counter = 0
    const start = async () => {
      // 有请求,有通道
      while (idx < len && max > 0) {
        max-- // 占用通道
        // console.log(idx, 'start')
        const formData = requestList[idx].formData
        // const index = requestList[idx].index

        const cur = idx

        idx++
        uploadMaxFile(formData)
          .then(res => {
            // 页面上的进度条处理
            successNum++
            progressCofig.setPercentage(
              parseInt((successNum / requestList.length).toFixed(2) * 100),
            )
          })
          .catch(err => {
            // progressCofig.show = false
            // idx = len
            // ElMessage.error('上传失败!')

            const curObj = requestList[cur]

            // 这是第一次上传的,肯定是没有的,就直接push了
            faildRequestList.push({
              formData: curObj.formData,
              ...curObj.param,
              num: 0,
            })
          })
          .finally(() => {
            // 网络请求并发控制
            // 释放通道
            max++
            counter++
            if (counter === len) {
              resolve()
            } else {
              start()
            }
          })
      }
    }
    start()
  })
}

/**
 * 判断当前文件是否已上传
 * @param {*} contentHash md5的值
 * @returns
 */
async function isExist(contentHash) {
  return getResData(await isExistAssignmentFile(contentHash))
}

/**
 * 处理上传的参数
 * @param {*} uploadParam 参数
 */
async function handleUploadParam(selectedFile, uploadParam, loading) {
  const fileChunkList = createFileChunk(selectedFile, uploadParam)
  contentHash = await calculateHash(fileChunkList)
  fileParam.md5 = contentHash
  fileParam.totalIndex = fileChunkList.length
  const isUploaded = await isExist({
    md5: contentHash,
    businessId: uploadParam.businessId,
    fileType: uploadParam.fileType,
    module: uploadParam.module,
  })

  // 如果已经上传了,直接返回
  if (isUploaded) {
    ElMessage.warning('当前文件已存在!')
    progressCofig.show = false
    return {
      flag: false,
      requestList: [],
    }
  }

  try {
    // 初始化
    await initFileData({
      md5: contentHash,
      totalSlice: fileChunkList.length, // 总分片个数
      isSlice: isSlice ? '1' : '0', //是否分片 0:不分片 1:分片
      originalFileName: fileName,
      fileSize: selectedFile.size,
      businessId: uploadParam.businessId,
      recordingDuration: uploadParam.recordingDuration,
      fileType: uploadParam.fileType,
      module: uploadParam.module,
    })
  } catch (error) {
    loading && loading.close()
    return {
      flag: false,
      requestList: [],
    }
  }

  // 文件切片的数据
  uploadOption.value = fileChunkList.map(({ file }, index) => ({
    chunk: file,
    hash: fileName + '-' + index,
    percentage: 0,
    index,
    size: '',
  }))

  const requestList = fileChunkList.map(({ file }, index) => {
    const formData = new FormData()
    formData.append('md5', contentHash)
    formData.append('sliceIndex', index)

    formData.append('totalIndex', total)
    formData.append('file', file)
    formData.append('originalFilename', fileName)
    formData.append('businessId', uploadParam.businessId)
    formData.append('isSlice', isSlice ? '1' : '0')

    return {
      formData,
      param: {
        md5: contentHash,
        sliceIndex: index,
        totalIndex: total,
        originalFilename: fileName,
        businessId: uploadParam.businessId,
        file,
        isSlice: isSlice ? '1' : '0',
      },
    }
  })

  return {
    flag: true,
    requestList,
  }
}

/**
 * 循环递归失败的请求
 * @param {*} faildList 失败请求的list
 * @returns
 */
async function recursionFaild(faildList) {
  const requestFaildList = faildList.map((item, index) => {
    return uploadMaxFile(item.formData, 'retryRequest')
      .then(res => {
        faildList = faildList.filter(it => it.sliceIndex !== item.sliceIndex)

        // 页面上的进度条处理
        successNum++
        progressCofig.setPercentage(
          parseInt((successNum / requestList.length).toFixed(2) * 100),
        )
      })
      .catch(err => {
        const curObj = faildList[index] || { num: 0 }
        // console.log('curObjcurObjcurObjcurObj', curObj)
        // const obj = faildList.find(it => it.sliceIndex === curObj.sliceIndex)
        curObj.num++

        // 如果有接口调用了两次都不成功,就直接失败了
        if (curObj.num >= 2) {
          progressCofig.show = false
          faildList.length = 0
          ElMessage.error('上传失败!')
        }
      })
  })

  console.log('recursionFaild', requestFaildList)

  await Promise.all(requestFaildList)

  if (faildList.length) {
    recursionFaild(faildList)
  } else {
    return true
  }
}

// 小于5M直接上传,不用现实弹框等信息
async function uploadSingleFile(selectedFile, param) {
  const loading = ElLoading.service({
    lock: true,
    text: '',
    background: 'rgba(0, 0, 0, 0.7)',
  })
  const { flag, requestList } = await handleUploadParam(
    selectedFile,
    param,
    loading,
  )
  if (!flag) return
  const formData = requestList[0].formData

  await uploadMaxFile(formData)
    .then(() => {
      ElMessage.success('上传成功!')
    })
    .catch(() => {
      ElMessage.error('上传失败!')
    })
    .finally(() => {
      loading.close()
    })
}

/**
 * 上传附件
 * @param {*} selectedFile 选中的文件
 * @param {*} param 上传参数
 * @param {*} cofig 上传进度的配置
 * @returns
 */

export async function handleFileSpliceUpload(selectedFile, param, cofig) {
  if (selectedFile) {
    progressCofig = cofig
    fileParam = param
    fileName = param.fileName || selectedFile.name
    total = Math.ceil(selectedFile.size / size)
    isSlice = selectedFile.size > 1024 * 1024 * 5

    if (selectedFile.size > 1024 * 1024 * 1024 * 1.7) {
      ElMessage.warning('最大仅可上传1.7G文件!')
      return false
    }
    // 小于5M直接上传,不用现实弹框等信息
    if (selectedFile.size <= 1024 * 1024 * 5) {
      await uploadSingleFile(selectedFile, param)
      return false
    }

    progressCofig.setPercentage(0)
    progressCofig.setAnProcentage(0)
    progressCofig.show = true
    const { flag, requestList } = await handleUploadParam(selectedFile, param)
    if (!flag) return false

    // 并发请求
    // await Promise.all(requestList)

    await sendRequest(requestList)
    let faildFlag = faildRequestList.length === 0
    if (faildRequestList.length) {
      faildFlag = await recursionFaild(faildRequestList)
    }

    if (faildFlag) {
      // 合并切片
      isSlice && (await mergeRequest())
      progressCofig.show = false
      ElMessage.success('上传成功!')
      return true
    }
  }
}

export default handleFileSpliceUpload

hash.js

// 导入脚本
self.importScripts('/spark-md5.min.js')

// 生成文件 hash
self.onmessage = e => {
  const { fileChunkList } = e.data
  const spark = new self.SparkMD5.ArrayBuffer()
  let percentage = 0
  let count = 0
  const loadNext = index => {
    const reader = new FileReader()
    reader.readAsArrayBuffer(fileChunkList[index].file)
    reader.onload = e => {
      count++
      spark.append(e.target.result)
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end(),
        })
        self.close()
      } else {
        percentage += 100 / fileChunkList.length
        self.postMessage({
          percentage,
        })
        // calculate recursively
        loadNext(count)
      }
    }
  }
  loadNext(0)
}
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值