You've already forked DataMate
- 在 XMLHttpRequest 中添加 signal.aborted 检查 - 修复 useSliceUpload 中的 cancelFn 闭包问题 - 确保流式上传和分片上传都能正确取消
545 lines
14 KiB
TypeScript
545 lines
14 KiB
TypeScript
import { message } from "antd";
|
|
import Loading from "./loading";
|
|
|
|
/**
|
|
* 通用请求工具类
|
|
*/
|
|
class Request {
|
|
constructor(baseURL = "") {
|
|
this.baseURL = baseURL;
|
|
this.defaultHeaders = {
|
|
"Content-Type": "application/json",
|
|
Accept: "*/*",
|
|
};
|
|
// 请求拦截器列表
|
|
this.requestInterceptors = [];
|
|
// 响应拦截器列表
|
|
this.responseInterceptors = [];
|
|
}
|
|
|
|
_count = 0;
|
|
$interval;
|
|
|
|
get count() {
|
|
return this._count;
|
|
}
|
|
|
|
set count(value) {
|
|
clearTimeout(this.$interval);
|
|
if (value > 0) {
|
|
Loading.show();
|
|
}
|
|
if (value <= 0) {
|
|
this.$interval = setTimeout(() => {
|
|
Loading.hide();
|
|
}, 300);
|
|
}
|
|
this._count = value >= 0 ? value : 0;
|
|
}
|
|
|
|
/**
|
|
* 添加请求拦截器
|
|
*/
|
|
addRequestInterceptor(interceptor) {
|
|
this.requestInterceptors.push(interceptor);
|
|
}
|
|
|
|
/**
|
|
* 添加响应拦截器
|
|
*/
|
|
addResponseInterceptor(interceptor) {
|
|
this.responseInterceptors.push(interceptor);
|
|
}
|
|
|
|
/**
|
|
* 执行请求拦截器
|
|
*/
|
|
async executeRequestInterceptors(config) {
|
|
let processedConfig = { ...config };
|
|
for (const interceptor of this.requestInterceptors) {
|
|
processedConfig = (await interceptor(processedConfig)) || processedConfig;
|
|
}
|
|
|
|
return processedConfig;
|
|
}
|
|
|
|
/**
|
|
* 执行响应拦截器
|
|
*/
|
|
async executeResponseInterceptors(response, config) {
|
|
let processedResponse = response;
|
|
|
|
for (const interceptor of this.responseInterceptors) {
|
|
processedResponse =
|
|
(await interceptor(processedResponse, config)) || processedResponse;
|
|
}
|
|
|
|
return processedResponse;
|
|
}
|
|
|
|
/**
|
|
* 创建支持进度监听的XMLHttpRequest
|
|
*/
|
|
createXHRWithProgress(url, config, onProgress, onDownloadProgress) {
|
|
return new Promise((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open(config.method || "POST", url);
|
|
|
|
// 设置请求头
|
|
if (config.headers) {
|
|
Object.keys(config.headers).forEach((key) => {
|
|
xhr.setRequestHeader(key, config.headers[key]);
|
|
});
|
|
}
|
|
|
|
// 监听 AbortSignal 来中止请求
|
|
if (config.signal) {
|
|
config.signal.addEventListener("abort", () => {
|
|
xhr.abort();
|
|
reject(new Error("上传已取消"));
|
|
});
|
|
}
|
|
|
|
// 监听上传进度
|
|
xhr.upload.addEventListener("progress", function (event) {
|
|
if (event.lengthComputable) {
|
|
if (onProgress) {
|
|
onProgress(event);
|
|
}
|
|
if (onDownloadProgress) {
|
|
onDownloadProgress(event);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 请求完成处理
|
|
xhr.addEventListener("load", () => {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
let response;
|
|
try {
|
|
// 尝试解析JSON
|
|
const contentType = xhr.getResponseHeader("content-type");
|
|
if (contentType && contentType.includes("application/json")) {
|
|
response = JSON.parse(xhr.responseText);
|
|
} else {
|
|
response = xhr.responseText;
|
|
}
|
|
} catch {
|
|
response = xhr.responseText;
|
|
}
|
|
|
|
resolve({
|
|
data: response,
|
|
status: xhr.status,
|
|
statusText: xhr.statusText,
|
|
headers: xhr.getAllResponseHeaders(),
|
|
xhr: xhr,
|
|
});
|
|
} else {
|
|
reject(new Error(`HTTP error! status: ${xhr.status}`));
|
|
}
|
|
});
|
|
|
|
// 请求错误
|
|
xhr.addEventListener("error", function () {
|
|
console.error("网络错误");
|
|
reject(new Error("网络错误"));
|
|
});
|
|
|
|
// 请求中止
|
|
xhr.addEventListener("abort", function () {
|
|
console.log("上传已取消");
|
|
reject(new Error("上传已取消"));
|
|
});
|
|
|
|
xhr.send(config.body);
|
|
|
|
return xhr; // 返回 xhr 对象以便后续控制
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 构建完整URL
|
|
*/
|
|
buildURL(url, params) {
|
|
const fullURL = this.baseURL + url;
|
|
if (!params) return fullURL;
|
|
|
|
const searchParams = new URLSearchParams();
|
|
Object.keys(params).forEach((key) => {
|
|
if (params[key] !== undefined && params[key] !== null) {
|
|
searchParams.append(key, params[key]);
|
|
}
|
|
});
|
|
|
|
const queryString = searchParams.toString();
|
|
return queryString ? `${fullURL}?${queryString}` : fullURL;
|
|
}
|
|
|
|
/**
|
|
* 处理响应
|
|
*/
|
|
async handleResponse(response, config) {
|
|
// 如果显示了loading,需要隐藏
|
|
if (config.showLoading) {
|
|
this.count--;
|
|
}
|
|
|
|
// 执行响应拦截器
|
|
const processedResponse = await this.executeResponseInterceptors(
|
|
response,
|
|
config
|
|
);
|
|
|
|
if (!processedResponse.ok) {
|
|
const error = new Error(
|
|
`HTTP error! status: ${processedResponse.status}`
|
|
);
|
|
error.status = processedResponse.status;
|
|
error.statusText = processedResponse.statusText;
|
|
|
|
try {
|
|
const errorData = await processedResponse.json();
|
|
error.data = errorData;
|
|
// message.error(`请求失败,错误信息: ${errorData.message}`);
|
|
} catch {
|
|
// 忽略JSON解析错误
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
|
|
// 检查响应是否为空
|
|
const contentType = processedResponse.headers.get("content-type");
|
|
if (contentType && contentType.includes("application/json")) {
|
|
return await processedResponse.json();
|
|
}
|
|
|
|
return await processedResponse.text();
|
|
}
|
|
|
|
/**
|
|
* 处理XHR响应
|
|
*/
|
|
async handleXHRResponse(xhrResponse, config) {
|
|
// 模拟fetch响应格式用于拦截器
|
|
const mockResponse = {
|
|
ok: xhrResponse.status >= 200 && xhrResponse.status < 300,
|
|
status: xhrResponse.status,
|
|
statusText: xhrResponse.statusText,
|
|
headers: {
|
|
get: (key) => xhrResponse.xhr.getResponseHeader(key),
|
|
},
|
|
};
|
|
|
|
// 执行响应拦截器
|
|
await this.executeResponseInterceptors(mockResponse, config);
|
|
|
|
if (!mockResponse.ok) {
|
|
const error = new Error(`HTTP error! status: ${xhrResponse.status}`);
|
|
error.status = xhrResponse.status;
|
|
error.statusText = xhrResponse.statusText;
|
|
error.data = xhrResponse.data;
|
|
message.error(`请求失败,错误信息: ${xhrResponse.statusText}`);
|
|
throw error;
|
|
}
|
|
|
|
return xhrResponse.data;
|
|
}
|
|
|
|
/**
|
|
* 通用请求方法
|
|
*/
|
|
async request(url, config) {
|
|
// 处理showLoading参数
|
|
if (config.showLoading) {
|
|
this.count++;
|
|
}
|
|
|
|
// 执行请求拦截器
|
|
const processedConfig = await this.executeRequestInterceptors(config);
|
|
|
|
// 如果需要进度监听,使用XMLHttpRequest
|
|
if (config.onUploadProgress || config.onDownloadProgress) {
|
|
const xhrResponse = await this.createXHRWithProgress(
|
|
url,
|
|
processedConfig,
|
|
config.onUploadProgress,
|
|
config.onDownloadProgress
|
|
);
|
|
return await this.handleXHRResponse(xhrResponse, processedConfig);
|
|
}
|
|
// 否则使用fetch
|
|
const response = await fetch(url, processedConfig);
|
|
return await this.handleResponse(response, processedConfig);
|
|
}
|
|
|
|
/**
|
|
* GET请求
|
|
* @param {string} url - 请求URL
|
|
* @param {object} params - 查询参数
|
|
* @param {object} options - 额外的fetch选项,包括showLoading, onDownloadProgress
|
|
*/
|
|
async get(url, params = null, options = {}) {
|
|
const fullURL = this.buildURL(url, params);
|
|
|
|
const config = {
|
|
method: "GET",
|
|
headers: {
|
|
...this.defaultHeaders,
|
|
...options.headers,
|
|
},
|
|
...options,
|
|
};
|
|
|
|
return this.request(fullURL, config);
|
|
}
|
|
|
|
/**
|
|
* POST请求
|
|
* @param {string} url - 请求URL
|
|
* @param {object} data - 请求体数据
|
|
* @param {object} options - 额外的fetch选项,包括showLoading, onUploadProgress, onDownloadProgress
|
|
*/
|
|
async post(url, data = {}, options = {}) {
|
|
let config = {
|
|
method: "POST",
|
|
headers: {
|
|
...this.defaultHeaders,
|
|
...options.headers,
|
|
},
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
...options,
|
|
};
|
|
|
|
const isFormData = data instanceof FormData;
|
|
if (isFormData) {
|
|
config = {
|
|
method: "POST",
|
|
headers: {
|
|
...options.headers, // FormData不需要Content-Type
|
|
},
|
|
body: data,
|
|
...options,
|
|
};
|
|
}
|
|
return this.request(this.baseURL + url, config);
|
|
}
|
|
|
|
/**
|
|
* PUT请求
|
|
* @param {string} url - 请求URL
|
|
* @param {object} data - 请求体数据
|
|
* @param {object} options - 额外的fetch选项,包括showLoading, onUploadProgress, onDownloadProgress
|
|
*/
|
|
async put(url, data = null, options = {}) {
|
|
let config = {
|
|
method: "PUT",
|
|
headers: {
|
|
...this.defaultHeaders,
|
|
...options.headers,
|
|
},
|
|
body: data ? JSON.stringify(data) : undefined,
|
|
...options,
|
|
};
|
|
|
|
const isFormData = data instanceof FormData;
|
|
if (isFormData) {
|
|
config = {
|
|
method: "PUT",
|
|
headers: {
|
|
...options.headers,
|
|
},
|
|
body: data,
|
|
...options,
|
|
};
|
|
}
|
|
|
|
return this.request(this.baseURL + url, config);
|
|
}
|
|
|
|
/**
|
|
* DELETE请求
|
|
* @param {string} url - 请求URL
|
|
* @param {object} params - 查询参数或请求体数据
|
|
* @param {object} options - 额外的fetch选项,包括showLoading
|
|
*/
|
|
async delete(url, params = null, options = {}) {
|
|
let fullURL = this.baseURL + url;
|
|
const config = {
|
|
method: "DELETE",
|
|
redirect: "follow",
|
|
headers: {
|
|
...this.defaultHeaders,
|
|
...options.headers,
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
},
|
|
credentials: "include",
|
|
mode: "cors",
|
|
body: params ? JSON.stringify(params) : undefined,
|
|
...options,
|
|
};
|
|
|
|
// 判断params是否应该作为查询参数或请求体
|
|
if (params && typeof params === "object" && !Array.isArray(params)) {
|
|
// 如果params是普通对象,检查是否应该作为查询参数
|
|
const isQueryParams =
|
|
Object.keys(params).length === 1 &&
|
|
(Object.prototype.hasOwnProperty.call(params, "id") ||
|
|
Object.prototype.hasOwnProperty.call(params, "ids"));
|
|
|
|
if (isQueryParams) {
|
|
fullURL = this.buildURL(url, params);
|
|
} else {
|
|
// 作为请求体发送
|
|
config.body = JSON.stringify(params);
|
|
}
|
|
} else if (Array.isArray(params)) {
|
|
// 数组形式的数据作为请求体发送
|
|
config.body = JSON.stringify(params);
|
|
} else if (params) {
|
|
// 其他情况作为查询参数
|
|
fullURL = this.buildURL(url, { id: params });
|
|
}
|
|
|
|
return this.request(fullURL, config);
|
|
}
|
|
|
|
/**
|
|
* 下载文件
|
|
* @param {string} url - 请求URL
|
|
* @param {object} params - 查询参数
|
|
* @param {string} filename - 下载文件名
|
|
* @param {object} options - 额外的fetch选项,包括showLoading, onDownloadProgress
|
|
*/
|
|
async download(url, params = null, filename = "", options = {}) {
|
|
const fullURL = this.buildURL(url, params);
|
|
|
|
const config = {
|
|
method: "GET",
|
|
responseType: "blob",
|
|
...options,
|
|
};
|
|
|
|
// 执行请求拦截器
|
|
const processedConfig = await this.executeRequestInterceptors(config);
|
|
|
|
let blob;
|
|
let name = filename;
|
|
|
|
// 如果需要下载进度监听,使用XMLHttpRequest
|
|
if (config.onDownloadProgress) {
|
|
const xhrResponse = await this.createXHRWithProgress(
|
|
fullURL,
|
|
{ ...processedConfig, responseType: "blob" },
|
|
null,
|
|
config.onDownloadProgress
|
|
);
|
|
|
|
if (xhrResponse.status < 200 || xhrResponse.status >= 300) {
|
|
throw new Error(`HTTP error! status: ${xhrResponse.status}`);
|
|
}
|
|
|
|
blob = xhrResponse.xhr.response;
|
|
name =
|
|
name ||
|
|
xhrResponse.headers.get("Content-Disposition")?.split("filename=")[1] ||
|
|
"download";
|
|
} else {
|
|
// 使用fetch
|
|
const response = await fetch(fullURL, processedConfig);
|
|
|
|
// 执行响应拦截器
|
|
const processedResponse = await this.executeResponseInterceptors(
|
|
response,
|
|
processedConfig
|
|
);
|
|
|
|
if (!processedResponse.ok) {
|
|
throw new Error(`HTTP error! status: ${processedResponse.status}`);
|
|
}
|
|
|
|
blob = await processedResponse.blob();
|
|
name =
|
|
name ||
|
|
response.headers.get("Content-Disposition")?.split("filename=")[1] ||
|
|
`download_${Date.now()}`;
|
|
}
|
|
|
|
// 创建下载链接
|
|
const downloadUrl = window.URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = downloadUrl;
|
|
link.download = filename ?? name;
|
|
|
|
// 添加到DOM并触发下载
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
// 清理URL对象
|
|
window.URL.revokeObjectURL(downloadUrl);
|
|
|
|
return blob;
|
|
}
|
|
|
|
/**
|
|
* 上传文件(专门的上传方法)
|
|
* @param {string} url - 上传URL
|
|
* @param {FormData|File} data - 文件数据
|
|
* @param {object} options - 选项,包括onUploadProgress回调
|
|
*/
|
|
async upload(url, data, options = {}) {
|
|
let formData = data;
|
|
|
|
// 如果传入的是File对象,包装成FormData
|
|
if (data instanceof File) {
|
|
formData = new FormData();
|
|
formData.append("file", data);
|
|
}
|
|
|
|
return this.post(url, formData, {
|
|
...options,
|
|
showLoading: options.showLoading !== false, // 上传默认显示loading
|
|
onUploadProgress: options.onUploadProgress, // 上传进度回调
|
|
});
|
|
}
|
|
}
|
|
|
|
// 创建默认实例
|
|
const request = new Request();
|
|
|
|
// 添加默认请求拦截器 - Token处理
|
|
request.addRequestInterceptor((config) => {
|
|
const token =
|
|
localStorage.getItem("token") || sessionStorage.getItem("token");
|
|
if (token) {
|
|
config.headers = {
|
|
...config.headers,
|
|
Authorization: `Bearer ${token}`,
|
|
};
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// 添加默认响应拦截器 - 错误处理
|
|
request.addResponseInterceptor((response) => {
|
|
// 可以在这里添加全局错误处理逻辑
|
|
// 比如token过期自动跳转登录页等
|
|
return response;
|
|
});
|
|
|
|
// 导出方法
|
|
export const get = request.get.bind(request);
|
|
export const post = request.post.bind(request);
|
|
export const put = request.put.bind(request);
|
|
export const del = request.delete.bind(request);
|
|
export const download = request.download.bind(request);
|
|
export const upload = request.upload.bind(request);
|
|
|
|
// 导出类,允许创建自定义实例
|
|
export { Request };
|
|
|
|
// 默认导出
|
|
export default request;
|