因为前后端分离以后,很多前端同学因为没有接触过后端代码,导致后端代码对他们来说就是一个黑盒子,让他们困惑不堪,当接口出了问题以后,前后端相互扯皮!就因为不懂后端代码,就要被这样欺负吗?我不服气,那就奋发图强自己写个接口,好好玩玩,看看后端到底有啥可神秘的?
本文讲述的主要内容是 React 下 Upload 的封装,后端用的是 Express 实现,当然你也可以把这套理念移植到 Vue 下面封装它的 Upload。内容主打一个轻松愉快,为了要大家快速掌握封装组件的核心,我没有对代码做任何边界处理,全部以清晰可见的核心代码为主。
文章主要分为三部分
1.自己写接口,好好体验下 Antd 下面的 Upload 组件,
2.封装自己的 Upload 组件
3.总结封装全过程
一. 全新体验前后端上传文件
1. 创建项目:
npx create-vite
在创建项目的时候直接选择:react 和 typescript 就好了。创建好以后,执行命令,启动项目。
npm install
npm run dev
2. 引入 antd 试试他的 upload
npm install antd -S
复制upload的基础代码粘贴到app里面
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, Upload, message } from 'antd';
const props: UploadProps = {
name: 'file',
action: 'https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188',
headers: {},
onChange(info) {
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (info.file.status === 'done') {
message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
},
};
const App: React.FC = () => (
<Upload {...props}>
<Button icon={<UploadOutlined />}>Click to Upload</Button>
</Upload>
);
export default App;
如果是写到这个假地址里面去,和我们平常的开发不就一样了么,有啥好玩的?你说是不是?它依旧对我来说是一个黑盒子,我哪知道文件去哪里了,它内部到底是怎么运作的?看下面,写个服务,就是这么简单
3. 在根目录下面创建 server,写后端代码
命令行运行代码:
npm i express multer cors fs -D
把他们都安装好以后,执行命令,启动服务。
node ./server/index.js
解释下上面的代码:
-
因为我们前端的服务端口是5173,后端的端口是3002,是不是就存在跨域问题,跨域有2个解决办法,一个是在接口里面写上:res.setHeader('Access-Control-Allow-Origin', '*');另一个是引入cors
-
当后端拿到文件以后,总的存储到一个地方去吧,怎么存储就是multer该做的事情了, multer({ dest: 'uploads/'})
-
如果直接上传上去的话,文件名是一个hash值,我们用fs把他转化成原来的名字: fs.renameSync(oldName, newName)
4.接入接口
在app.tsx的upload组件的action里面接入我们自己的接口
5.测试
进服务端看看
它的意思就是找不到uploads/目录,在代码里面我们用multer指定了文件的去处,但是我们没有创建这个文件夹,现在创建一个再测试看看
测试
看看文件在不在
神奇不神奇?
测试下中文文件看看
处理下看看:
在Express中使用中间件multer处理文件上传时,可以设置filename属性,并通过iconv-lite模块将非UTF-8编码的文件名转换为UTF-8。
启动服务,测试如下:
看下uploads文件夹里面有没有美女图片?
现在下载的文件不但文件名是对的,连文件内容,还有扩展名都有了。接下来我们就用这个node服务作为后端,处理我们封装的 Upload。
二. 封装自己的 Upload
第一步,引入 input 标签
封装的第一步就是创建 Upload 文件夹,从<input type="file"/>入手,比如:
上传成功,就说明我们的思路是对的,然后在这个基础上,一步一步的完善他的内容要他变得和 antd 的 Upload 一模一样,你说疯狂不?
上面代码长这样,就一个原生态的上传,是不是丑了,我们做的第一件事情,就是改变他的样子。
首先App.tsx里面的 Upload 组件里面传入了一个 Button 作为他的 children,现在我们的目标是 Children 传入什么,Upload 就显示什么。同时还要把原来的文件上传器给隐藏掉。隐藏掉 input 以后,我要实现,点击了children 以后,出现上传文件时候的文件选择框。此时就用到 ref,通过 ref 拿到 input 然后执行input 的 click 事件,是不是就实现了基本功能?
import {ChangeEvent, PropsWithChildren, useRef } from 'react'
import axios from 'axios'
import './index.css';
interface IProps extends PropsWithChildren {
}
export const Upload = (props: IProps) => {
const {
children,
} = props
const fileRef = useRef<HTMLInputElement>(null);
const handleChange = () => {
fileRef.current && fileRef.current.click()
}
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
// 获取file
let file = e?.target?.files?.[0] as File;
let formdata = new FormData()
formdata.append('file', file)
axios.post('http://localhost:3002/upload', formdata, {
headers: {
'Content-Type': 'multipart/form-data;charset=UTF-8'
}
})
.then(response => {
console.log('结果', response);
})
.catch(() => {
console.log('Error uploading file');
});
}
return (
<div className="upload-container">
<div className="upload-btn" onClick={handleChange}>
{children}
<input
className="upload-file"
type="file"
onChange={handleFileChange}
ref={fileRef}
/>
</div>
</div>
)
}
export default Upload;
Upload组件
App.tsx
测试
测试代码以后,你会发现执行成功了,但是差的也还挺远的,比如页面上没有显示出来上传的文件,支持多个文件上传,上传文件类型的限制等等,慢慢来不着急,其实剩下的都只是加属性,很简单的。
第二步 添加属性
接下来我们添加这些属性:
interface IProps extends PropsWithChildren {
action: string; // 接口地址
headers?: Record<string, any>; // 请求头
name?: string; // 表单的名字,在formData的时候使用
data?: Record<string, any>; //带给组件的数据
accept?: string; // 文件接受的数据类型
multiple?: boolean; //多文件上传
}
import {ChangeEvent, PropsWithChildren, useRef } from 'react'
import axios from 'axios'
import './index.css';
interface IProps extends PropsWithChildren {
action: string; // 接口地址
headers?: Record<string, any>; // 请求头
name?: string; // 表单的名字,在formData的时候使用
data?: Record<string, any>; //带给组件的数据
accept?: string; // 文件接受的数据类型
multiple?: boolean; //多文件上传
}
export const Upload = (props: IProps) => {
const {children, action, name, headers, data, accept, multiple} = props
const fileRef = useRef<HTMLInputElement>(null);
const handleChange = () => {
fileRef.current && fileRef.current.click()
}
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
// 获取file
let file = e?.target?.files?.[0] as File;
let formdata = new FormData()
formdata.append(name || 'file', file)
if (data) {
Object.keys(data).forEach(key => {
formdata.append(key, data[key])
})
}
axios.post(action, formdata, {
headers: {
...headers,
'Content-Type': 'multipart/form-data;charset=UTF-8'
}
}).then(response => {
console.log('结果', response);
}).catch(() => {
console.log('Error uploading file');
});
}
return (
<div className="upload-container">
<div className="upload-btn" onClick={handleChange}>
{children}
<input
className="upload-file"
type="file"
onChange={handleFileChange}
ref={fileRef}
accept={accept}
multiple={multiple}
/>
</div>
</div>
)
}
export default Upload;
基本的功能都已经支持了。接下来我们添加下页面的文件列表,没有列表老感觉在耍猴,是不是?
第三步 添加回调函数
就是创建一个数组,用数据来收集所有上传进来的文件,然后把他们用 ul 平铺在页面上。在此之前我们先要把Upload的回调函数接进来。(模仿使人进步,学习的第一步就是模仿,而非创造。)
先在Upload组件里面添加下ts的类型如下:

