基于antd二次封装手动上传组件
背景
上传下载的场景中,需要封装一个易用的上传下载组件
要求:上传前校验文件类型(白名单),上传时显示上传进度,跟踪上传结果,上传成功则显示下载和删除icon;失败则文件标红,并且只能删除,无法提交最终上传结果。
由于接口问题,需要自定义自己的上传实现,无法使用Upload组件的默认上传行为。
部分代码实现
UploadCp.jsx
生命周期
jsx
componentDidMount (){
this.props.wrappedComponentRef(this); // 绑定实例
}
其他主要代码
jsx
// ...相关依赖导入
export default class UploadCp extends PureComponent {
constructor(props){
super(props);
this.state = {
fileList: [], // 手动维护的上传文件列表
}
}
// 需要手动派发上传状态钩子
handleUpload = async ({file, onError, onProgress, onSucess}) => {
try{
await this.validateAttachmentInfo(file);
} catch (error) {
onError(); // 上传前校验不通过
return
}
const formData = new FormData();
formData.append("file", file);
// 自定义上传
axios
.post(this.props.uploadUrl, formData,{
// 请求头 需要指定数据流类型和携带令牌
headers:{
Content-Type: "multipart/form-data",
authorization: "authorization-text", //需要根据实际情况提供有效的身份验证令牌
Jumpcloud-allowToken: "xxx",
},
onUploadProgress:({ total, loaded}) => {
// 监听上传进度
onProgress(
{
percent: Math.round((loaded / total) * 100).toFixed(2),
},
file
)
}
})
.then((res) => {
try{
// 上传回调的判断,注意;可能上传成功,但数据库没更新或其他原因,仍判定为上传失败
if(res.repType === "Success"){
// 成功
onSucess(res, file);
}else{
onError(res, file);
}
}cache(err){
throw new Error(`${fileName}上传失败`); // 其他报错=>推入上传失败条件
}
})
.catch(err => {
// 捕获所有的错误信息
// ... do some thing
onError(err, file); // 更改附件状态
})
.finally(() => {
// do some thing
// 取消页面loading啥的
})
}
// 上传文件的组件配置
uploadProps = {
method: "POST",
name: "file",
customRequest: (file) => this.handleUpload(file), // 覆盖默认的上传行为,实现自定义手动上传!
showUploadList: this.props.showUploadList || false; // 没指定展示列表时,默认不展示
}
handleChange = (info) => {
const fileList = [...info.fileList];
const attachIdList = getAttachIdListHandle(info.fileList); // 确认上传的文件标识集合
this.props.upLoadAttachIdHandle(attachIdList);
this.setState({fileList}); // 手动更新上传列表
}
render(){
return (
<Upload
className="upload_button"
{...this.uploadProps}
multiple
fileList={this.state.fileList}
onRemove={file => this.dispatchDelAttachment(file)}
onDownload={file => this.downloadHandle(file)}
onChange={this.handleChange}
>
<Button type="primary" size="small" > 附件上传 </Button>
</Upload>
)
}
}
细节
- 如果"上传失败" ,onError函数需要传入一个Error对象,这样鼠标移上去显示的失败信息会是Error对象的message字段
error 默认显示接口失败返回报文
判定失败的时候,需要file的 status为 'error',
上传前的校验函数封装
校验上传类型白名单
js
const fileList = [
"xls",
"xlsx",
"doc",
"docx",
"pdf",
"txt",
"png",
"jpeg",
"jpg",
"csv",
"ppt",
"pptx",
"zip",
"rar",
"tar",
"gz",
"bmp"
]
export default fileList;
校验函数
jsx
validateUploadFileName = (fileType = "") => uploadWhiteList.includes(fileType);
validateAttachmentInfo = async (file) => {
const fileName = `${file.name.replace(/\s+/g, "")}`;
const fileType = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length);
// 上传的文件格式校验(白名单)
if(!this.validateUploadFileName(fileType)){
// 提示
return Promise.reject(false);
};
// 文件名称超长校验
if(new TextEncoder("utf8").encode(fileName).length > 200){
// 提示
return Promise.reject(false);
};
// 文件过大
if(file.size > 1024 * 1024 * 20){
// 提示
return Promise.reject(false);
};
// 空文件 或 文件命名错误
if(file.size < 1 || !~(fileName.indexOf(".")){
// 提示
return Promise.reject(false);
};
return Promise.resolve(true); // 校验通过
}
删除功能部分
jsx
// 删除附件
delAttachment = ({resolve, reject, attachId}) => {
const data = { attachId };
// 接口请求
// connect.succecc( resolve() ).failed( reject() );
}
dispatchDelAttachment = (file) => {
const {status } = file;
// 上传失败的不需要调用接口删除,前端删除即可
if(status === "error"){
return null
}
return new Promise((resolve, reject) => {
const { attachId } = file; // 后端回传的唯一标识
const delInfo = {resolve, reject, attachId};
// 删除前要先向用户确认下
Modal.confirm({
mask: true, // 需要遮罩 可以通过maskStyle配置 {backgroundColor: '#ff0000'}
maskClosable: false, // 禁止点击蒙层关闭(防止误操作)
title: "确认提示",
content: `请确认是否删除<${ fileName }}>`,
oKText: "确认",
cancelText: "取消",
onOk: () => this.delAttachment(delInfo),
})
})
}
下载部分
下载函数封装:
jsx
dispatchDownloadAttachment = (data, fileName) => {
new Promise(resolve => {
// 详细见下载接口封装
downloadFileRequest(url, fileName, data, (res) => {
return resolve(res);
})
})
}
downloadHandle = async (file) => {
// 解构出文件标识和文件名 用于下载
const { attachId, fileName } = file;
const data = { attachId };
const { res, msg } = await this.dispatchDownloadAttachment(data, fileName);
const messageType = res ? "success" :"error";
message[messageType](`${msg}`);
}
下载接口封装:
js
export async function downloadFileRequest(
processUrl,
fileName,
reqBody,
callbackFunc
) {
const formData = new FormData();
// 组装请求体
const requHeader = { TRAN_PROCESS: "", TRAIN_ID: "" };
const param = { REQ_HEAD: reqHeader, REQ_BODY: reqBody };
formData.append("REQ_MESSAGE", JSON.stringify(param));
// 使用xhr下载
const xhr = new XMLHttpRequest();
xhr.open("POST", processUrl, true); // 参数3:程序会继续执行后续代码,而不会等待请求完成。
xhr.setRequestHeader("allowToken", "xxx"); // 添加令牌
xhr.responseType = "arraybuffer"; // 希望以二进制数据的形式接收响应数据。=> 响应的头部Content-Type: application/octet-stream 则设置成功,服务器返回的是二进制数据,即使用了 arraybuffer 的响应类型。
xhr.onload = function (e) {
if (this.status === 200) {
const type = xhr.getResponseHeader("Content-Type"); // 通过此字段判断是否下载成功
if (!type) {
// ...
callbackFunc({ res: false, msg: "返回类型为空,下载失败" });
return;
}
// 下载
const blob = new Blob([this.response], { type });
if (typeof window.navigator.msSaveBlob) {
window.navigator.msSaveBlob(blob, filename);
} else {
const URL = window.URL || window.webkitURL;
const downLoadUrl = URL.createObjectURL(blob);
if (fileName) {
// 下载成功
callbackFunc({ res: true, msg: "下载成功" });
const a = document.createElement("a");
if (!typeof a.download) {
window.loaction = downLoadUrl;
} else {
a.href = downLoadUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
} else {
// 没文件名也算下载失败
callbackFunc({ res: false, msg: "没文件名,下载失败" });
}
setTimeout(() => {
URL.revokeObjectURL(downLoadUrl); // cleanUp
}, 100);
}
} else {
// 下载失败
callbackFunc({ res: false, msg: e });
}
};
xhr.onerror = function (e) {
onerror(e);
};
xhr.ontimeout = function (e) {
ontimeout(e);
};
xhr.send(formData);
}
使用封装的上传组件
js
// 响应式数据
const [uploadInfo] = useState({
showUploadList: {
showDownloadIcon: true,
showRemoveIcon: true,
},
uploadUrl: "xxx"
})
// 绑定上传组件的引用
const UploadRef = useRef();
// 点击确认,给后端传递需要保留的文件标识
const uploadAttachIdCallBack = (idList) => {
arrAttachIds = Array.from(new Set([...idList]));
}
// 完成之后,需要清除刚刚上传的文件集合,准备下一批/次的上传
// 清空文件集合
// 主要代码实现
arrAttachIds = (() => {
UploadRef.cleanAttachmentList();
return [];
})()
// 组件
<UploadCp
{...uploadInfo}
uploadAttachIdHandle={(list) => uploadAttachIdCallBack(list)}
wrappedComponentRef={ref => UploadRef = ref}
/>
结果与反思
本次组件封装满足了业务需求,但是还存在一些不足
- 下载的时候不会显示进度条(懒)
- 更新上传列表的文件标识集合的时候,触发过于频繁
对于上传的文件类型,到底是选用formData的格式还是arrayBuffer的形式,还是不太能理解,需要加强学习。
如果使用fetch进行上传的话,fetch是不提供aip检测上传进度的。