实现文件上传Upload组件

3,254 阅读5分钟

之前一直使用antd的upload组件,受antd启发,自己简单实现一个类似的文件上传组件,可以进行普通上传,拖拽上传,取消上传功能

TODO: 大文件上传和断点续传待补充

1. 实现文件上传功能

import React, { ChangeEvent, useRef, useState } from 'react';
import logo from './logo.svg';
import axios from 'axios'
import { Button } from 'antd';
import './App.css';
import { ButtonSize, ButtonType} from './ButtonCom/Button'
import UploadList from './UploadList'
import Dragger from './dragger'
import { DownloadOutlined } from '@ant-design/icons';
class Images {
  public src: string = 'https://www.google.com.hk/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png'
  public alt: string = '谷歌'
  public width: number = 500
}

type propsName = keyof(Images)
interface Obj {
  [key: string]:any;
}

interface User {
  username: string
  id: number
  token: string
  avatar: string
  role: string
}

// 变成可选的
type Partial<T> = {
  [P in keyof T]? : T[P]
};

type Partials<T> = {
  [P in keyof T]?: T[P]
}

type PartialProps = Partial<User>

export type UploadFileStatus = 'ready' | 'uploading' | 'success' | 'error'
export interface UploadFile {
  uid: string;
  size: number;
  name: string;
  status?: UploadFileStatus;
  percent?: number;
  raw?: File;
  response?: any;
  error?: any;
}

interface PropsType {
  // 上传之前的操作
  beforeUpload?: (file: File) => boolean | Promise<File>
   // 额外的参数
  data?: {[key: string]: any}
  // name: 上传文件时 对应的key值,默认为 file: file
  name?: string
  // 上传的地址
  action: string
  //  headers
  headers?: {[key: string]: any}
  // 跨域是否需要凭证,axios默认为false
  withCredentials?: boolean
  // 移除时候的回调
  onRemove?: (file: File) => void; 
  // 上传成功的回调
  onSuccess?: (data: any, file: File) => void;
  // 上传失败的回调
  onError?: (err: any, file: File) => void;
  // 上传改变的回调
  onChange?: (file: File) => void;
  // 是否允许拖拽
  drag?: boolean
}

