uniapp+vue3+TS小程序内手写分片上传

235 阅读3分钟
1.分片上传hook

需要后端提供三个接口,初始化接口、上传分片接口、合并分片接口。
可以先将uploadInOrder函数(上传每个分片)折叠,看完其他逻辑再看uploadInOrder内容。
这个hook实现了 切割分片并上传自定义分片大小随时终止上传回显上传进度 等功能

// 上传大文件,分片处理
import {
  initiateMultipartUpload,
  uploadPart,
  completeMultipartUpload,
} from '@/service/index/upload'

/**
 * 分片上传
 * @param formData 上传文件信息tempFilePath: 文件路径, fileSize: 文件大小, uploadOssUrl: 上传到云端的路径
 * @param callBack 上传回调success: 是否成功, result: 上传结果, err: 错误信息
 * @param options 上传配置chunkSize: 分片大小, onProgress: 上传进度, shouldAbort: 是否中止上传
 */
export default function useMultipart<T = string>(
  formData: {
    tempFilePath: string
    fileSize: number
    uploadOssUrl: string
  },
  callBack: (success: boolean, result?: T, err?: any) => void,
  options?: {
    chunkSize?: number
    onProgress?: (progress: number) => void
    shouldAbort?: () => boolean
  },
) {
  const defaultOptions = {
    chunkSize: formData.fileSize > 50 * 1024 * 1024 ? 5 * 1024 * 1024 : 2 * 1024 * 1024, // 默认分块大小:大于50MB时5MB每块,否则2MB每块
    ...options,
  }

  // 分块上传流程
  const uploadProcess = async () => {
    try {
      // 1. 读取文件的buffer格式
      const file = uni.getFileSystemManager().readFileSync(formData.tempFilePath)

      // 2. 初始化分片上传
      const initResponse = await initiateMultipartUpload({
        object_name: formData.uploadOssUrl,
      })
      const uploadId = initResponse.data.upload_id // 获取上传ID

      // 3. 分块处理
      const chunkCount = Math.ceil(formData.fileSize / defaultOptions.chunkSize)
      const chunks = Array.from({ length: chunkCount }, (_, i) => i)
      const uploadedParts: { PartNumber: number; ETag: string }[] = []

      // 单片上传
      const uploadInOrder = async (chunkNumber: number) => {
        if (defaultOptions.shouldAbort?.()) {
          throw new Error('Upload aborted by user')
        }

        const start = chunkNumber * defaultOptions.chunkSize
        const end = Math.min(start + defaultOptions.chunkSize, formData.fileSize)
        const chunkData = (file as ArrayBuffer).slice(start, end)

        console.log('chunkNumber', chunkNumber, new Date().getTime())
        const partResponse = await uploadPart({
          upload_id: uploadId,
          object_name: formData.uploadOssUrl,
          content: uni.arrayBufferToBase64(chunkData),
          part_number: chunkNumber + 1,
        })

        // ETag是接口返回值,是最后合并时需要的标识,方便合并时查到具体哪个分片
        uploadedParts.push({
          PartNumber: chunkNumber + 1,
          ETag: partResponse.data.ETag,
        })

        // 更新上传进度
        const progress = Math.round(((chunkNumber + 1) / chunkCount) * 100)
        defaultOptions.onProgress?.(progress)
      }

      // 所有分片按顺序执行上传的函数
      const uploadItems = async (items: number[]) => {
        for (const item of items) {
          // 用户手动终止上传
          if (defaultOptions.shouldAbort?.()) {
            throw new Error('Upload aborted by user')
          }
          await uploadInOrder(item)
        }
      }

      // 4. 开始上传所有分片
      await uploadItems(chunks)

      // 5. 完成上传前检查用户是否选择中止
      if (defaultOptions.shouldAbort?.()) {
        throw new Error('Upload aborted by user')
      }

      // 6. 执行合并操作
      const completeResponse = await completeMultipartUpload({
        upload_id: uploadId, // 初始化上传时获取的id
        object_name: formData.uploadOssUrl, // 需要上传到的云端路径
        upload_parts: uploadedParts, // 上传的所有分片合集
      })

      callBack(true, completeResponse.data as T)
    } catch (err) {
      callBack(false, undefined, err?.message === 'Upload aborted by user' ? 'aborted' : err)
    }
  }

  uploadProcess()
}
2.使用分片上传hook示例

各种前置变量创建

// 生成uuid的函数
const generateUUID = () => {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    const r = (Math.random() * 16) | 0
    const v = c === 'x' ? r : (r & 0x3) | 0x8
    return v.toString(16)
  })
}
// 路径示例,实际以uni.chooseMedia获取到的tempFilePath为准,建议大小和格式限制也一并校验
const filePath = 'http://tmp/g8eLrsqW4dhSa50a1fa970c086b21f6c06985413e0d6.mp4'
// 文件后缀
const videoFormat = filePath.split('.').pop()
// 用户id和随机uuid生成一个云端文件路径(即将要上传到云端的路径)
const uploadOssUrl = userId + '/video/' + generateUUID() + '.' + videoFormat

// 显示进度条弹框
showUploadInfo.value = true

主要调用方法

// 调用分片上传hook
useMultipart(
  {
    tempFilePath: firstFile.tempFilePath, // 临时文件路径
    fileSize: firstFile.size, // 文件大小
    uploadOssUrl, // 上传地址
  },
  (success, result, err) => {
    console.log('上传结果', success, result, err)
    if (success) {
      toast.success('上传成功!')
    } else {
      toast.show('上传失败!')
    }
    // 关闭进度条弹框,并将进度归零
    showUploadInfo.value = false
    uploadProgress.value = 0
  },
  {
    onProgress: (res) => {
      console.log('progress', res)
      // 随时更新进度百分比
      uploadProgress.value = res
    },
    // 传入showUploadInfo的值传给hook,当用户点击取消时,showUploadInfo弹框关闭,hook就会停止上传
    shouldAbort: () => {
      return !showUploadInfo.value
    },
  },
)