想法

最近用Ant Mobile开发移动端项目,发现并没有提供上传的组件,再加上给出的需求有着各种各样的上传组件样式,故萌生将文件上传的功能抽取出来,以产生实例的方式绑定到各类上传组件上的想法。

实施

实现一个Uploader类

import http from '@/service/http'
import { Toast } from 'antd-mobile'
import { Base64 } from 'js-base64'

const URL = '/obtainguest/api/upload',
  NOPREFIX = true

interface UploadParams {
  file: FormData
  url: string
  noPrefix: boolean
}

let id = 0

export class Uploader {
  private type: 'image' | 'video'
  private total: number
  private dom: HTMLInputElement

  constructor(type: 'image' | 'video', setter: Function, total: number = 1) {
    let input = document.createElement('input')
    input.setAttribute('type', 'file')
    input.setAttribute('id', `uploader-${++id}`)
    input.setAttribute('accept', `${type}/*`)
    if (total > 1) {
      input.multiple = true
    }
    
    // 产生实例的同时会向document添加一个隐藏的input:file元素
    document.body.appendChild(input)

    input.style.visibility = 'hidden'
    input.style.position = 'absolute'
    
    // 当这个input元素“输入”也就是选择好文件确定时,会调用内置的upload方法,我这里是调用后台的上传接口,然后把接口返回的文件信息丢给传进来的回调函数处理
    input.addEventListener('input', () => {
      this.upload().then(data => {
        setter(data)
      })
    })

    this.dom = input
    this.type = type
    this.total = total
  }

  getDom() {
    return this.dom
  }

  upload() {
    // 这里是上传的约束以及调用后台的上传接口,最后返回上传成功后的文件信息
    const input = this.dom,
      type = this.type
    return new Promise((resolve, reject) => {
      if (!input || !input.files?.length) return

      const files = input.files,
        proList = []

      if (files.length > this.total) {
        Toast.fail(`最多上传${this.total}张图片`)
        return
      }

      for (let file of [...files]) {
        if (!file.type.startsWith(type)) {
          Toast.fail(`仅支持上传${type === 'image' ? '图片' : '视频'}类型文件`)
          reject('file type error')
          return
        }

        if (type === 'video' && file.size / 1024 > 1024 * 50) {
          Toast.fail('仅支持上传50MB以内的视频文件')
          reject('file size error')
          return
        }

        if (type === 'image' && file.size / 1024 >= 1024 * 10) {
          Toast.fail('仅支持上传10MB以内的图片文件')
          reject('file size error')
          return
        }

        const data = new FormData()
        data.append('file', file)
        data.append('state', Base64.encode('‬10485760'))

        const params: UploadParams = {
          file: data,
          url: URL,
          noPrefix: NOPREFIX,
        }

        proList.push(http(params))
      }

      Promise.all(proList).then(res => {
        resolve(
          res.map((item, idx) => ({
            ...item.data,
            file_name: files[idx].name,
          }))
        )
      })
    })
  }
}

实现自定义hook -- useUpload

上面的Uploader实例其实已经可以满足需求了,但是会产生一个问题,生成的实例同时也会生成input元素挂载在body上,为避免冗余的无用的元素就需要每次离开当前页面时都得手动清除掉input,显然是很麻烦的一件事情,所以将清除的功能提取进自定义hook里。

import { useEffect, useRef } from 'react'
import { Uploader } from 'utils/upload'

interface IFile {
  file_url: string
}

interface UploadParams {
  callback(file: Array<IFile>): void
  type?: 'image' | 'video'
  count?: number
}

export default function useUpload({ callback, type = 'image', count }: UploadParams) {
  const uploader = useRef<HTMLInputElement>()

  useEffect(() => {
    let { dom } = new Uploader(
      type,
      (file: Array<IFile>) => {
        callback(file)
      },
      count
    )
    // 暴露出input元素
    uploader.current = dom
    return () => {
      document.body.removeChild(dom)
    }
  }, [callback, type, count])

  return uploader
}

使用

import useUpload from '@/hooks/useUpload'
export default function NewPost() {
  const [postParams, setPostParams] = useState(initParams),
    getUploadList = useCallback((file: File[]) => {
      setPostParams(postParams => {
        if (file.length + postParams.file.length > 9) {
          Toast.fail('最多上传9张图片')
          return postParams
        }
        return {
          ...postParams,
          file: postParams.file.concat(file),
        }
      })
    }, []),
    imgUploader = useUpload({ callback: getUploadList, count: 9 }),
    mediaUploader = useUpload({
      callback: file => {
        console.log(file)
      },
      type: 'video',
    })
  
  function addImg() {
    imgUploader?.current?.click()
  }
  
  function addMedia() {
    mediaUploader?.current?.click()
  }
  
  return (
    <div className="newpost">
      <div onClick={addMedia}>upload</div>
      <AddIcon type="lg" onClick={addImg} className="cover" />
    </div>
  )
}

这里需要注意的是useUpload里对Uploader实例的生成是基于callback,type和count,imgUploader对callback进行了useCallback缓存,而mediaUploader没有,所以当页面元素更新时,mediaUploader的实例会跟着重新生成。

image.png


AwesomeHan
125 声望4 粉丝