浅谈 customRequest with umi-request 监听上传进度
由于 umi-request 是基于 fetch 实现,但fetch 并不像 axios或(xhr自带的upload.onProgress
)一样具备现成的 onUploadProgress
API监听文件上传处理进度的事件 progressEvent,但我们可以利用 umi-request-progress中间件 一样可以处理进度监听。
简单实现
upload.ts 定义统一上传请求方法
import umiRequest from 'umi-request';
import progressMiddleware from 'umi-request-progress';
// 注册进度监听内核中间件
umiRequest.use(progressMiddleware, { core: true });
/**
* 上传文件
* @param data FormData
* @param onProgress 上传进度回调
* @param path 上传路径
*/
export function uploadService(
data: FormData,
{
onProgress,
path,
method = 'post',
}: { onProgress: ({ percent }: { percent: number }) => void; path: string; method?: string },
) {
return request<{
data: any;
}>(path, {
method,
data,
onReqProgress: function ({ loaded, total }: ProgressEvent) {
onProgress({ percent: parseInt(Math.round((loaded / total) * 100).toFixed(2)) });
},
requestType: 'form',
});
}
uploader.tsx customRequest自定义上传逻辑处理
import { ContainerOutlined, DeleteOutlined, PaperClipOutlined } from '@ant-design/icons';
import { useRequest, useUpdateEffect } from 'ahooks';
import { Button, message, Modal, Progress, Spin, Tooltip, Upload } from 'antd';
import React, { useCallback, useState } from 'react';
import styles from './index.module.less';
import { uploadService } from '@/services/upload';
import type { RcFile } from 'antd/lib/upload';
const fileTypeList = [
'application/vnd.ms-excel',
...
];
const Uploader: React.FC<{ empty?: boolean; onChange?: (arg0?: string) => void }> = (
props,
) => {
/** 当前上传的文件 */
const [importFile, setImportFile] = useState<{
status?: 'uploading' | 'done' | 'error';
percent?: number;
name?: string;
}>({});
const handleBeforeUpload = useCallback((file: RcFile): boolean | Upload.LIST_IGNORE => {
const fileSize = 10;
const isLtFileSize = file.size / 1024 / 1024 < fileSize;
/** 文件类型 */
if (fileTypeList.indexOf(file.type) === -1) {
message.error(`文件类型不符合`);
return Upload.LIST_IGNORE;
}
/** 文件大小 */
if (!isLtFileSize) {
message.error(`单个文件不超过${fileSize}MB`);
return Upload.LIST_IGNORE;
}
return true;
}, []);
/**
* uploading: 是否上传中
* uploadRes: 上传结果
*/
const {
loading: uploading,
data: uploadRes,
run: triggerUpload,
} = useRequest(
(formData, { onProgress }) =>
//path根据实际地址填写
uploadService(formData, { onProgress, path: '/api/xxxxxxxxx.......'}),
{
manual: true,
onSuccess: (res) => {
setImportFile({ ...importFile, percent: 100, status: 'done' });
},
onError: (e) => {
setImportFile({ ...importFile, status: 'error' });
},
},
);
const uploadConfig = {
multiple: false,
// data: 自定义请求参数 根据实际业务替换
data: { id:'1234' },
onStart(file: File) {
setImportFile({ ...file, ...importFile, name: file?.name, percent: 0, status: 'uploading' });
},
onProgress({ percent }: { percent: number }) {
setImportFile({ ...importFile, percent });
},
// 自定义上传请求
customRequest({
data,
file,
filename,
onProgress,
}: {
onProgress: ({ percent }: { percent: number }, file: File) => void;
data: { id: string };
filename: string;
file: File;
}) {
const formData = new FormData();
if (data) {
/** 添加额外参数 */
Object.keys(data).forEach((key) => {
formData.append(key, data[key]);
});
}
/** 添加文件参数 */
formData.append(filename, file);
triggerUpload(formData, { onProgress });
},
};
return (
<div className={styles.uploader}>
<Upload.Dragger
accept={fileTypeList.join(',')}
beforeUpload={handleBeforeUpload}
disabled={uploading || !!importFile?.status}
name="data"
className={styles.dragger}
showUploadList={false}
{...uploadConfig}
>
<>
<p className="ant-upload-drag-icon">
<ContainerOutlined />
</p>
<p className="ant-upload-text">点击或将文件拖拽到这里上传</p>
</>
</Upload.Dragger>
{importFile?.status && (
<>
<div>
<div className={styles.nameItem}>
<span>{importFile?.name}</span>
<DeleteOutlined
style={{ cursor: 'pointer' }}
onClick={() => {
setImportFile({});
}}
/>
</div>
</div>
<Progress
showInfo={false}
style={{ paddingLeft: 15 }}
percent={importFile?.percent}
strokeWidth={2}
status={importFile?.status === 'uploading' ? 'active' : 'exception'}
/>
</>
)}
</div>
);
};
export default Uploader;
踩坑记录
1.响应拦截不执行
上述代码,可完成正常上传业务需求,但如若基于umi-request 注册了request.interceptors.response
,此时并不会执行,因为 umi-request-progress 修改了umiRequestCoreType
为 ajaxRequest
,而 umi-request 对于 options.umiRequestCoreType
为非normal
类型时,会跳过interceptor
,源码如下:
2.无法捕获401/404等ResponseError
原因:umi-request-progress resolve
出来的是普通的xhr.response,
并不是 Response实例,不具备 clone
等方法,但 umi-request 是基于res是否具有 clone
方法的判断得到 copy
,方可根据http status,throw 对应的ResponseError,源码如下:
解法
- 创建自定义中间件,结合 umi-request-progress代码,稍作调整
function ajaxRequest(url, options) {
return new Promise(function (resolve, reject) {
var _a, _b, _c;
const method = (_a = options.method) != null ? _a : 'get';
const headers = (_b = options.headers) != null ? _b : {};
const {
onReqProgress,
onResProgress,
credentials,
responseType,
body,
parseResponse,
timeout,
cancelToken,
signal,
} = options;
const xhr = new XMLHttpRequest();
if (timeout != null) {
xhr.timeout = timeout;
}
switch (credentials) {
case 'include': {
xhr.withCredentials = true;
break;
}
case 'omit': {
xhr.withCredentials = false;
break;
}
}
if (responseType !== 'formData') {
xhr.responseType =
(_c = responseType == null ? void 0 : responseType.toLowerCase()) != null ? _c : 'json';
}
xhr.open(method, url);
let headersEntries = [];
if (Array.isArray(headers)) {
headersEntries = headers;
} else if (headers instanceof Headers) {
headers.forEach((value, key) => {
headersEntries.push([key, value]);
});
} else {
headersEntries = Object.entries(headers);
}
for (const [key, value] of headersEntries) {
xhr.setRequestHeader(key, value);
}
if (cancelToken) {
cancelToken.promise.then(function () {
xhr.abort();
});
}
if (signal) {
signal.addEventListener(
'abort',
function () {
xhr.abort();
},
{
once: true,
},
);
}
xhr.onload = () => {
// resolve(parseResponse === false ? xhr.responseText : xhr.response);
if (xhr.status >= 200 && xhr.status < 300) {
resolve(parseResponse === false ? xhr.responseText : xhr.response);
} else {
const myBlob = new Blob();
const myOptions = {
status: xhr.status,
statusText: 'SuperSmashingGreat!',
};
//创建 Response实例
const myResponse = new Response(myBlob, myOptions);
resolve(myResponse);
}
};
xhr.onerror = reject;
if (xhr.upload && onReqProgress) {
xhr.upload.onprogress = onReqProgress;
}
if (onResProgress) {
xhr.onprogress = onResProgress;
}
xhr.send(body);
});
}
const progressMiddleware = async function (ctx, next) {
const { url, options } = ctx.req;
if (!(options.onReqProgress || options.onResProgress)) {
await next();
return;
}
//如需执行响应拦截 不可修改 __umiRequestCoreType__
// options.__umiRequestCoreType__ = "ajaxRequest";
const response = await ajaxRequest(url, options);
ctx.res = response;
await next();
};
export { progressMiddleware as default };
2.可提issue至 umi-request-progress 仓库