开发背景
使用React + Antd 完成一个上传图片的页面,选用Antd的Upload组件,若是选择超过100张图片或者更多的情况,文件回显到列表上需要5s以上,一次性选择500张图更是需要60s左右才会回显到列表上。浏览器很容易就崩溃卡死了,而且在等待文件回显的过程中,点击页面其他均无响应。使用效果极其不友好。
分析了下原因:
Upload的onChange事件,每一次选择文件都会响应,若是一次性选择多个文件,则会响应多次,且每个文件都触发onChange事件结束后,才回回显到列表上。
onChange事件期间使用useState的修改字段来改变某些字段值,虽然能改变成功,但是页面无响应。我还未找到原因。
使用input封装一个上传组件
优点
选择文件响应极其快,上千的文件1-2s就可以回显。若是需要生成缩略图的,则是回显到列表后去异步显示缩略图。
代码
为了保持和项目使用组件统一,我写了和Antd Upload相似的样式。
ts
/**
*@description 自己封装的上传文件组件
*@author cy
*@date 2022-05-18 13:52
**/
import React, { useRef } from 'react';
import { Button, Col, message, Row, Typography, Upload } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { beforeUploadLimit } from '@utils/CommonFunc';
import './myUpload.less';
const { Text } = Typography;
/**
* limitType: 限制文件的 格式
* file: 文件
* limitSize: 文件限制大小(MB)
* limitFileNameLength: 限制文件名长度
* limitFileName: 文件名中不应包含字符
**/
export const beforeUploadLimit = (limitType: Array<string>, file: any, limitSize?: number | 'none', limitFileNameLength?: number, limitFileName?: Array<string>) => {
if (limitSize !== 'none') {
let fileSize = limitSize ? limitSize : 40;
const isLtLimitSize = file.size / 1024 / 1024 < fileSize;
// 限制文件大小
if (!isLtLimitSize) {
message.error({
content: '文件不能超过 ' + fileSize + ' MB',
key: 'fileSize'
});
return Upload.LIST_IGNORE;
}
}
// 限制文件格式
let fileSuf = file.name.split('.');
let suffix = fileSuf[fileSuf.length - 1].toLowerCase();
if (limitType.indexOf('.' + suffix) === -1) {
message.error({
content: '文件限' + limitType.join('、') + '格式',
key: 'fileType'
});
return Upload.LIST_IGNORE;
}
let nameLength = limitFileNameLength ? limitFileNameLength : 100;
// 限制文件名长度
if (file.name.length > nameLength) {
message.error({
content: '文件名长度不能超过 ' + nameLength + ' 字',
key: 'fileLength'
});
return Upload.LIST_IGNORE;
}
let nameLimit = limitFileName ? limitFileName : ['&', '+', '=', '#', '%'];
// 限制文件名中不应包含字符
for (let i = 0; i < nameLimit.length; i++) {
const item = nameLimit[i];
if (file.name.indexOf(item) !== -1) {
message.error({
content: '文件名中不应包含字符 ' + nameLimit.join(' ') + ' 字符',
key: 'fileCode'
});
return Upload.LIST_IGNORE;
}
}
return true;
};
export enum EFileStatus {
ready,
uploading = 'uploading',
success = 'success',
done = 'done',
error = 'error'
}
interface IProps {
onChange: (fileList: Array<any>) => void;
showFileList: Array<any>;
multiple?: boolean; // 是否多文件
maxCount?: number; // 最多选择文件
onRemove?: (file: any) => void; // 删除文件
fileSpan?: number; // 单个文件占的flex大小
uploadItemClass?: string;
listType?: 'text' | 'picture' | 'length';
accept?: any; // 文件格式
children?: React.ReactNode;
}
const MyUpload = (props: IProps) => {
const {
multiple = false, maxCount = 1, onRemove, onChange, showFileList = [], fileSpan = 24, uploadItemClass,
listType = 'text', accept, children
} = props;
const inputRef: any = useRef();
const fileBeforeShow = (file: any) => {
let index = showFileList.findIndex((item: any) => {
return item.name === file.name;
});
if (index > -1) {
message.error({ key: 'sameName', content: '图片不能重名' });
return Upload.LIST_IGNORE;
}
return beforeUploadLimit(accept, file, 'none', 200);
};
const fileChange = (e: any) => {
let files = inputRef.current.files;
let dayValue = dayjs().valueOf();
let canChooseNum = maxCount - showFileList.length; // 限制文件数量
let newFileList: Array<any> = [];
for (let fileIndex in files) {
if (files.hasOwnProperty(fileIndex) && fileBeforeShow(files[fileIndex]) === true && newFileList.length < canChooseNum) {
let sourceObj = {
status: EFileStatus.ready,
uid: dayValue + '-' + fileIndex
};
// 若是图片形式展示,需要设置缩略图
if (listType === 'picture') {
let url = URL.createObjectURL(files[fileIndex]);
sourceObj.thumbnailPath = url;
}
let obj = Object.assign(files[fileIndex], sourceObj);
newFileList.push(obj);
}
}
onChange([...showFileList, ...newFileList]);
};
const onFileRemove = (file: any) => {
const list = [...showFileList];
let fileIndex = list.findIndex((item: any) => item.uid === file.uid);
if (fileIndex > -1) {
list.splice(fileIndex, 1);
onChange([...list]);
}
onRemove && onRemove(file);
};
const inputClick = () => {
inputRef.current.click();
};
return (
<>
<div>
<div className={uploadItemClass}>
{listType !== 'picture' && (
children ? (
<div onClick={inputClick}>{children}</div>
) : (
<>
<Button disabled={showFileList.length >= maxCount} onClick={inputClick}>上传文件</Button>
<Text style={{ marginLeft: 5 }}>已选择 {showFileList.length} 张图片</Text>
</>
)
)}
<input
type="file" style={{ display: 'none' }}
id="inputFile" multiple={multiple}
onChange={fileChange} ref={inputRef}
accept={accept}
/>
</div>
{listType === 'picture' && (
<div className="my-upload-picture-wrap">
<div className="my-upload-select-picture-card">
{children ? (
<div onClick={inputClick} className={`${listType === 'picture' ? 'my-upload' : ''}`}>{children}</div>
) : (
<>
<Button disabled={showFileList.length >= maxCount} onClick={inputClick}>上传文件</Button>
<Text style={{ marginLeft: 5 }}>已选择 {showFileList.length} 张图片</Text>
</>
)}
</div>
{showFileList.map((item: any) => (
<div key={item.uid} className="my-upload-item-picture-div">
<div className={`my-upload-list-picture ${item.status === EFileStatus.success && 'my-upload-item-done'}
${item.status === EFileStatus.error && 'my-upload-item-error'}`}>
<div className="my-upload-item-picture">
<img src={item.thumbnailPath} width="100%" height="auto" style={{ maxHeight: '100%', objectFit: 'contain' }} />
<Button
className="my-upload-item-pic-btn"
type="link"
icon={<DeleteOutlined style={{ color: '#fff' }} />}
onClick={() => onFileRemove(item)}
title="删除文件"
/>
</div>
</div>
</div>
))}
</div>
)}
{listType === 'text' && (
<Row wrap={true} className="my-upload">
{showFileList.map((item: any) => (
<Col span={fileSpan} key={item.uid} className="my-upload-list-item">
<Text className={`my-upload-item-span ${item.status === EFileStatus.success && 'my-upload-item-done'} ${item.status === EFileStatus.error && 'my-upload-item-error'}`}>{item.name}</Text>
<Button
className="my-upload-item-btn"
type="link"
icon={<DeleteOutlined />}
onClick={() => onFileRemove(item)}
title="删除文件"
/>
</Col>
))}
</Row>
)}
</div>
</>
);
};
export default MyUpload;
css
@primary-color: #1890ff;
.my-upload-select-picture-card {
width: 100px;
height: 100px;
margin-right: 8px;
margin-bottom: 8px;
text-align: center;
vertical-align: top;
background-color: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 2px;
cursor: pointer;
transition: border-color .3s;
}
.my-upload-select-picture-card:hover {
border-color: @primary-color;
}
.my-upload-select-picture-card>.my-upload {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
}
//.my-upload {
.my-upload-list-item {
height: 100%;
padding: 0 4px;
transition: background-color 0.3s;
display: flex;
align-items: center;
}
.my-upload-list-item:hover {
background-color: #eee;
}
.my-upload-item-btn {
opacity: 0;
}
.my-upload-list-item:hover .my-upload-item-btn {
opacity: 1;
}
.my-upload-item-span {
flex: auto;
margin: 0;
//padding: 0 8px;
display: inline-block;
width: 100%;
padding-left: 22px;
overflow: hidden;
line-height: 1.5715;
white-space: nowrap;
text-overflow: ellipsis;
}
.my-upload-picture-wrap {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
}
.my-upload-item-picture-div {
width: 100px;
height: 100px;
//padding: 8px;
//border: 1px solid #d9d9d9;
//border-radius: 2px;
margin: 0 8px 8px 0;
vertical-align: top;
}
.my-upload-item-picture-div:hover {
background: 0 0;
}
.my-upload-item-picture-div:hover .my-upload-item-picture {
background: 0 0;
}
.my-upload-item-pic-btn {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
white-space: nowrap;
transform: translate(-50%,-50%);
opacity: 0;
transition: all .3s;
}
.my-upload-item-picture-div:hover .my-upload-item-pic-btn {
opacity: 1;
}
.my-upload-item-picture {
position: relative;
height: 100%;
overflow: hidden;
transition: background-color .3s;
display: flex;
align-items: center;
}
// 图片上传的外框
.my-upload-list-picture {
height: 100%;
margin-top: 0;
position: relative;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
}
// 鼠标的hover事件给图片添加蒙层
.my-upload-item-picture:before {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
background-color: #00000080;
opacity: 0;
transition: all .3s;
content: " ";
}
.my-upload-item-picture-div:hover .my-upload-item-picture:before {
opacity: 1;
}
// 文字列表的上传成功
.my-upload-item-span.my-upload-item-done:before {
content: '√';
font-weight: 600;
color: #fff;
background-color: #52c41a;
border-radius: 50%;
width: 20px;
height: 20px;
left: 2px;
position: absolute;
padding-left: 5px;
}
// 图片列表的上传成功
.my-upload-list-picture.my-upload-item-done:after {
content: '√';
font-weight: 600;
color: #fff;
background-color: #52c41a;
border-radius: 50%;
width: 20px;
height: 20px;
left: 2px;
top: 0;
position: absolute;
padding-left: 5px;
}
// 文字列表的上传失败
.my-upload-item-span.my-upload-item-error:before {
content: '×';
font-weight: 600;
color: #fff;
background-color: #f11c49;
border-radius: 50%;
width: 20px;
height: 20px;
left: 2px;
position: absolute;
padding-left: 5px;
}
// 图片展示的 上传失败
.my-upload-list-picture.my-upload-item-error:after {
content: '×';
font-weight: 600;
color: #fff;
background-color: #f11c49;
border-radius: 50%;
width: 20px;
height: 20px;
left: 2px;
top: 0;
position: absolute;
padding-left: 5px;
}
使用
const [fileList, setFileList] = useState<Array<any>>([]); // 准备上传的文件
const fileChange = (files: Array<any>) => {
setFileList(files);
};
return (
<MyUpload
onChange={fileChange}
showFileList={fileList}
onRemove={onFileRemove}
multiple={true}
maxCount={uploadMaxLimitNum}
fileSpan={8}
listType="picture"
accept={fileAccept.img.join(',')}>
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</div>
</MyUpload>
)