react实现阿里oss分片上传hook

315 阅读2分钟

参考前端阿里云OSS分片上传

分片上传,普通上传,停止上传,恢复,用了一下分片上传和停止上传,其他还没试过

useAliOssHook

import env from '@/config'
import api from '@/services/aliOss'
import { generateFileName } from '@/utils'
import { message } from '@/utils/AntdGlobal'
import OSS from 'ali-oss'
import dayjs from 'dayjs'
import { nanoid } from 'nanoid'
import { useState } from 'react'

// 阿里OSS

type CustomRequestOptions = {
  onProgress?: (
    event: {
      percent: number
    },
    file: File
  ) => void
  onError?: (error: Error) => void
  onSuccess?: (response: object, file: File) => void
  data?: object
  filename?: string
  file: File
  withCredentials?: boolean
  action?: string
  headers?: object
}

let credentials: {
  accessKeyId: string
  accessKeySecret: string
  stsToken: string
  expiration: string
}

const ossClientMap: any = {} // oss客户端实例
const fileNameMap: any = {} // 文件名
const checkpointMap: any = {} // 所有分片上传文件的检查点
const optionMap: any = {}
const partSize = 1 * 1024 * 1024 // 每个分片大小1MB
const parallel = 3 // 同时上传的分片数

export const useAliOssHook = (newPath: string = '') => {
  const [uuid] = useState(nanoid())
  const [percent, setPercent] = useState(0)
  const [loading, setLoading] = useState(false)

  const { bucket, region } = env.aliOss

  // 获取StsToken
  const getCredential = async () => {
    const { accessKey, accessKeySecret, securityToken, expiration } = await api.getStsToken()
    credentials = {
      accessKeyId: accessKey,
      accessKeySecret: accessKeySecret,
      stsToken: securityToken,
      expiration: expiration
    }
  }

  // 创建OssClient
  const initOSSClient = async () => {
    ossClientMap[uuid] = new OSS({
      ...credentials,
      bucket,
      region
    })
  }

  // 普通上传
  const commonUpload = async (file: any, type?: boolean) => {
    let folderPath = ''
    if (type) {
      const folder = file?.webkitRelativePath.split('/')
      folderPath = `${folder.splice(0, folder.length - 1).join('/')}/`
    }

    if (!ossClientMap[uuid]) {
      await initOSSClient()
    }

    try {
      const data = await ossClientMap[uuid].put(newPath + folderPath + fileNameMap[uuid], file)
      const { url } = data
      // optionMap[uuid].onSuccess(
      // 	{ fileId: res?.fileId, url }, optionMap[uuid].file,
      // )
      setLoading(false)
      return url
    } catch (error) {
      const msg = (error as Error).message
      message.error(`上传失败,${msg}`)
    }
  }

  // 断点续传
  const resumeMultipartUpload = async () => {
    Object.values(checkpointMap[uuid]).forEach(async (checkpoint: any) => {
      const { uploadId, file, name } = checkpoint
      try {
        await ossClientMap[uuid].multipartUpload(uploadId, file, {
          parallel,
          partSize,
          progress: onMultipartUploadProgress,
          checkpoint
        })

        delete checkpointMap[uuid][checkpoint.uploadId]
        const url = `http://${bucket}.${region}.aliyuncs.com/${name}`
        // optionMap[uuid].onSuccess({ fileId: null, url }, optionMap[uuid].file)
        return url
      } catch (error) {
        const msg = (error as Error).message
        message.error(`上传失败,${msg}`)
      }
    })
  }

  // 分片上传进度改变回调
  const onMultipartUploadProgress = async (progress: number, checkpoint: any) => {
    const percentNum = Number.parseInt(`${progress * 100}`, 10)
    setPercent(percentNum)
    if (optionMap[uuid] && optionMap[uuid].onProgress) {
      optionMap[uuid].onProgress({ percent: percentNum }, checkpoint.file)
    }

    if (progress === 1 && checkpoint === null) {
      // 结束回调
      return
    }

    checkpointMap[uuid] = {
      [checkpoint.uploadId]: checkpoint
    }
    // 判断STS Token是否将要过期,过期则重新获取
    const { expiration } = credentials
    // const timegap = 2
    // if (expiration && moment(expiration).subtract(timegap, 'minute').isBefore(moment())) {
    if (expiration && dayjs() > dayjs(expiration)) {
      if (ossClientMap[uuid]) {
        ossClientMap[uuid].cancel()
      }
      await getCredential()
      await resumeMultipartUpload()
    }
  }

  // 分片上传
  const multipartUpload = async (file: any, type?: boolean) => {
    let folderPath = ''
    if (type) {
      const folder = file?.webkitRelativePath.split('/')
      folderPath = `${folder.splice(0, folder.length - 1).join('/')}/`
    }

    if (!ossClientMap[uuid]) {
      await initOSSClient()
    }

    const data = await ossClientMap[uuid].multipartUpload(newPath + folderPath + fileNameMap[uuid], file, {
      parallel,
      partSize,
      progress: onMultipartUploadProgress
    })
    const { name } = data
    // optionMap[uuid].onSuccess({ fileId: res?.fileId, url }, optionMap[uuid].file)
    delete ossClientMap[uuid]
    delete optionMap[uuid]
    delete fileNameMap[uuid]
    delete checkpointMap[uuid]
    setLoading(false)

    return name
  }

  const upload: any = async (initOptions: CustomRequestOptions, type?: boolean) => {
    try {
      setLoading(true)
      checkpointMap[uuid] = {}
      // 获取STS Token
      await getCredential()
      optionMap[uuid] = initOptions
      fileNameMap[uuid] = generateFileName(optionMap[uuid].file)
      if (optionMap[uuid].file.size < partSize) {
        return commonUpload(optionMap[uuid].file, type)
      }
      return multipartUpload(optionMap[uuid].file, type)
    } catch (error) {
      const msg = (error as Error).message
      message.error(`上传失败,${msg}`)
    }
  }

  // 暂停上传
  const stop = () => {
    if (ossClientMap[uuid]) ossClientMap[uuid].cancel()
  }

  // 续传
  const resume = () => {
    if (ossClientMap[uuid]) resumeMultipartUpload()
  }

  return { stop, resume, upload, percent, loading }
}

utils

// 生成不重复文件名
export function generateFileName(file: any) {
  const suffix = file.name.split('.').pop()
  const fileName = nanoid()

  return `${fileName}.${suffix}`
}

调用

const { upload, percent, stop } = useAliOssHook('服务器上存放上传文件的路径/')

const params = {
  file
}
const name = await upload(params)