七牛的单个的大文件上传的hooks,vue3版

1,837 阅读5分钟

七牛的单个大文件上传的组件封装,vue3+elment-plus+hooks

背景

某一天早上,主管说今天能不能搞下大文件上传,紧急支持一下

我心想,哇趣,根本没做过呀,虽然文章很多是了,但没法子拒绝,那天就只有我1个前端在公司上班,于是只能硬着头皮上了,后来发现,这个大文件上传好像很简单

需求

  • 支持单个的大文件上传,如100MB
  • 有进度条展示
  • 可取消上传
  • 支持保留上传进度,比方说页面不小心关掉了

技术栈

  • vue3
  • node: v16.x.x
  • elment-plus
  • qiniu、qiniu-js的v3版本

开发前的准备工作

考虑到,相关文章非常多,时间又非常紧迫,这种情况,只能继续使用gpt了,于是询问他关于vue3+element-plus+七牛实现一个大文件上传功能,然后gpt就甩给我一个hooks,我一看基本都满足我的需求了,而且看起来蛮简单,就开心的复制了。

不过只靠gpt也不行,还需要看七牛文档,对照下参数是否有问题,七牛文档中有个2个关键点: 服务端生成token、前端调用文件上传服务,我一看大文件上传服务核心点都被七牛实现了,我就负责传递参数即可,心想这真的是帮大忙了。

特别注意:代码只涉及到了单个的大文件上传的场景

具体开发过程

服务端生成token

对接服务端的同学,先给了我akskbucket用,考虑到时间紧迫,等不起他的开发,我就先用js脚本模拟生成了我需要的token,反正不复杂,具体如下

// config.js
// 定一个七牛配置
export const qiniuConfig = {
  ak: 'xxxx',
  sk: 'xxx',
  bucket: 'xxx',
}

// qn.js
// 获取token
import qiniu from 'qiniu
import { qiniuConfig } from './config.js'

const AK = qiniuConfig.ak
const SK = qiniuConfig.sk
const BUCKET = qiniuConfig.bucket

const mac = new qiniu.auth.digest.Mac(AK, SK)

export function getUploadToken(fileName) {
  const prefix = 'xxx/xxx/' // 可以和后端沟通需要什么路径,比如file/时间戳/ 
  const key = prefix + fileName // key其实就是文件资源名,加上前缀可以指定存放路径
  const putPolicy = new qiniu.rs.PutPolicy({ scope: `${BUCKET}:${key}` })
  const uploadToken = putPolicy.uploadToken(mac)
  return { token: uploadToken, key }
}

记住这个token只能用node运行得出来,因为qiniu是后端服务,不能被前端调用

前端大文件上传hooks

hooks比较简单,最关键的是发起上传和取消上传时机,具体如下

// js v3 七牛参考文档: https://developer.qiniu.com/kodo/1283/javascript

import * as qiniu from 'qiniu-js'

import commonApi from '@/api/common'

export interface QnConfig {
  bucket: string // 七牛存储空间名称
  prefix: string // 七牛存储前缀
}

export interface UseQiniuUploaderOptions {
  chunkSize?: number // 切片大小(默认4MB)
  concurrency?: number // 并发上传数(默认3)
  retryCount?: number // 失败重试次数(默认3)
  isShowMessage?: boolean // 是否显示消息提示
  qnConfig: QnConfig // 七牛配置
  onError?: (args?: any) => void // 错误回调
}

export interface ProgressCompose {
  size: number // chunk 的尺寸
  loaded: number // 已经发送完毕的尺寸
  percent: number // 进度比例,范围在 0 - 100
  fromCache: boolean // 是否是来自缓存
}

export interface UseQiniuNextInfo {
  uploadInfo: {
    id: string | number // 上传任务的唯一标识
    url: string // 上传地址
  }
  chunks: Array<ProgressCompose>
  total: {
    loaded: number // 已上传大小,单位为字节
    total: number // 本次上传的总量控制信息,单位为字节,注意这里的 total 跟文件大小并不一致
    percent: number // 当前上传进度,范围:0~100
  }
}

