React 使用input替换Antd的Upload组件,解决Upload选择超量文件,浏览器卡死崩溃问题

1,054 阅读4分钟

开发背景

使用React + Antd 完成一个上传图片的页面,选用Antd的Upload组件,若是选择超过100张图片或者更多的情况,文件回显到列表上需要5s以上,一次性选择500张图更是需要60s左右才会回显到列表上。浏览器很容易就崩溃卡死了,而且在等待文件回显的过程中,点击页面其他均无响应。使用效果极其不友好。

image.png

分析了下原因:

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>
)

image.png