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

前文的hooks参考: 七牛的单个的大文件上传的hooks,vue3版 hooks已经实现了上传、取消上传、记录上次上传位置、重传等功能

前言

本来一开始项目中只有一个地方用到了大文件上传,且时间紧急,没有直接封装这个组件,但我又接了一个新需求,这个需求也要求大文件上传,所以就顺手封装了下这个大文件上传的组件

注意: 本文章介绍的为单个的大文件上传组件

技术栈

  • vue3、element-plus、auto-import

简单分析下组件组成

  • 上传按钮、文件列表展示: 可通过el-upload来解决
  • 进度条展示: 进度条可取消文件上传,可通过el-progress解决,取消功能已经在hooks里写过了
  • 文件类型限制: accpet作为props传入
  • 只上传1个文件: limit为1
  • modelValue: 同时保证父组件传递一个file数据即可,而不是数组,避免了对数组查询操作等,方便简洁
  • 七牛配置传入: 主要传入bucketprefixbucket可能会不一样,具体看后端沟通
  • 文件校验回显: 在一些表单场景里,会需要到这种回显,通过elmenet-plus的useItem来解决
  • 可支持添加文件上传前的逻辑(比如校验文件行数): 添加before-upload函数作为props传入

效果图

预期的父文件使用代码

html 复制代码
<template>
    <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" label-position="left">
        <el-form-item label="文件" prop="file">
          <BigFileUploadOne
            ref="bigFileUploadRef"
            v-model="form.file"
            :qn-config="qnConfig"
            accept=".xlsx,.xls"
          />
        </el-form-item>
    </el-form>
</template>

<script setup lang="ts">

const bigFileUploadRef = ref()
const qnConfig = reactive({
  bucket: 'xxx-xxx-xxx',
  prefix: 'xxx/xxx/'
})

const formRef = ref()
const form = ref({
  file: null as File | null,
})
const rules = reactive({
  file: [
    {
      required: true,
      message: '请上传文件',
      trigger: 'change',
    },
  ],
})

</script>

组件代码-BigFileUploadOne