const App: React.FC<PropsType> =  (props) => {

  const { beforeUpload, data, name, action, headers, withCredentials, onRemove, onSuccess, onError, onChange, children,
    drag, } = props;


  const [fileList, setFileList] = useState<any>([{
    uid: 1,
    size: 2343,
    name: 'test',
    status: 'uploading',
    percent: 75,
    // raw?: File;
    // response?: any;
    // error?: any;
  }])

  const InputRef = useRef<HTMLInputElement>(null)

  const handleClick = () => {
    if (InputRef.current) {
      InputRef.current.click();
  }
  }

  const handleRemove = (File: any) => {
    console.log('点击要移除的文件',File)
    setFileList((prevList:any) => {
      return prevList.filter((item:any) => item.uid !== File.uid)
    })

    // 取消上传
    // 将取消请求函数保存到file文件里面,删除的时候调用这个请求
    if ( typeof(File.cancels) === 'function') {
      File.cancels()
   }

   if (onRemove) {
    onRemove(File)
  } 

    // if (onRemove) {
    //   onRemove(file)
    // }

    
   
    
   
  }

  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    // 获取到文件
    const Files = e.target.files
    // INput 没有上传直接退出
    if (!Files) {
      return
    }
    uploadFiles(Files)
    // 防止上传相同文件不会触发onChange, 清楚上传值
    if (InputRef.current) {
      InputRef.current.value=""
    }

  }

  const uploadFiles = (Files: any) => {
    console.log(Files,'---')
    
    let postFiles = Array.from(Files)
    console.log(postFiles)
     postFiles.forEach((item:any) => {

        // 如果是图片。将图片以base64格式展示出来
        // const reader = new FileReader();
        // reader.readAsDataURL(item)
        // reader.onload = (e:any) => {
        //   console.log(e.target.result)
        //   const ImgElement = document.createElement('img')
        //   ImgElement.src = e.target.result;
        //   document.body.appendChild(ImgElement)
        // }
      
      // beforeUpload 不存在 则直接上传
      if (!beforeUpload) {
        post(item)
      } else {
        const result = beforeUpload(item)
        if (result && result instanceof Promise) {
          result.then(processFile => {
            post(processFile)
          })
        } else if (result !== false) {
          post(item)
        }
      }
        
     })
    
  }

  const post = (file:any) => {
    // 用于取消上传 
    let cancels:any

    // 要保存文件上传的信息
    let _file = {
      uid: Date.now() + 'upload-file',
      name: file.name,
      size: file.size,
      status: 'ready',
      percent: 0,
      raw: file,
      // 保存取消上传的函数
      cancels: cancels
    }

    // 将要上传的文件以及信息保存到filelist里面,用于上传列表展示 
    setFileList((prevList:any) => {
      return [_file, ...prevList]
    })

    // 利用formData 数据
    const formData = new FormData()
    formData.append(name || 'file', file )

    // 如果有传入额外的参数(data),也添加到formData中
    if (data) {
      Object.keys(data).forEach(item => {
        formData.append(item, data[item])
      })
    }

    // 开始上传文件
    // 利用axioa
    axios.post(action, formData, {
      headers: {
        ...headers,
        'Content-Type': 'multipart/form-data'
      },
      // 取消请求
      cancelToken: new axios.CancelToken((c) => {
        // 让状态为ready的时候也可以取消上传
        updataFileList(_file, { cancels : c })
        cancels = c
    }),
      // 表示跨域请求时候是否需要使用凭证
      withCredentials,
      // 上传处理进度事件
      onUploadProgress: (e) => {
        let procentTage = Math.round((e.loaded * 100) / e.total) || 0
        if (procentTage < 100) {
          // 更新fileList的值 并且将取消上传的函数也保存下来,用于取消上传
          updataFileList(_file, { percent: procentTage, status: 'uploading', cancels : cancels  })
        }
      }
    }).then(resp => {
      updataFileList(_file,{ status: 'success', response: resp.data })
      if (onSuccess) {
        onSuccess(resp.data, file)
      }
      if (onChange) {
        onChange(file)
      }
    }).catch(err => {
      updataFileList(_file, {status: 'error', error: err})
      if (onError) {
        onError(err, file)
      }
      if (onChange) {
        onChange(file)
      }
    })

  }

  // 更新fileLIst列表,进度以及状态
  function updataFileList(file: any,updateObj: any) {
    setFileList((prev:any) => {
      return prev.map((item: any) => {
        if (file.uid === item.uid) {
          return {...file, ...updateObj }
        } else {
          return item
        }
      })
    })
  }

  return (
    <div className="App">
      <header>
        <div> 
  {/* <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <Button> 默认的 </Button>
        <Button href="xxxx" btnType={ButtonType.Link}> 第一个</Button> */}


        

        <div>
          
          <Button shape="round" icon={<DownloadOutlined />} onClick={handleClick}>
            上传文件
            {/* {drag ? 
              <Dragger onFile={(files) => {uploadFiles(files)}}>
                {children}
              </Dragger>:
              children
            } */}
           
            <input 
              style={{ display: 'none' }} 
              type="file" 
              ref={InputRef}
              onChange={handleInputChange}
              multiple
            />
          </Button>
           {
              <Dragger onFile={(files) => {uploadFiles(files)}}>
                {children}
                <a onClick={handleClick}>
                    点此上传
                </a>
              </Dragger>
            }
          <UploadList 
            fileList={fileList}
            onRemove={handleRemove}
          />
          {/* <Button type="primary">Button</Button> */}
            
{/* <div id="drag" draggable="true">zzzz</div> */}

        </div>

        </div>
      
      </header>
    </div>
  );
}

export default App;


利用axios实现上传接口请求

2. 展示上传文件信息列表

展示的文件信息应该展示 1.上传进度 2.重新上传 3.文件名
注意: 点击重新上传的时候,需要取消掉接口,因为是利用axios上传,那么我们直接利用axios的取消请求

axios 取消请求 方法一:




import React from 'react'
import { Progress,Button, Space, Tooltip } from 'antd'
import { DeleteOutlined } from '@ant-design/icons';

interface Props {
  fileList?: any;
  onRemove?: any;
}

const UploadList = (props: Props) => {

  const { fileList, onRemove } = props;
  console.log(fileList, '文件list')
  return (
    <>
      <div>
        上传文件列表
      </div>
      <ul>
        {
          fileList.map((item:any) => {

            return (
              <div key={item.uid}>
                {/* <div style={{ width: 100 }}>
                  {item.name}
                </div> */}
                <Space>
                    <span>
                      <Tooltip title={item.name}>
                      {(item.status === 'uploading' || item.status === 'ready') && (<span>上传中</span>)}
                      {item.status === 'success' && (<span>上传成功</span>)}
                      {item.status === 'error' && (<span>上传失败</span>)}
                      </Tooltip>
                    </span>
                    <div style={{ width: 200 }}>
                      {
                        item.status === 'uploading' && (
                          <Progress percent={item.percent || 0} />
                        )
                      }
                      {
                        item.status === 'success' && (
                          <Progress percent={100} />
                        )
                      }
                      {
                        item.status === 'ready' && (
                          <Progress percent={0} />
                        )
                      }
                      {
                        item.status === 'error' && (
                          <Progress percent={item.percent || 100} status="exception" />
                        )
                      }
                    </div>
                    
                    <Tooltip title={  item.status === 'uploading' || item.status === 'ready'? '取消上传' : '重新上传'}>
                     <Button type="primary" shape="circle" icon={<DeleteOutlined />} onClick={() => onRemove(item)} />
                    </Tooltip>
                </Space>
              </div>
            )

          })
        }
      </ul>
    </>
  )

}




