UNPKG

create-vvt

Version:

一个基于 Vite + Vue3 + TypeScript/JavaScript 的项目模板脚手架

454 lines (410 loc) 13 kB
import axios, { type AxiosInstance, type AxiosRequestConfig, type CustomParamsSerializer } from 'axios'; import type { PureHttpError, RequestMethods, PureHttpResponse, PureHttpRequestConfig, IResponse } from './types'; import { stringify } from 'qs'; import NProgress from '../progress'; import { handleChangeRequestHeader, TIMEOUT_EXEMPT_URLS, DIRECT_RETURN_PATTERNS, handleResponseData, handleNetworkError } from './config'; import { downloadFile } from '@utils/tools'; // Default config const defaultConfig: AxiosRequestConfig = { timeout: 60000 * 3, headers: { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, paramsSerializer: { serialize: stringify as unknown as CustomParamsSerializer } }; // Custom error classes class TimeoutError extends Error { constructor(message: string) { super(message); this.name = 'TimeoutError'; } } // Main HTTP client class export class PureHttp { private static instance: AxiosInstance = axios.create(defaultConfig); private static initConfig: PureHttpRequestConfig = {}; constructor() { this.httpInterceptorsRequest(); this.httpInterceptorsResponse(); } private httpInterceptorsRequest(): void { PureHttp.instance.interceptors.request.use( async (config: PureHttpRequestConfig): Promise<any> => { NProgress.start(); // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调 if (typeof config.beforeRequestCallback === 'function') { config.beforeRequestCallback(config); return config; } if (PureHttp.initConfig.beforeRequestCallback) { PureHttp.initConfig.beforeRequestCallback(config); return config; } // 添加请求头 config = handleChangeRequestHeader(config); // 为豁免接口移除超时设置 if (TIMEOUT_EXEMPT_URLS.some((url) => config.url?.includes(url))) { config.timeout = 0; } return config; }, (error) => Promise.reject(error) ); } private httpInterceptorsResponse(): void { PureHttp.instance.interceptors.response.use( (response: PureHttpResponse) => { NProgress.done(); // 处理直接返回流 if ( DIRECT_RETURN_PATTERNS.some((pattern) => pattern.test(response.config.url || '') ) && response.headers['content-type'] === 'application/octet-stream' ) { return response; } // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调 if (typeof response.config.beforeResponseCallback === 'function') { response.config.beforeResponseCallback(response); return response.data; } if (PureHttp.initConfig.beforeResponseCallback) { PureHttp.initConfig.beforeResponseCallback(response); return response.data; } // 只处理 2xx http状态码 if (response.status >= 200 && response.status < 300) { try { return handleResponseData(response); } catch (error) { return error instanceof Error ? Promise.reject(error.message || '处理响应出现错误') : Promise.reject(error || '未知错误类型'); } } else { return Promise.reject('非2xx的状态码:' + response.status); } }, (error: PureHttpError) => { NProgress.done(); error.isCancelRequest = axios.isCancel(error); if ( error.code === 'ECONNABORTED' && error.message.includes('timeout') ) { return Promise.reject(new TimeoutError('请求超时,请稍后重试')); } else if (error.response) { // 服务器响应了请求但有错误状态码 handleNetworkError(error?.response?.status as number); return Promise.reject(error.response); } else if (error.request) { // 请求已发出但没有收到响应 return Promise.reject('无响应:' + error.message); } else { // 在设置请求时触发了错误 return Promise.reject('请求错误:' + error.message); } } ); } public cancelTokenSource() { return axios.CancelToken.source(); } public request<T>( method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: PureHttpRequestConfig ): Promise<IResponse<T>> { const config = { method, url, ...param, ...axiosConfig } as PureHttpRequestConfig; return new Promise((resolve, reject) => { PureHttp.instance .request(config) .then((response: any) => resolve(response)) .catch((error) => reject(error)); }); } public get<T, P = any>( url: string, params?: P, config?: PureHttpRequestConfig ): Promise<IResponse<T>> { return this.request<T>('get', url, { params }, config); } public post<T, P = any>( url: string, data?: P, config?: PureHttpRequestConfig ): Promise<IResponse<T>> { return this.request<T>('post', url, { data }, config); } public put<T, P = any>( url: string, data?: P, config?: PureHttpRequestConfig ): Promise<IResponse<T>> { return this.request<T>('put', url, { data }, config); } public delete<T>( url: string, config?: PureHttpRequestConfig ): Promise<IResponse<T>> { return this.request<T>('delete', url, {}, config); } /** * @description 处理文件流: 下载、预览时可用 * @param url 文件流地址 * @param method 请求方法 * @param config 请求配置 * @returns 文件名和文件blob对象 */ public async getFileStream( url: string, method: 'get' | 'post' = 'post', config?: Record<string, any> ): Promise<{ serverFilename: string; blob: Blob; }> { try { const response = await (method === 'post' ? PureHttp.instance.post(url, config, { responseType: 'blob', headers: { Accept: 'application/octet-stream' } }) : PureHttp.instance.get(url, { responseType: 'blob', headers: { Accept: 'application/octet-stream' } })); // 如果接口响应失败,返回的responseType是 json,这个在错误处理中统一处理了。 // 所以接口响应失败后将获取不到response.headers,这个方法会直接 catch const contentType = response.headers['content-type'] || 'application/octet-stream'; const contentDisposition = response.headers['content-disposition']; // 如果服务端设置了文件名,优先使用服务端的文件名,这里取文件名的方法需要根据服务端实际的格式进行调整 const serverFilename = contentDisposition ? contentDisposition.includes("filename*=utf-8''") ? decodeURIComponent(contentDisposition.split("filename*=utf-8''")[1]) : decodeURIComponent( contentDisposition.split('filename=')[1]?.replace(/["']/g, '') ) : ''; const blob = new Blob([response.data], { type: contentType }); return { serverFilename, blob }; } catch (error) { console.error('获取文件失败:', error); throw error; } } /** * @description 下载文件 * @param url 文件流地址 * @param method 请求方法 * @param filename 文件名 * @param config 请求配置 */ public async downloadFileByStream( url: string, method: 'get' | 'post' = 'post', filename?: string, config?: Record<string, any> ): Promise<void> { const { serverFilename, blob } = await this.getFileStream( url, method, config ); const downloadFilename = serverFilename.split('-')[1] || filename || 'download'; downloadFile(blob, downloadFilename); } /** * @description 单文件上传:一次请求只上传一个文件 * @param url 上传地址 * @param file 要上传的文件 * @param config 请求配置 * @param onProgress 上传进度回调 * @returns 上传结果 */ public async uploadFile<T = any>( url: string, file: File, config?: { fieldName?: string; additionalData?: Record<string, any>; headers?: Record<string, string>; }, onProgress?: (progress: number) => void ): Promise<IResponse<T>> { const formData = new FormData(); const fieldName = config?.fieldName || 'file'; // 添加文件 formData.append(fieldName, file); // 添加额外数据 if (config?.additionalData) { Object.keys(config.additionalData).forEach((key) => { formData.append(key, config.additionalData![key]); }); } // 构建请求配置 const requestConfig: PureHttpRequestConfig = { method: 'post', url, data: formData, headers: { 'Content-Type': 'multipart/form-data', ...config?.headers }, onUploadProgress: (progressEvent) => { if (progressEvent.total && onProgress) { const progress = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); onProgress(progress); } } }; return this.request<T>('post', url, { data: formData }, requestConfig); } /** * @description 并发批量上传(大)文件:某个文件上传失败不影响其他文件 * @param url 上传地址 * @param files 要上传的文件数组 * @param config 请求配置 * @param onProgress 上传进度回调 * @returns 上传结果数组 */ public async uploadMultipleFiles<T = any>( url: string, files: File[], config?: { fieldName?: string; additionalData?: Record<string, any>; headers?: Record<string, string>; concurrent?: number; // 并发上传数量,默认3个 }, onProgress?: ( progress: number, currentFile: File, currentIndex: number ) => void ): Promise<IResponse<T>[]> { const concurrent = config?.concurrent || 3; const results: IResponse<T>[] = []; // 分批处理文件 for (let i = 0; i < files.length; i += concurrent) { const batch = files.slice(i, i + concurrent); const batchPromises = batch.map(async (file, batchIndex) => { const fileIndex = i + batchIndex; try { const result = await this.uploadFile<T>( url, file, config, (progress) => { if (onProgress) { onProgress(progress, file, fileIndex); } } ); return result; } catch (error) { throw error; } }); const batchResults = await Promise.allSettled(batchPromises); batchResults.forEach((result, index) => { if (result.status === 'fulfilled') { results[i + index] = result.value; } else { results[i + index] = { code: -1, data: null as any, msg: `文件 ${files[i + index].name} 上传失败: ${result.reason}` } as IResponse<T>; } }); } return results; } /** * @description 批量上传文件到单个请求(推荐用于小文件):文件只能一起成功/失败 * @param url 上传地址 * @param files 要上传的文件数组 * @param config 请求配置 * @param onProgress 上传进度回调 * @returns 上传结果 */ public async uploadFilesAsBatch<T = any>( url: string, files: File[], config?: { fieldName?: string; additionalData?: Record<string, any>; headers?: Record<string, string>; }, onProgress?: (progress: number) => void ): Promise<IResponse<T>> { const formData = new FormData(); const fieldName = config?.fieldName || 'files'; // 添加所有文件 files.forEach((file, index) => { formData.append(`${fieldName}[${index}]`, file); }); // 添加额外数据 if (config?.additionalData) { Object.keys(config.additionalData).forEach((key) => { formData.append(key, config.additionalData![key]); }); } // 构建请求配置 const requestConfig: PureHttpRequestConfig = { method: 'post', url, data: formData, headers: { 'Content-Type': 'multipart/form-data', ...config?.headers }, onUploadProgress: (progressEvent) => { if (progressEvent.total && onProgress) { const progress = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); onProgress(progress); } } }; return this.request<T>('post', url, { data: formData }, requestConfig); } } export const http = new PureHttp();