前文的hooks参考: 七牛的单个的大文件上传的hooks,vue3版 hooks已经实现了上传、取消上传、记录上次上传位置、重传等功能
前言
本来一开始项目中只有一个地方用到了大文件上传,且时间紧急,没有直接封装这个组件,但我又接了一个新需求,这个需求也要求大文件上传,所以就顺手封装了下这个大文件上传的组件
注意: 本文章介绍的为单个的大文件上传组件
技术栈
- vue3、element-plus、auto-import
简单分析下组件组成
- 上传按钮、文件列表展示:
可通过el-upload来解决
- 进度条展示: 进度条可取消文件上传,
可通过el-progress解决
,取消功能已经在hooks里写过了 - 文件类型限制:
accpet作为props传入
- 只上传1个文件:
limit为1
- modelValue: 同时保证父组件传递一个file数据即可,而不是数组,避免了对数组查询操作等,方便简洁
- 七牛配置传入: 主要传入
bucket
、prefix
,bucket
可能会不一样,具体看后端沟通 - 文件校验回显: 在一些表单场景里,会需要到这种回显,
通过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-upload
的before-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')
}
- 判断有没有
file
数据 - 判断有没有
beforeUpload
- 最后调用大文件上传服务
- 触发数据回显
- 触发规则校验,
formItem.validate
可触发规则校验,优化体验
这里的beforeUpload
返回false
,就会清空文件列表,考虑到我就只负责单个的大文件上传,所以这个也是可以的,但是如果是多个文件上传,就需要换个写法了
cancelUpload()
是来自useBigFileUpload
的hooks
里的,具体如下:
使用
javascript
const { progress, uploading, error, success, startUpload, cancelUpload } = useBigFileUpload({
onError: handleCancel,
qnConfig: props.qnConfig,
})
useBigFileUpload
的cancelUpload
的源代码
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或者进度展示,更好的优化体验