export default UploadList

3. 实现拖拽上传

利用H5新增的拖拽方法

元素在拖放过程中触发的事件 HTML5中,只要将元素的 draggable 属性设置为 true 就可以实现拖放功能

<div draggable="true">实现拖拽</div> 

在拖放过程中,触发了多个事件,如下:

dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发。

drag:事件主体是被拖放元素,在正在拖放被拖放元素时触发。

dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。

dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。

dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触发。

drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。

dragend:事件主体是被拖放元素,在整个拖放操作结束时触发。

以上我们看到拖动总共有7个事件,其中事件主体是拖放元素的是,dragstart(开始拖动) 、drag(正在拖放) 、dragend(拖放结束),其他4个事件主体都是目标元素,进入、移动、离开、完全进入四个状态。

简单的一个小例子来了解drag

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #divTarget {
      width: 300px;
      height: 300px;
      border: 5px solid red;
    }
  </style>
</head>
<body>
  <div id="dragText" draggable="true">这是可拖动的元素</div>
  <img src="./404.gif" class="dragimg" alt=""  style="width: 200px; height: 200px;">
  <div id="divTarget">
    xx
  </div>
  <script>

    let TargetBox = document.getElementById('divTarget')
    let dragtexts = document.getElementById('dragText')
    // 元素开始拖动时
    dragtexts.addEventListener('dragstart', (event) => {
      //  dataTransfer.setData() 来添加数据
      //  dataTransfer.getData() 来获取数据 在drop中才能拿到值
      event.dataTransfer.setData('keyss', '拖拽保存的数据')
      console.log('元素开始拖动时')

    })

    // 元素正在拖动时
    dragtexts.addEventListener('drag', (event) => {
      console.log('元素正在拖动时')
    })
    
    // 元素拖动结束
    dragtexts.addEventListener('dragend', (event) => {
      console.log('元素拖动结束')
    })

    // 进入区域
    TargetBox.addEventListener('dragenter', (event) => {
      console.log('进入到此区域')
    })

    // 离开区域
    TargetBox.addEventListener('dragleave', (event) => {
      console.log('离开区域')
    })

    // 区域拖动
    TargetBox.addEventListener('dragover', (event) => {
      event.preventDefault(); // 如果要放到区域必须使用这个
      console.log('区域拖动')
    })

    // 放到区域
    TargetBox.addEventListener('drop', (event) => {
      console.log('放到区域')
      console.log(event.dataTransfer.getData('keyss'))
    })
    
    </script>
</body>



</html>

然后来写dragger组件

组件里面也可以引用 classNames 来进行控制样式的更改,这里样式没啥要求,就简单写下

import React, { FC, useState, DragEvent } from 'react'
import classNames from 'classnames'

interface DraggerProps {
  onFile: (files: FileList) => void;
}

export const Dragger: FC<DraggerProps> = (props) => {
  const { onFile, children } = props
  const [ dragOver, setDragOver ] = useState(false)

  // 使用方法 true 的时候加上这个类,false 不加这个类
  const klass = classNames('viking-uploader-dragger', {
    'is-dragover': dragOver
  })

  // 拖拽的元素放到了目标元素上
  const handleDrop = (e: DragEvent<HTMLElement>) => {
    e.preventDefault()
    setDragOver(false)
    // 将拖拽获取到的文件传出去
    onFile(e.dataTransfer.files)
  }

  // 用来判断目标元素
  const handleDrag = (e: DragEvent<HTMLElement>, over: boolean) => {
    console.log('drageover' )
    e.preventDefault()
    setDragOver(over)
  }
  return (
    <div 
      className={klass}
      style={{background: dragOver ? 'red' : '#ccc' , width: 200, height: 200, border: dragOver ? '5px dashed black' : '1px solid #fff', margin: '0 auto'}}
      // 在目标元素移动中触发
      onDragOver={e => { handleDrag(e, true)}}
      // 在离开目标元素触发
      onDragLeave={e => { handleDrag(e, false)}}
      // 把拖拽元素放到目标元素上触发
      onDrop={handleDrop}
    >
      {children}
      <div style={{margin: '0 auto'}}>拖拽上传</div>
      
    </div>
  )
}

export default Dragger;