基于antd二次封装上传组件

基于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>    
        )
    }
}
细节
  1. 如果"上传失败" ,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}
	/>

结果与反思

本次组件封装满足了业务需求,但是还存在一些不足

  1. 下载的时候不会显示进度条(懒)
  2. 更新上传列表的文件标识集合的时候,触发过于频繁

对于上传的文件类型,到底是选用formData的格式还是arrayBuffer的形式,还是不太能理解,需要加强学习。

如果使用fetch进行上传的话,fetch是不提供aip检测上传进度的。

相关推荐
光影少年1 小时前
usemeno和usecallback区别及使用场景
react.js
吕彬-前端6 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白6 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧6 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
程序员小杨v17 小时前
如何使用 React Compiler – 完整指南
前端·react.js
谢尔登8 小时前
Babel
前端·react.js·node.js
卸任9 小时前
使用高阶组件封装路由拦截逻辑
前端·react.js
清汤饺子11 小时前
实践指南之网页转PDF
前端·javascript·react.js
霸气小男11 小时前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
小白小白从不日白12 小时前
react 组件通讯
前端·react.js