html 复制代码
<template>
  <div>
    <el-upload
      ref="uploadRef"
      v-model:file-list="fileList"
      :show-file-list="true"
      :limit="1"
      :auto-upload="false"
      :accept="accept"
      @change="onFileChange"
      @exceed="onFileExceed"
      @remove="cancelUpload"
    >
      <el-button type="primary">{{ btnText }}</el-button>
    </el-upload>

    <div v-if="uploading" class="flex items-center mt-12">
      <el-progress
        status="success"
        striped
        striped-flow
        :percentage="progress"
        :stroke-width="15"
        :duration="10"
        :format="formatProgress"
        text-inside
        :style="{ width: '600px' }"
      />
      <el-button type="danger" size="small" class="ml-12" @click="handleCancel">取消</el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { ElUpload, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
import { useFormItem } from 'element-plus'
import type { PropType, Ref } from 'vue'

import type { QnConfig } from '@/hooks/useBigFileUpload'
import { useBigFileUpload } from '@/hooks/useBigFileUpload'

const emit = defineEmits(['update:modelValue', 'cancel', 'beforeUpload', 'change', 'exceed'])

const props = defineProps({
  modelValue: {
    type: Object as () => UploadRawFile,
    default: null,
  },
  accept: {
    type: String,
    default: '',
  },
  qnConfig: {
    type: Object as () => QnConfig,
    default: () => ({
      bucket: '',
      prefix: '',
    }),
  },
  btnText: {
    type: String,
    default: '点击上传',
  },
  beforeUpload: {
    type: Function as PropType<(file: UploadRawFile) => Promise<boolean> | boolean>,
    default: null,
  },
})

const { formItem } = useFormItem()
const uploadRef = ref<InstanceType<typeof ElUpload>>()
const fileList = ref<UploadRawFile[]>([])

watch(
  () => props.modelValue,
  (val) => {
    fileList.value = val ? [val] : []
  },
  { immediate: true }
)

watch(fileList, (val) => {
  emit('update:modelValue', val[0] ?? null)
})

const formatProgress = (percentage: number) => (percentage === 100 ? '完成' : `${percentage}%`)

const { progress, uploading, error, success, startUpload, cancelUpload } = useBigFileUpload({
  onError: handleCancel,
  qnConfig: props.qnConfig,
})

const onFileChange: UploadProps['onChange'] = async (_uploadFile, uploadFiles) => {
  if (!uploadFiles.length) return
  const file = uploadFiles[0].raw as UploadRawFile
  if (!file) return

  if (props.beforeUpload && typeof props.beforeUpload === 'function') {
    const res = await props.beforeUpload(file)
    if (!res) {
      cancelUpload()
      uploadRef.value?.clearFiles()
      return
    }
  }

  startUpload(file)
  emit('change', file)
  emit('update:modelValue', file)
  formItem?.validate('change')
}

const onFileExceed: UploadProps['onExceed'] = (files: File[], uploadFiles: UploadUserFile[]) => {
  cancelUpload()
  uploadRef.value?.clearFiles()
  const file = files[0] as UploadRawFile
  uploadRef.value?.handleStart(file)
  emit('exceed', file)
}

function handleCancel() {
  cancelUpload()
  uploadRef.value?.clearFiles()
  emit('cancel')
}

defineExpose<{
  cancel: () => void
  uploading: Ref<boolean>
  progress: Ref<number>
  error: Ref<Error | null>
  success: Ref<boolean>
}>({
  cancel: handleCancel,
  uploading,
  progress,
  error,
  success,
})
</script>

<style scoped lang="scss"></style>

核心组件代码解释

v-model:file-list="fileList"

由于期望的是父组件传递一个file对象即可,并且只能限制1个文件上传,所以v-model的数据不能是file对象,只能是通过file构造出来的一个数组,来满足el-upload使用规则

所以代码上要通过watch监听file来构造出一个file-list,同时保证file-list的变化,要传递给file

javascript 复制代码
const props = defineProps({
  modelValue: {
    type: Object as () => UploadRawFile,
    default: null,
  },
})

const fileList = ref<UploadRawFile[]>([])

watch(
  () => props.modelValue,
  (val) => {
    fileList.value = val ? [val] : []
  },
  { immediate: true }
)

watch(fileList, (val) => {
  emit('update:modelValue', val[0] ?? null)
})

immediate: true:主要目的是为了file的初始化如果有值,那么可以通过初始化进行赋值,理论上可加可不加。

取消自动上传,改为手动上传

  • 核心点在于auto-upload=false,并且通过监听@change="onFileChange"事件来调用大文件上传服务
  • 这里就用不到el-uploadbefore-upload了,不会生效的
  • 监听时还需要满足上传前的事件处理
javascript 复制代码
const onFileChange: UploadProps['onChange'] = async (_uploadFile, uploadFiles) => {
  if (!uploadFiles.length) return
  const file = uploadFiles[0].raw as UploadRawFile
  if (!file) return

  if (props.beforeUpload && typeof props.beforeUpload === 'function') {
    const res = await props.beforeUpload(file)
    if (!res) {
      cancelUpload()
      uploadRef.value?.clearFiles()
      return
    }
  }

  startUpload(file)
  emit('change', file)
  emit('update:modelValue', file)
  formItem?.validate('change')
}
  1. 判断有没有file数据
  2. 判断有没有beforeUpload
  3. 最后调用大文件上传服务
  4. 触发数据回显
  5. 触发规则校验,formItem.validate可触发规则校验,优化体验

这里的beforeUpload返回false,就会清空文件列表,考虑到我就只负责单个的大文件上传,所以这个也是可以的,但是如果是多个文件上传,就需要换个写法了

cancelUpload()是来自useBigFileUploadhooks里的,具体如下:

使用

javascript 复制代码
const { progress, uploading, error, success, startUpload, cancelUpload } = useBigFileUpload({
  onError: handleCancel,
  qnConfig: props.qnConfig,
})

useBigFileUploadcancelUpload的源代码

javascript 复制代码
  // 取消上传
  function cancelUpload() {
    uploadSubscription?.unsubscribe()
    uploading.value = false
  }

只能上传1个文件

这里采用element-plus官方给的case

javascript 复制代码
const onFileExceed: UploadProps['onExceed'] = (files: File[], uploadFiles: UploadUserFile[]) => {
  uploadRef.value?.clearFiles()
  const file = files[0] as UploadRawFile
  uploadRef.value?.handleStart(file)
}

这里唯一注意点就是,重新上传一个新文件,要取消上传之前的文件,所以我这里的更改完之后就是这样

javascript 复制代码
const onFileExceed: UploadProps['onExceed'] = (files: File[], uploadFiles: UploadUserFile[]) => {
  cancelUpload() // 取消上传
  uploadRef.value?.clearFiles()
  const file = files[0] as UploadRawFile
  uploadRef.value?.handleStart(file)
  emit('exceed', file) // emit事件
}

取消上传

这里的取消上传,有3个场景

  • 文件列表的取消上传:@remove="cancelUpload"
  • 进度条的取消:@click="handleCancel"
  • 重置:resetForm + bigFileUploadRef?.value.cancel()

文件列表的取消上传 这里只需要监听@remove即可,调用cancelUpload

进度条的取消 进度条的取消,除了取消文件上传,还需要清空文件列表,考虑到我只负责单个的大文件上传,所以可以这样做,但多个的不行

重置 重置:重置表单+取消文件上传

javascript 复制代码
function handleResetForm() {
  resetForm(formRef.value)
  bigFileUploadRef?.value.cancel()
}

bigFileUploadRef?.value.cancel()这个其实就是handleCancel

后续

  • 研究多个大文件上传
  • el-upload或者input的监听文件资源读取进度,比如你上传一个600MB的文件,但就读取就需要10级几秒的时间,这可以做一个loading或者进度展示,更好的优化体验
相关推荐
我怎么能这么帅气11 分钟前
JavaScript 变量提升 (Hoisting):那些让你代码“行为异常”的隐形规则
前端·javascript
江城开朗的豌豆13 分钟前
JavaScript篇:JS类型转换的黑魔法:从入门到怀疑人生
前端·javascript·面试
用户214118326360213 分钟前
dify案例分享-豆包文本生成图像、文本生成视频以及图像转视频工作流
前端
我怎么能这么帅气17 分钟前
告别旧标签:HTML5 废弃标签清单与现代替代方案
前端·javascript·html
ak啊18 分钟前
WebGL入门指南:从零构建你的第一个3D应用
前端·webgl
SameX19 分钟前
HarmonyOS Next类型安全实践:强类型检查与溢出控制
前端
前端店小二21 分钟前
前端Mac从零到一搭建开发环境
前端·mac
全靠搬砖加氧气23 分钟前
ElementUI - select 全选组件
前端
小离a_a23 分钟前
css实现圆环展示百分比,根据值动态展示所占比例
前端·css