手撸React组件「Upload」文件上传

4,364 阅读4分钟

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

Consic是一个简单易用轻量级React组件库
本文记录了我如何实现Upload组件
附上组件源码链接!!!

写在前面(小声)

准大三菜鸟一枚,文章记录了我在封装Upload组件时的一些想法以及思考,如果代码有什么不好的地方希望能够帮忙指出(一定会好好回复!)👂

组件展示(duang duang duang)

image.png

支持的API列表!

NameDescriptionTypeDefault
accept允许的文件类型,参考MDN(accept)属性string*
multiple是否支持多选booleanfalse
limit限制上传文件的数量number无限制
disable是否禁用上传booleanfalse
name上传文件的键名stringfile
action远程上传地址string
autoUpload是否自动上传booleantrue
withCredentials是否携带cookie上传booleanfalse
defaultFileList默认文件列表FileItem[][]
headers发送请求时自定义请求头HeadersInit{}
beforeUpload上传前回调函数(file: File) => Awaitable<boolean | void>noop
onRemove移除后回调函数(fileItem: FileItem, fileList: FileItem[]) => voidnoop
onChange文件列表变动时回调函数(fileList: FileItem[]) => voidnoop
onSuccess上传成功回调函数(response: any, fileItem: FileItem, fileList: FileItem[]) => voidnoop
onError上传失败回调函数(error: any, fileItem: FileItem, fileList: FileItem[]) => voidnoop
onExceedLimit超出限制上传数量时回调(fileList: FileItem[], uploadFiles: FileList) => voidnoop

开始我们的码代码之旅!

静态结构展示

Upload组件的结构比较简单(目前),因为还没做上传图片时的特殊展示(肯定不是因为我懒!),结构部分概括就是div包裹住用于可以触发上传文件的input标签(需要隐藏,然后使用click()触发上传事件)以及用于点击的Button按钮和展示上传文件的List组件,具体代码如下:

<div>
  <input
    accept={accept}
    multiple={multiple}
    onChange={fileChange}
    type="file"
    ref={inputRef}
    style={{ display: 'none' }}
  ></input>
  <Button handleClick={uploadClick} style={{ margin: '8px' }} type="primary">
    <UploadOutlined />
    上传
  </Button>
  {fileList.length !== 0 && (
    <List
      style={{ fontSize: '14px', width: '100%', marginTop: '10px' }}
      header="文件列表"
      size="small"
      dataSource={fileList}
      render={(_: FileItem, idx: number) => {
        return (
          <List.Item
            key={idx}
            style={{ fontSize: '14px', display: 'flex', justifyContent: 'space-between'}}>
            <span style={{ color: _.status === 'success' ? '#00b42a' : '#d81f27' }}>
              {_.name}
            </span>
            <span>
              {_.status === 'unUpload' ? (
                <Button
                  handleClick={() => uploadFile(_.file, _)}
                  style={{  height: '100%', fontSize: '12px', }}
                > 上传 </Button>
              ) : ( <CheckCircleFilled style={{ color: '#00b42a' }} / )}
              <CloseCircleFilled
                onClick={(e) => {  e.stopPropagation();  deleteFile(_); }}
                style={{ color: '#d81f27', marginLeft: '5px' }}
              />
            </span>
          </List.Item>
        );
      }}
    ></List>
  )}
</div>

功能代码

Upload组件结构相对比较简单,还是快点开始具体的代码实现吧嘿嘿,我会按上传流程来讲解一个个函数各自的功能

uploadClick()函数

该函数的用于点击上传按钮后,通过inputRef绑定的DOM触发上传事件,在触发前会对disable进行判断。下面贴代码:

const uploadClick = () => {
  if (disable) return;
  inputRef.current.click();
};

fileChange()函数

当选择完文件并确定后,触发input绑定的onChange事件,选择的文件通过e.target.files 拿到,对文件数组进行遍历, limit传参时,当上传文件总数大于等于limit,触发自定义事件onExceedLimit并跳出循环,在循环中,将File包裹,并添加uid(通过web自带函数 crypto.randomUUID()生成)|status等属性,如果用户设置了autoUpload,再此刻就会触发文件上传。代码如下:

const fileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
  const files = e.target.files || [];
  const list: FileItem[] = [];
  for (let i = 0; i < files.length; i++) {
    if (isNumber(limit) && fileList.length + list.length >= limit) {
      onExceedLimit && onExceedLimit(fileList, files as FileList);
      break;
    }
    list.push({
      uid: crypto.randomUUID(),
      status: 'unUpload',
      name: files[i].name,
      file: files[i],
    });
    autoUpload && (await uploadFile(files[i], list[list.length - 1]));
  }
  inputRef.current.value = '';
  setFileList([...fileList, ...list]);
};

beforeUploadFile()函数

假设上面中autoUpload=true,进入上传文件函数uploadFile,函数首行调用beforeUploadFile,函数支持用户返回Promise的reject以及false,如果这样,则不会上传文件。具体代码如下:

const beforeUploadFile = async (file: File): Promise<boolean> => {
  if (beforeUpload) {
    try {
      const res = await beforeUpload(file);
      return !(typeof res === 'boolean' && !res);
    } catch (e) {
      return false;
    }
  } else {
    return true;
  }
};

uploadFile()函数

函数参数是目标文件,以及FileItem,在这里我选择浏览器环境自带的fetch进行文件上传,fetch配置项包括,上传地址action,是否携带cookiewithCredentials,请求头自定义headers,请求回调中,then中,更改上传成功的文件statussuccess,然后调用用户的自定义函数onSuccesscatch中,更改上传成功的文件statuserror,然后调用用户的自定义函数onError。具体代码如下:

const uploadFile = async (file: File, fileItem: FileItem) => {
  if (!(await beforeUploadFile(file))) return;
  const formData = new FormData();
  formData.append(name, file);
  fetch(action, {
    method: 'POST',
    body: formData,
    credentials: withCredentials ? 'include' : 'omit',
    headers,
  })
    .then((response) => response)
    .then((data) => {
      setFileList((fileList) => {
        return fileList.map((_) => {
          if (_.uid === fileItem.uid) {
            return { ..._,status: 'success',};
          } else return _;
        });
      });
      onSuccess && onSuccess(data, fileItem, fileList);
    })
    .catch((error) => {
      setFileList((fileList) => {
        return fileList.map((_) => {
          if (_.uid === fileItem.uid) {
            return { ..._,status: 'error',};
          } else return _;
        });
      });
      onError && onError(error, fileItem, fileList);
    });
};

deleteFile()函数

const deleteFile = (fileItem: FileItem) => {
  setFileList((fileList) => fileList.filter((_) => _.uid !== fileItem.uid));
  onRemove && onRemove(fileItem, fileList);
};

FLAG

  • 支持图片文件的展示
  • 图片可预览
  • 图片上传前可裁剪
  • 支持List的自定义render

这是我在掘金更新的有关组件的第三篇文章了,我知道代码中有挺多不规范和不合理的地方,希望大家可以多多指出!!

系列链接
手撸你的第一个组件
手撸React组件「TimePicker」
手撸React组件「Upload」文件上传

源码链接

源码链接

线上文档

Concis点个star⭐再走啊!!!