一、前言
在我们封装Vue文件上传组件的时候,如果需要进行自定义文件上传,通常会将文件上传的逻辑用通过 prop 参数解析接收,然后再在父组件中拿到文件上传组件的实例手动调用某个 API 触发上传操作。就像下面给的例子一样,不是那么的优雅。
src/components/CustomUpload.vue
<template>
<input type="file" ref="fileElem" @change="onChange" />
</template>
<script lang="ts">
import { defineComponent, ref, reactive, PropType, Ref } from 'vue'
type UploadHandle = (file: File) => Promise<void>
export default defineComponent({
name: 'CustomUpload',
props: {
upload: Function as PropType<UploadHandle>,
},
setup(props) {
const fileElem = ref<any>(null) as Ref<HTMLInputElement>
const misc = reactive({
file: null as null | File,
})
function onChange() {
const files = fileElem.value.files!
misc.file = files[0] ?? null
}
function submit() {
if (typeof props.upload === 'function' && misc.file) {
return props.upload(misc.file)
}
return Promise.resolve()
}
return { fileElem, onChange, submit }
}
})
</script>
src/App.vue
<template>
<custom-upload ref="uploadElem" :upload="uploadHandle" />
<button @click="onSubmit">上传</button>
</template>
<script setup lang="ts">
import CustomUpload from './components/CustomUpload.vue'
import axios from 'axios'
import { defineComponent, ref, Ref } from 'vue'
export default defineComponent({
components: { CustomUpload },
setup() {
const uploadElem = ref<any>(null) as Ref<{ submit: () => Promise<void> }>
const formData = reactive({
picture: ''
})
function uploadHandle(file: File) {
// 上传逻辑
return xxx(file).then((value) => {
formData.picture = value
})
}
function onSubmit() {
uploadElem.value.submit().then(() => {
// 提交表单数据
axios.post('xxx/xxx', formData)
})
}
return { uploadElem, uploadHandle, onSubmit }
}
})
</script>
因此我就在想入如何将 const uploadElem = ref<any>(null) as Ref<{ submit: () => Promise<void> }>
和 return { uploadElem }
省掉。如果能直接调用 uploadHandle().then(() => { // 提交表单数据 })
就能触发上传那岂不美哉。因此就有了这篇文章。
二、正文
其实要实现这个需求并不难,难的是跳出现有的思维模式。其思路如下:
父组件中:根据文件上传的逻辑函数生成一个触发上传的句柄函数,并把这个函数句柄提供给子组件
子组件中:通过 prop 拿取到句柄函数,再根据句柄函数注册一个执行函数,这个执行函数的参数为文件上传的逻辑函数。
触发上传:句柄函数执行时调用子组件注册的执行函数,并将逻辑函数作为参数传递给执行函数。
下面我们来看一下具体实现
二.一、第三方
我们需要一个第三方管理机构来暂存文件上传的逻辑函数,并将父组件中的句柄函数与子组件的执行函数建立关联。
src/components/UploadHandle.ts
type UploadHandle = (file: File) => Promise<string>
type SubmitHandle = () => Promise<void>
type ExecutHandle = (upload: UploadHandle) => Promise<void>
const temp: WeakMap<SubmitHandle, ExecutHandle> = new WeakMap()
export function provideUploadSubmit(upload: UploadHandle) {
const submit: SubmitHandle = function () {
const execut = temp.get(submit)
if (typeof execut === 'function') {
return execut(upload)
}
return Promise.resolve()
}
return submit
}
export function injectUploadExecut(submit: SubmitHandle, execut: ExecutHandle) {
temp.set(submit, execut)
}
二.二、子组件
src/components/CustomUpload.vue
<template>
<input type="file" ref="fileElem" @change="onChange" />
</template>
<script lang="ts">
import { defineComponent, ref, reactive, PropType, Ref, watch } from 'vue'
import { SubmitHandle, ExecutHandle, injectUploadExecut } from './UploadHandle'
export default defineComponent({
name: 'CustomUpload',
props: {
modelValue: {
type: String,
default: '',
},
// 句柄函数
upload: Function as PropType<SubmitHandle>,
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const fileElem = ref<any>(null) as Ref<HTMLInputElement>
// 执行函数(接受文件上传的 逻辑函数 为参数)
const execut: ExecutHandle = (upload) => {
if (misc.file) {
return upload(misc.file).then((value) => {
// 清除缓存,避免重复提交
misc.file = null
// 双向绑定把值传递给父组件
emit('update:modelValue', value)
})
}
return Promise.resolve()
}
watch(
() => props.upload,
(nv) => {
if (typeof nv === 'function') {
// 建立关联(句柄函数 与 执行函数)
injectUploadExecut(nv, execut)
}
},
{ immediate: true }
)
watch(
() => props.modelValue,
(nv) => {
if (nv) misc.file = null
},
{ immediate: true }
)
const misc = reactive({
file: null as null | File,
})
function onChange() {
const files = fileElem.value.files!
misc.file = files[0] ?? null
}
return { fileElem, onChange }
},
})
</script>
二.三、父组件
src/App.vue
<template>
<custom-upload v-model="formData.picture" :upload="uploadHandle" />
<button @click="onSubmit">上传</button>
</template>
<script lang="ts">
import axios from 'axios'
import CustomUpload from '@@/CustomUpload.vue'
import { provideUploadSubmit } from '@@/UploadHandle'
import { defineComponent, reactive, ref, Ref } from 'vue'
export default defineComponent({
components: { CustomUpload },
setup() {
const formData = reactive({
picture: '',
})
const uploadHandle = provideUploadSubmit((file: File) => {
// 上传逻辑
// ...
return Promise.resolve(file.name)
})
function onSubmit() {
uploadHandle().then(() => {
// 提交表单数据
axios.post('xxx/xxx', formData)
})
}
return { formData, uploadHandle, onSubmit }
},
})
</script>
三、总结
其实本文的思路受益于 Vue3 停止侦听 的启发,结合 element Notification
的 close
方法,进而思索出的一种解决方式。