文件上传,如何绕开 RefDom.submit()

319 阅读2分钟

一、前言

在我们封装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 Notificationclose 方法,进而思索出的一种解决方式。