先接入 onChange,onSuccess,onError
我们先优化下,把代码拆分到不同的函数里面去
import {ChangeEvent, PropsWithChildren, useRef, useState } from 'react'
import axios from 'axios'
import './index.css';
interface IProps extends PropsWithChildren {
action: string; // 接口地址
headers?: Record<string, any>; // 请求头
name?: string; // 表单的名字,在formData的时候使用
data?: Record<string, any>; //带给组件的数据
accept?: string; // 文件接受的数据类型
multiple?: boolean; //多文件上传
beforeUpload?: (file: File)=> boolean | Promise<File>;
onProgress?: (percentage: number, file: File) => void;//是进度更新时的回调,可以拿到进度
onSuccess?: (err: any, file: File) => void; //是上传成功的回调。
onError?: (err: any, file: File)=>void;//是上传失败的回调。
onChange?: (file: File) => void; //上传状态改变时的回调。
}
export const Upload = (props: IProps) => {
const {children,
action,
name,
headers,
data,
accept,
multiple,
beforeUpload,
onProgress,
onSuccess,
onError,
onChange,} = props
const fileRef = useRef<HTMLInputElement>(null);
const [ uploadList, setUploadList ] = useState<Array<File>>([]);
const handleChange = () => {
fileRef.current && fileRef.current.click()
}
const uploadFiles = (files: FileList) => {
let postFiles = Array.from(files)
postFiles.forEach(file => {
post(file)
})
}
const post = (file: File) => {
const formData = new FormData()
formData.append(name || 'file', file);
if (data) {
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
}
axios.post(action, formData, {
headers: {
...headers,
'Content-Type': 'multipart/form-data'
},
})
}
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
// 获取file,单文件下载就是multiple 为false的时候直接取 let file = e?.target?.files?.[0] as File;
// 多文件用 let file = e?.target?.files
const files = e.target.files
if(!files) {
return
}
uploadFiles(files)
}
return (
<div className="upload-container">
<div className="upload-btn" onClick={handleChange}>
{children}
<input
className="upload-file"
type="file"
onChange={handleFileChange}
ref={fileRef}
accept={accept}
multiple={multiple}
/>
</div>
<ul></ul>
</div>
)
}
export default Upload;
在此基础上,接入回调函数:
进度条用的是axios的 onUploadProgress,onUploadProgress 是 Axios 库中用于处理 HTTP 请求的一个配置选项之一,onUploadProgress 允许指定一个回调函数,在上传进度发生变化时被调用。这个回调函数接收一个进度事件对象作为参数,可以从中获取上传进度的信息。这对于跟踪文件上传的进度很有用,特别是在需要显示进度条时。
还有一个是:beforeUpload, 他的任务就是在上传文件之前去做一些事情。
接入app.tsx
测试:先把网速调低一些
完整代码如下:
// Upload/index.tsx
import {ChangeEvent, PropsWithChildren, useRef, useState } from 'react'
import axios from 'axios'
import './index.css';
export interface UploadProps extends PropsWithChildren {
action: string; // 接口地址
headers?: Record<string, any>; // 请求头
name?: string; // 表单的名字,在formData的时候使用
data?: Record<string, any>; //带给组件的数据
accept?: string; // 文件接受的数据类型
multiple?: boolean; //多文件上传
beforeUpload?: (file: File)=> boolean | Promise<File>;
onProgress?: (percentage: number, file: File) => void;//是进度更新时的回调,可以拿到进度
onSuccess?: (data: any, file: File) => void; //是上传成功的回调。
onError?: (err: any, file: File)=>void;//是上传失败的回调。
onChange?: (file: File) => void; //上传状态改变时的回调。
}
export const Upload = (props: UploadProps) => {
const {children,
action,
name,
headers,
data,
accept,
multiple,
beforeUpload,
onSuccess,
onError,
onChange,
onProgress,
} = props
const fileRef = useRef<HTMLInputElement>(null);
const handleChange = () => {
fileRef.current && fileRef.current.click()
}
const uploadFiles = (files: FileList) => {
let postFiles = Array.from(files)
postFiles.forEach(file => {
if(!beforeUpload){
post(file);
return;
}
const result = beforeUpload(file)
if (result && result instanceof Promise) {
result.then(processedFile => {
post(processedFile)
})
return;
}
post(file);
})
}
const post = (file: File) => {
const formData = new FormData()
formData.append(name || 'file', file);
if (data) {
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
}
//进度直接用 axios 的 onUploadProgress
axios.post(action, formData, {
headers: {
...headers,
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (e) => {
let percentage = Math.round((e.loaded * 100) / e.total!) || 0;
if (percentage < 100) {
if (onProgress) {
onProgress(percentage, file)
}
}
}
}).then((res)=>{
onSuccess?.(res.data, file)
onChange?.(file)
}).catch((err)=>{
onError?.(err, file)
onChange?.(file)
})
}
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
// 获取file,单文件下载就是multiple 为false的时候直接取 let file = e?.target?.files?.[0] as File;
// 多文件用 let file = e?.target?.files
const files = e.target.files
if(!files) {
return
}
uploadFiles(files)
}
return (
<div className="upload-container">
<div className="upload-btn" onClick={handleChange}>
{children}
<input
className="upload-file"
type="file"
onChange={handleFileChange}
ref={fileRef}
accept={accept}
multiple={multiple}
/>
</div>
<ul></ul>
</div>
)
}
export default Upload;
App.tsx下面的代码如下:
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
// import type { UploadProps } from 'antd';
import { Button, message } from 'antd';
import Upload, {UploadProps} from './components/Upload'
const props: UploadProps = {
name: 'file', //表单名字
action: 'http://localhost:3002/upload',//接口
headers: {},//请求头
data: {},
multiple: true,
beforeUpload(file) {
if(file.name.includes('1.image')) {
return false;
}
return true;
},
onSuccess(data: any) {
console.log('onSuccess', data);
},
onError(err) {
console.log('onError', err);
},
onProgress(percentage) {
console.log('onProgress', percentage);
},
onChange(file) {
console.log('onChange', file);
}
};
const App: React.FC = () => (
<Upload {...props}>
<Button icon={<UploadOutlined />}>上传</Button>
</Upload>
);
export default App;
第四步 添加文件列表
上传文件以后,肯定要在页面上显示咱们都传了什么文件过来,是不是?文件列表写出来是不是就更像那么回事了?抄一下antd的样子,我们实现一个和它一样的东西出来。
我们可以用 mock 的数据写一个 UploadList 组件出来,然后够再用 state 和Upload组件相互连接,开干:
// UploadList.tsx
import { FC } from 'react'
import { Progress } from 'antd';
import { CheckOutlined, CloseOutlined, DeleteOutlined, FileOutlined, LoadingOutlined } from '@ant-design/icons';
export interface UploadFile {
uid: string;
size: number;
name: string;
status?: 'ready' | 'uploading' | 'success' | 'error';
percent?: number;
raw?: File;
response?: any;
error?: any;
}
interface UploadListProps {
fileList: UploadFile[];
}
export const UploadList: FC<UploadListProps> = (props) => {
const {
fileList,
} = props;
return (
<ul className="upload-list">
{
fileList.map(item => {
return (
<li className={`upload-list-item upload-list-item-${item.status}`} key={item.uid}>
<span className='file-name'>
{
(item.status === 'uploading' || item.status === 'ready') &&
<LoadingOutlined />
}
{
item.status === 'success' &&
<CheckOutlined />
}
{
item.status === 'error' &&
<CloseOutlined />
}
{item.name}
</span>
<span className="file-actions">
<DeleteOutlined />
</span>
{
item.status === 'uploading' && <Progress percent={item.percent || 0}/>
}
</li>
)
})
}
</ul>
)
}
export default UploadList;
测试看看
写个样式:之前样式用的css,我们改成scss 好写一些: npm i sass -D ,在vite的框架里面已经帮我们集成了sass需要的环境,比如node-sass,我们就不用再安装它了,直接改改文件名写代码就好。
// index.scss
.upload-btn {
display: inline-block;
}
.upload-file {
display: none;
}
.upload-list {
margin: 0;
padding: 0;
list-style-type: none;
}
.upload-list-item {
margin-top: 5px;
font-size: 12px;
line-height: 2em;
box-sizing: border-box;
min-width: 200px;
position: relative;
&-success {
color: #1677ff;
}
&-error {
color: #ff4d4f;
}
.file-name {
.anticon {
margin-right: 10px;
}
}
.file-actions {
display: none;
position: absolute;
right: 7px;
top: 0;
cursor: pointer;
}
&:hover {
.file-actions {
display: block;
}
}
}
刷新页面如下:
已经很接近antd的案例了,对不?还缺少个删除
测试如下:
现在做的事情就是接入 Upload 组件,删掉之前的 fileList 的 mock 数据,然后重新定义。const [ fileList, setFileList ] = useState<Array<UploadFile>>([]);。
我们要在什么时候加入fileList,有三个地方,第一个是发起请求前就往 fileList里面加入file数据,状态是:uploading,然后当文件上传成功以后,在执行 onSuccess 之前修改 fileList 对应的状态为:success, 当上传失败以后,在onError 之前,修改状态为 error。
测试看看:
完整代码如下:
// Upload/index.tsx
import {ChangeEvent, PropsWithChildren, useRef, useState } from 'react';
import UploadLost, {UploadFile} from './UploadList'
import { InboxOutlined } from '@ant-design/icons';
import Dragger from './Dragger'
import axios from 'axios';
import './index.scss';
export interface UploadProps extends PropsWithChildren {
action: string; // 接口地址
headers?: Record<string, any>; // 请求头
name?: string; // 表单的名字,在formData的时候使用
data?: Record<string, any>; //带给组件的数据
accept?: string; // 文件接受的数据类型
multiple?: boolean; //多文件上传
beforeUpload?: (file: File)=> boolean | Promise<File>;
onProgress?: (percentage: number, file: File) => void;//是进度更新时的回调,可以拿到进度
onSuccess?: (data: any, file: File) => void; //是上传成功的回调。
onError?: (err: any, file: File)=>void;//是上传失败的回调。
onChange?: (file: File) => void; //上传状态改变时的回调。
drag?: boolean;
onRemove?: (file: UploadFile) => void;
}
export const Upload = (props: UploadProps) => {
const {children,
action,
name,
headers,
data,
accept,
multiple,
beforeUpload,
onSuccess,
onError,
onChange,
onProgress,
onRemove,
drag
} = props
const fileRef = useRef<HTMLInputElement>(null);
const [ fileList, setFileList ] = useState<Array<UploadFile>>([]);
const handleChange = () => {
fileRef.current && fileRef.current.click()
}
const uploadFiles = (files: FileList) => {
let postFiles = Array.from(files)
postFiles.forEach(file => {
if(!beforeUpload){
post(file);
return;
}
const result = beforeUpload(file)
if (result && result instanceof Promise) {
result.then(processedFile => {
post(processedFile)
})
return;
}
post(file);
})
}
const handleRemove = (file: UploadFile) => {
setFileList((prevList) => {
return prevList.filter(item => item.uid !== file.uid)
})
if (onRemove) {
onRemove(file)
}
}
const post = (file: File) => {
let uploadFile: UploadFile = {
uid: Date.now() + 'upload-file',
status: 'ready',
name: file.name,
size: file.size,
percent: 0,
raw: file
}
setFileList(prevList => {
return [uploadFile, ...prevList]
})
const formData = new FormData()
formData.append(name || 'file', file);
if (data) {
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
}
//进度直接用 axios 的 onUploadProgress
axios.post(action, formData, {
headers: {
...headers,
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (e) => {
let percentage = Math.round((e.loaded * 100) / e.total!) || 0;
if (percentage < 100) {
updateFileList(uploadFile, { percent: percentage, status: 'uploading'});
if (onProgress) {
onProgress(percentage, file)
}
}
}
}).then((res)=>{
updateFileList(uploadFile, {status: 'success', response: res.data})
onSuccess?.(res.data, file)
onChange?.(file)
}).catch((err)=>{
updateFileList(uploadFile, { status: 'error', error: err})
onError?.(err, file)
onChange?.(file)
})
}
const updateFileList = (updateFile: UploadFile, updateObj: Partial<UploadFile>) => {
setFileList(prevList => {
return prevList.map(file => {
if (file.uid === updateFile.uid) {
return { ...file, ...updateObj }
} else {
return file
}
})
})
}
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
// 获取file,单文件下载就是multiple 为false的时候直接取 let file = e?.target?.files?.[0] as File;
// 多文件用 let file = e?.target?.files
const files = e.target.files
if(!files) {
return
}
uploadFiles(files)
}
return (
<div className="upload-container">
<div className="upload-btn" onClick={handleChange}>
{
drag ? <Dragger onFile={(files) => {uploadFiles(files)}}>
<p>
<InboxOutlined style={{fontSize: '50px'}}/>
</p>
<p>点击或者拖拽文件到此处</p>
</Dragger>
: children
}
<input
className="upload-file"
type="file"
onChange={handleFileChange}
ref={fileRef}
accept={accept}
multiple={multiple}
/>
</div>
<UploadLost
fileList={fileList}
onRemove={handleRemove}
/>
</div>
)
}
export default Upload;
第五步 拖拽上传文件
在 Upload 文件夹下面创建 Dragger.tsx.
1.上传文件的时候,样式会发生频繁的变化,所以需要引入classnames: npm i classNames from "classnames"
2.拖拽我们用的是 div 的
onDragOver={e => { handleDrag(e, true)}}
onDragLeave={e => { handleDrag(e, false)}}
onDrop={handleDrop}
具体代码如下:
import { FC, useState, DragEvent, PropsWithChildren } from 'react'
import classNames from 'classnames'
interface DraggerProps extends PropsWithChildren{
onFile: (files: FileList) => void;
}
export const Dragger: FC<DraggerProps> = (props) => {
const { onFile, children } = props
const [ dragOver, setDragOver ] = useState(false)
const cs = classNames('upload-dragger', {
'is-dragover': dragOver
})
const handleDrop = (e: DragEvent<HTMLElement>) => {
e.preventDefault()
setDragOver(false)
onFile(e.dataTransfer.files)
}
const handleDrag = (e: DragEvent<HTMLElement>, over: boolean) => {
e.preventDefault()
setDragOver(over)
}
return (
<div
className={cs}
onDragOver={e => { handleDrag(e, true)}}
onDragLeave={e => { handleDrag(e, false)}}
onDrop={handleDrop}
>
{children}
</div>
)
}
export default Dragger;
在index.css里面加上样式
.upload-dragger {
background: #eee;
border: 1px dashed #aaa;
border-radius: 4px;
cursor: pointer;
padding: 20px;
width: 200px;
height: 100px;
text-align: center;
&.is-dragover {
border: 2px dashed blue;
background: rgba(blue, .3);
}
}
在Upload/index.js里面引入拖拽
测试如下:
完整代码如下:
服务端:server、index.js
import express from 'express';
import multer from 'multer';
import cors from 'cors';
import iconv from 'iconv-lite';
const app = express()
app.use(cors());
// app用了cors(),就不用每个接口带 res.setHeader('Access-Control-Allow-Origin', '*'); 了
//const upload = multer({ dest: 'uploads/'})
const upload = multer({
storage: multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/')
},
filename: function (req, file, cb) {
// 假设上传的文件名是GBK编码
let filename = iconv.decode(file.originalname, 'UTF-8');
console.log(filename)
cb(null, filename);
}
})
});
app.post('/upload', upload.single('file'), function (req, res, next) {
res.send(JSON.stringify({
code: 200,
message: 'success'
}));
})
app.listen(3002, ()=>{
console.log('接口是:3002')
});
App.tsx
import React from 'react';
import { UploadOutlined } from '@ant-design/icons';
// import type { UploadProps } from 'antd';
import { Button, message } from 'antd';
import Upload, {UploadProps} from './components/Upload'
const props: UploadProps = {
name: 'file', //表单名字
action: 'http://localhost:3002/upload',//接口
headers: {},//请求头
data: {},
multiple: true,
drag: true,
beforeUpload(file) {
if(file.name.includes('1.image')) {
return false;
}
return true;
},
onSuccess(data: any) {
console.log('onSuccess', data);
},
onError(err) {
console.log('onError', err);
},
onProgress(percentage) {
console.log('onProgress', percentage);
},
onChange(file) {
console.log('onChange', file);
}
};
const App: React.FC = () => (
<Upload {...props}>
<Button icon={<UploadOutlined />}>Upload</Button>
</Upload>
);
export default App;
Upload/index.tsx
import {ChangeEvent, PropsWithChildren, useRef, useState } from 'react';
import UploadLost, {UploadFile} from './UploadList'
import { InboxOutlined } from '@ant-design/icons';
import Dragger from './Dragger'
import axios from 'axios';
import './index.scss';
export interface UploadProps extends PropsWithChildren {
action: string; // 接口地址
headers?: Record<string, any>; // 请求头
name?: string; // 表单的名字,在formData的时候使用
data?: Record<string, any>; //带给组件的数据
accept?: string; // 文件接受的数据类型
multiple?: boolean; //多文件上传
beforeUpload?: (file: File)=> boolean | Promise<File>;
onProgress?: (percentage: number, file: File) => void;//是进度更新时的回调,可以拿到进度
onSuccess?: (data: any, file: File) => void; //是上传成功的回调。
onError?: (err: any, file: File)=>void;//是上传失败的回调。
onChange?: (file: File) => void; //上传状态改变时的回调。
drag?: boolean;
onRemove?: (file: UploadFile) => void;
}
export const Upload = (props: UploadProps) => {
const {children,
action,
name,
headers,
data,
accept,
multiple,
beforeUpload,
onSuccess,
onError,
onChange,
onProgress,
onRemove,
drag
} = props
const fileRef = useRef<HTMLInputElement>(null);
const [ fileList, setFileList ] = useState<Array<UploadFile>>([]);
const handleChange = () => {
fileRef.current && fileRef.current.click()
}
const uploadFiles = (files: FileList) => {
let postFiles = Array.from(files)
postFiles.forEach(file => {
if(!beforeUpload){
post(file);
return;
}
const result = beforeUpload(file)
if (result && result instanceof Promise) {
result.then(processedFile => {
post(processedFile)
})
return;
}
post(file);
})
}
const handleRemove = (file: UploadFile) => {
setFileList((prevList) => {
return prevList.filter(item => item.uid !== file.uid)
})
if (onRemove) {
onRemove(file)
}
}
const post = (file: File) => {
let uploadFile: UploadFile = {
uid: Date.now() + 'upload-file',
status: 'ready',
name: file.name,
size: file.size,
percent: 0,
raw: file
}
setFileList(prevList => {
return [uploadFile, ...prevList]
})
const formData = new FormData()
formData.append(name || 'file', file);
if (data) {
Object.keys(data).forEach(key => {
formData.append(key, data[key])
})
}
//进度直接用 axios 的 onUploadProgress
axios.post(action, formData, {
headers: {
...headers,
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (e) => {
let percentage = Math.round((e.loaded * 100) / e.total!) || 0;
if (percentage < 100) {
updateFileList(uploadFile, { percent: percentage, status: 'uploading'});
if (onProgress) {
onProgress(percentage, file)
}
}
}
}).then((res)=>{
updateFileList(uploadFile, {status: 'success', response: res.data})
onSuccess?.(res.data, file)
onChange?.(file)
}).catch((err)=>{
updateFileList(uploadFile, { status: 'error', error: err})
onError?.(err, file)
onChange?.(file)
})
}
const updateFileList = (updateFile: UploadFile, updateObj: Partial<UploadFile>) => {
setFileList(prevList => {
return prevList.map(file => {
if (file.uid === updateFile.uid) {
return { ...file, ...updateObj }
} else {
return file
}
})
})
}
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
// 获取file,单文件下载就是multiple 为false的时候直接取 let file = e?.target?.files?.[0] as File;
// 多文件用 let file = e?.target?.files
const files = e.target.files
if(!files) {
return
}
uploadFiles(files)
}
return (
<div className="upload-container">
<div className="upload-btn" onClick={handleChange}>
{
drag ? <Dragger onFile={(files) => {uploadFiles(files)}}>
<p>
<InboxOutlined style={{fontSize: '50px'}}/>
</p>
<p>点击或者拖拽文件到此处</p>
</Dragger>
: children
}
<input
className="upload-file"
type="file"
onChange={handleFileChange}
ref={fileRef}
accept={accept}
multiple={multiple}
/>
</div>
<UploadLost
fileList={fileList}
onRemove={handleRemove}
/>
</div>
)
}
export default Upload;
Upload/index.scss
.upload-btn {
display: inline-block;
}
.upload-file {
display: none;
}
.upload-list {
margin: 0;
padding: 0;
list-style-type: none;
}
.upload-list-item {
margin-top: 5px;
font-size: 12px;
line-height: 2em;
box-sizing: border-box;
min-width: 200px;
position: relative;
&-success {
color: #1677ff;
}
&-error {
color: #ff4d4f;
}
.file-name {
.anticon {
margin-right: 10px;
}
}
.file-actions {
display: none;
position: absolute;
right: 7px;
top: 0;
cursor: pointer;
}
&:hover {
.file-actions {
display: block;
}
}
}
.upload-dragger {
background: #eee;
border: 1px dashed #aaa;
border-radius: 4px;
cursor: pointer;
padding: 20px;
width: 200px;
height: 100px;
text-align: center;
&.is-dragover {
border: 2px dashed blue;
background: rgba(blue, .3);
}
}
Upload/UploadList.tsx
import { FC } from 'react'
import { Progress } from 'antd';
import { CheckOutlined, CloseOutlined, DeleteOutlined, FileOutlined, LoadingOutlined } from '@ant-design/icons';
export interface UploadFile {
uid: string;
size: number;
name: string;
status?: 'ready' | 'uploading' | 'success' | 'error';
percent?: number;
raw?: File;
response?: any;
error?: any;
}
interface UploadListProps {
fileList: UploadFile[];
onRemove: (file: UploadFile)=>void;
}
export const UploadList: FC<UploadListProps> = (props) => {
const {
fileList,
onRemove
} = props;
return (
<ul className="upload-list">
{
fileList.map(item => {
return (
<li className={`upload-list-item upload-list-item-${item.status}`} key={item.uid}>
<span className='file-name'>
{
(item.status === 'uploading' || item.status === 'ready') &&
<LoadingOutlined />
}
{
item.status === 'success' &&
<CheckOutlined />
}
{
item.status === 'error' &&
<DeleteOutlined onClick={() => { onRemove(item)}}/>
}
{item.name}
</span>
<span className="file-actions">
<DeleteOutlined />
</span>
{
item.status === 'uploading' && <Progress percent={item.percent || 0} size="small" />
}
</li>
)
})
}
</ul>
)
}
export default UploadList;
Upload/Dragger.tsx
import { FC, useState, DragEvent, PropsWithChildren } from 'react'
import classNames from 'classnames'
interface DraggerProps extends PropsWithChildren{
onFile: (files: FileList) => void;
}
export const Dragger: FC<DraggerProps> = (props) => {
const { onFile, children } = props
const [ dragOver, setDragOver ] = useState(false)
const cs = classNames('upload-dragger', {
'is-dragover': dragOver
})
const handleDrop = (e: DragEvent<HTMLElement>) => {
e.preventDefault()
setDragOver(false)
onFile(e.dataTransfer.files)
}
const handleDrag = (e: DragEvent<HTMLElement>, over: boolean) => {
e.preventDefault()
setDragOver(over)
}
return (
<div
className={cs}
onDragOver={e => { handleDrag(e, true)}}
onDragLeave={e => { handleDrag(e, false)}}
onDrop={handleDrop}
>
{children}
</div>
)
}
export default Dragger;