前言
我们的后台系统都是基于 Antd Design 开发的。最近做的新系统里有比较多的场景需要使用到附件上传的功能,我们针对 Antd 的 <Upload />
组件在项目里进行了业务的封装。过程中也碰到些问题,遂总结于本文中。
基本使用
我们主要是用到了它多文件上传和功能。
import React from 'react';
import { Upload } from 'antd';
return () => {
const [fileList, setFileList] = useState([
{
uid: '1',
name: '1.txt',
status: 'done',
url: 'https://www.baidu.com',
},
]);
const handleChange = info => {
let fileList = info.fileList.slice();
fileList = fileList.map(file => {
if (file.response) {
// Component will show file.url as link
file.url = file.response.url;
}
return file;
});
setFileList(fileList);
};
return (
<Upload
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
fileList={fileList}
onChange={handleChange}
/>
);
};
这是官方文档中提供的示例,我们可以通过 action
属性定义上传的地址,通过 onChange
获取上传后的文件地址以及 fileList
设置上传文件。其中 onChange
以及 fileList
参数类型如下。
import { RcFile as OriRcFile } from 'rc-upload/es/interface';
export interface UploadFile<T = any> {
uid: string;
size?: number;
name: string;
fileName?: string;
lastModified?: number;
lastModifiedDate?: Date;
url?: string;
status?: UploadFileStatus;
percent?: number;
thumbUrl?: string;
originFileObj?: RcFile;
response?: T;
error?: any;
linkProps?: any;
type?: string;
xhr?: T;
preview?: string;
}
export interface RcFile extends OriRcFile {
readonly lastModifiedDate: Date;
}
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
export interface UploadChangeParam<T extends object = UploadFile> {
// https://github.com/ant-design/ant-design/issues/14420
file: T;
fileList: UploadFile[];
event?: { percent: number };
}
UploadFile
是主要的类型,最外层是组件包装的一些数据,包括 status
, percent
等用于记录下载状态字段。其中还有 response
字段,当下载完成 status === 'done'
的时候,该字段会存储服务端返回的相应数据。
需求描述
中后台场景会有大量的表单场景,其中我们的大部分附件提交都是在表单之中。当然我们的表单也是使用的 Antd 组件。它提供了类似于原生 <form />
的一套模式,你不需要关心表单的交互,当使用 <Form.Item name="" />
包裹之后,就会自动认为你是表单元素,在 onFinish
事件中可以达到所有提交后的表单数据。而通过initialValues
属性又可以对整个表单设置初值。简单的通过这两个属性就可以实现表单的大多数需求。
impoprt React from 'react';
import {Form, Input, Upload} from 'antd';
export default function() {
const initialValues = {
remark: 'hello',
attachment: {
attachmentNo: 1234,
fileKey: 2345
}
};
return (
<Form onFinish={onFinish} initialValues={initialValues}>
<Form.Item name="remark" label="说明">
<Input.Textarea />
</Form>
<Form.Item name="attachment" label="附件">
<Upload />
</Form>
<Button>提交</Button>
</Form>
);
}
这套表单的方式让上层交互变的非常纯粹,所以我期望我们封装的组件也最好能适配这套逻辑。而这里的矛盾点在于,我们需要的是 UploadFile['response']['data']
中的数据,但是当我们要给它赋值的时候,它接收的是 UploadFile[]
的数据格式。所以除了封装业务的配置之外,还需要将数据格式转换的逻辑封装进去。
思考实现
最开始我想的设想类似于下面这个 Demo,只需要定义 uploadFile2value
和 value2UploadFile
两个方法,用于处理数据的双向转换即可。
import { Upload } from 'antd';
const Upload = React.memo(({onChange}) => (
<Upload
fileList={value2UploadFile}
onChange={e => onChange(uploadFile2value(e.fileList))}
/>
));
但是我没想到的是,文件上传是一个异步的过程,最终 onChange
接收到的 fileList
数据是一组多状态数据的集合,具体的状态列表如下。
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
根据预期效果,显然我想要的是 status=done
后的数据。而如果我在 uploadFile2value
中对数据做过滤仅将 status=done
的数据返回给出去的话,在之后的渲染中 initialValues
传过来的初始数据中则不包含其它状态的数据了,会导致传入的 fileList
数据异常。这样我们就陷入了一种死循环,刚上传文件文件状态是 uploading
然后被 onChange
过滤为空数据传出,之后空数据作为初始数据再次被传入上传中的文件状态丢失组件回复到初始状态……
最终实现
后台维护组件库的小伙伴提醒了我,既然组件本身需要所有的数据,而外部只需要上传完成的数据,那我们可以考虑将所有的数据在组件内部自行维护,仅将外部组件需要的数据传递出去。当外部数据传入进来的时候,将其与内部数据做合并即可。
import React, { useEffect, useState } from 'react';
import { Upload } from 'antd';
const value2UploadFile = record => ({
uid: record.id,
name: record.name,
status: 'done',
response: { code: 0, msg: '', data: record }
});
function useUpload(files, onChange) {
const [filePool,setFilePool] = useState([]);
useEffect(() => {
if(!Array.isArray(files) || files.length === 0) {
return;
}
setFilePool(filePool => {
const fileIds = filePool.filter(({status}) => status === 'done').map(file => file.response?.data?.id);
const appendFiles = files.filter(({id}) => !fileIds.includes(id)).map(value2UploadFile);
return [...filePool, ...appendFiles];
});
}, [files]);
const handleUploadChange = ({fileList}) => {
fileList.filter(({status, response}) =>
status === 'done' && response.code !== 0
).forEach(file => {
file.status = 'error';
});
setFilePool(fileList);
const doneFiles = fileList.filter(({status}) => status === 'done').map(file => file.response.data);
onChange(doneFiles);
}
return [filePool, handleUploadChange];
}
export default function({value, onChange, ...props}) {
const [filePool, onFileChange] = useUpload(value, onChange);
return (
<Upload
listType="picture"
btnType="default"
btnText="上传文件"
{...props}
fileList={filePool}
onChange={handleUploadChange}
withCredentials
action="/api/file/upload"
/>
);
}
可以看到我们内部增加了 filePool
的状态用来存储数据,每次内部都会全量的存储待上传的文件列表,但是最终调用外部的 onChange
方法回传出去的时候则只会传出 status=done
的数据。而针对赋值的场景,我们鉴定了 files
的变化,根据最终返回数据的 id 获取到 fileIds
内部已存在的文件,然后再使用这个和传入的数据进行 diff 比较,查看是否有新增的数据。如果存在新增的数据则将其转换成组件需要的数据格式后更新文件列表。
通过以上操作,我们就将上传组件的逻辑封装在了内部组件中。甚至我们还能在内部增加当接口返回非 0 的 code 上传失败的时候我们会将组件数据状态修改为 error
而不是 done
。最终外部组件不需要关心上传接口本身内部的逻辑,只需要关系上传之后得到的数据即可,达到了业务上传组件解耦的目的。
看到80%的WordPress博客,都是讲前端开发的
@银行理财 , 可是我的也不是 WP 呀……