export function useBigFileUpload(options: UseQiniuUploaderOptions) {
  const {
    chunkSize = 4,
    concurrency = 3,
    retryCount = 3,
    qnConfig = {
      bucket: '',
      prefix: '',
    },
    isShowMessage = true,
    onError = (args?: any) => {
      /* noop */
    },
  } = options

  const progress = ref(0)
  const uploading = ref(false)
  const error = ref<Error | null>(null)
  const success = ref(false)

  // 七牛返回的 subscription,对象仅需调用 unsubscribe() 取消上传
  let uploadSubscription: { unsubscribe: () => void } | null = null

  async function getUploadToken(fileName = '') {
    const res = await commonApi.getQnToken()
    if (!res) return null
    return {
      token: res.data.uploadToken,
      key: qnConfig.prefix + fileName,
    }
  }

  // 开始上传
  async function startUpload(file: File) {
    uploading.value = true
    error.value = null
    success.value = false
    progress.value = 0

    try {
      const tokenRes = await getUploadToken(file.name)
      if (!tokenRes) {
        error.value = new Error('获取上传凭证失败')
        return
      }

      const { token, key } = tokenRes

      // 上传额外参数,可按需扩展
      const putExtra = {
        fname: file.name, // 文件名称
        mimeType: file.type, // 文件类型
        customVars: {} as Record<string, string>, // x:开头,如: {'x:tst': 'qiniu'}
        metadata: {} as Record<string, string>, // x-qn-meta-开头,如: {'x-qn-meta-title': 'qiniu'}
      }

      // 上传配置
      const config = {
        useCdnDomain: true, // 是否使用 cdn 加速域名
        retryCount, // 上传自动重试次数
        chunkSize, // 分片上传时每片的大小,必须为正整数,单位为 MB,且最大不能超过 1024,默认值 4
        concurrentRequestLimit: concurrency, // 分片上传的并发请求量,默认为3;因为浏览器本身也会限制最大并发量,所以最大并发量与浏览器有关。
        forceDirect: file.size < 1 * 1024 * 1024, // 是否上传全部采用直传方式
        checkByMD5: true, // 是否开启 MD5 校验, 用于断点上传
        // disableStatisticsReport: false, // 是否禁用日志报告
        // debugLogLevel: 'ERROR', // 调试日志级别 INFO | WARN | ERROR | OFF
      }

      // 发起上传
      const observable = qiniu.upload(file, key, token, putExtra, config) as any
      uploadSubscription = observable.subscribe(
        (res: UseQiniuNextInfo) => {
          // 上传进度
          progress.value = Number(res.total?.percent.toFixed(2)) || 0
        },
        (err: any) => {
          // 上传失败
          error.value = err
          uploading.value = false
          if (isShowMessage) {
            ElMessage({
              type: 'error',
              message: err.message,
            })
          }
        },
        (_: any) => {
          // 上传成功
          progress.value = 100
          success.value = true
          uploading.value = false
          if (isShowMessage) {
            ElMessage({
              type: 'success',
              message: '上传成功',
            })
          }
        }
      )
    } catch (err: any) {
      error.value = err
      uploading.value = false
    }
  }

  // 取消上传
  function cancelUpload() {
    uploadSubscription?.unsubscribe()
    uploading.value = false
  }

  // 错误处理,比方说上传失败要清空文件列表
  watch(
    () => error.value,
    (val) => {
      if (val) {
        onError(val)
      }
    }
  )

  // 组件卸载时自动取消
  onBeforeUnmount(() => {
    cancelUpload()
  })

  return {
    progress,
    uploading,
    error,
    success,
    startUpload,
    cancelUpload,
  }
}

前端文件上传样式

看这篇文章:七牛的单个大文件上传的组件封装,vue3+elment-plus+hooks

核心点

  • 传入qnConfig,灵活控制前缀和bucket
  • uploadSubscription = observable.subscribe,这段函数就是上传的核心点
  • 根据getUploadToken拿到的token,给后端进行查询链接