前端必看之超優(yōu)雅的Axios封裝實踐
經(jīng)過一上午的反復打磨,終于寫出了這個讓我滿意的 HTTP 客戶端封裝。當我把代碼發(fā)給朋友時,他竟然挑出了一堆問題!還好我都一一解決了。
為什么要重新封裝 Axios?
我們都知道 Axios 是 JavaScript 世界里最受歡迎的 HTTP 客戶端,但在實際項目中,我們總是需要:
- ?? 統(tǒng)一的錯誤處理 - 不想在每個請求里寫重復的 try-catch
- ?? 請求取消機制 - 用戶快速切換頁面時取消無用請求
- ?? 自動重試功能 - 網(wǎng)絡(luò)不穩(wěn)定時自動重試
- ?? TypeScript 完美支持 - 類型安全,IDE 智能提示
- ??? 多實例管理 - 不同 API 服務(wù)需要不同配置
市面上的封裝要么太簡單,要么太復雜。所以我決定寫一個既優(yōu)雅又實用的版本。
設(shè)計理念
這個封裝的設(shè)計遵循幾個核心原則:
- ?? 漸進式增強 - 可以像原生 Axios 一樣簡單使用,也可以啟用高級功能
- ?? 類型安全 - 完整的 TypeScript 支持,編譯時發(fā)現(xiàn)問題
- ?? 靈活擴展 - 支持多實例、自定義攔截器、業(yè)務(wù)定制
- ? 性能優(yōu)先 - 自動去重、智能重試、內(nèi)存管理
- ?? 文檔友好 - 豐富的示例和注釋,上手即用
完整源碼
import Axios, { type AxiosInstance, type AxiosRequestConfig, type CustomParamsSerializer, type AxiosResponse, type InternalAxiosRequestConfig, type Method, type AxiosError } from "axios";
import { stringify } from "qs";
// 基礎(chǔ)配置
const defaultConfig: AxiosRequestConfig = {
timeout: 6000,
headers: {
"Content-Type": "application/json;charset=utf-8"
},
paramsSerializer: {
serialize: stringify as unknown as CustomParamsSerializer
}
};
// 響應數(shù)據(jù)基礎(chǔ)結(jié)構(gòu)
export interface BaseResponse {
code: number;
message?: string;
}
// 去除與BaseResponse沖突的字段
type OmitBaseResponse<T> = Omit<T, keyof BaseResponse>;
// 響應數(shù)據(jù)類型定義 - 避免屬性沖突,確保BaseResponse優(yōu)先級
export type ResponseData<T = any> = BaseResponse & OmitBaseResponse<T>;
// 響應數(shù)據(jù)驗證函數(shù)類型
export type ResponseValidator<T = any> = (data: ResponseData<T>) => boolean;
// 重試配置
export interface RetryConfig {
retries?: number; // 重試次數(shù)
retryDelay?: number; // 重試延遲(毫秒)
retryCondition?: (error: AxiosError) => boolean; // 重試條件
}
// 攔截器配置類型
interface InterceptorsConfig {
requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
requestErrorInterceptor?: (error: AxiosError) => Promise<any>;
responseInterceptor?: (response: AxiosResponse<ResponseData<any>>) => any;
responseErrorInterceptor?: (error: AxiosError) => Promise<any>;
}
// 請求唯一鍵
type RequestKey = string | symbol;
/**
* 增強型 HTTP 客戶端,基于 Axios 封裝
* 支持攔截器配置、請求取消、多實例管理等功能
*/
class HttpClient {
private instance: AxiosInstance;
private requestInterceptorId?: number;
private responseInterceptorId?: number;
private abortControllers: Map<RequestKey, AbortController> = new Map();
/**
* 創(chuàng)建 HTTP 客戶端實例
* @param customConfig 自定義 Axios 配置
* @param interceptors 自定義攔截器配置
*/
constructor(customConfig?: AxiosRequestConfig, interceptors?: InterceptorsConfig) {
this.instance = Axios.create({ ...defaultConfig, ...customConfig });
this.initInterceptors(interceptors);
}
/** 初始化攔截器 */
private initInterceptors(interceptors?: InterceptorsConfig): void {
this.initRequestInterceptor(interceptors?.requestInterceptor, interceptors?.requestErrorInterceptor);
this.initResponseInterceptor(interceptors?.responseInterceptor, interceptors?.responseErrorInterceptor);
}
/** 初始化請求攔截器 */
private initRequestInterceptor(customInterceptor?: InterceptorsConfig["requestInterceptor"], customErrorInterceptor?: InterceptorsConfig["requestErrorInterceptor"]): void {
// 默認請求攔截器
const defaultInterceptor = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
/* 在這里寫請求攔截器的默認業(yè)務(wù)邏輯 */
// 示例: 添加token
// const token = localStorage.getItem('token');
// if (token) {
// config.headers.Authorization = `Bearer ${token}`;
// }
// 示例: 添加時間戳防止緩存
// if (config.method?.toUpperCase() === 'GET') {
// config.params = { ...config.params, _t: Date.now() };
// }
return config;
};
// 默認請求錯誤攔截器
const defaultErrorInterceptor = (error: AxiosError): Promise<any> => {
/* 在這里寫請求錯誤攔截器的默認業(yè)務(wù)邏輯 */
// 示例: 處理請求前的錯誤
// console.error('請求配置錯誤:', error);
return Promise.reject(error);
};
// 優(yōu)先使用自定義攔截器,否則使用默認攔截器
this.requestInterceptorId = this.instance.interceptors.request.use(customInterceptor || defaultInterceptor, customErrorInterceptor || defaultErrorInterceptor);
}
/** 初始化響應攔截器 */
private initResponseInterceptor(customInterceptor?: InterceptorsConfig["responseInterceptor"], customErrorInterceptor?: InterceptorsConfig["responseErrorInterceptor"]): void {
// 默認響應攔截器
const defaultInterceptor = (response: AxiosResponse<ResponseData<any>>) => {
const requestKey = this.getRequestKey(response.config);
if (requestKey) this.abortControllers.delete(requestKey);
/* 在這里寫響應攔截器的默認業(yè)務(wù)邏輯 */
// 示例: 處理不同的響應碼
// const { code, message } = response.data;
// switch(code) {
// case 200:
// return response.data;
// case 401:
// // 未授權(quán)處理
// break;
// case 403:
// // 權(quán)限不足處理
// break;
// default:
// // 其他錯誤處理
// console.error('請求錯誤:', message);
// }
return response.data;
};
// 默認響應錯誤攔截器
const defaultErrorInterceptor = (error: AxiosError): Promise<any> => {
if (error.config) {
const requestKey = this.getRequestKey(error.config);
if (requestKey) this.abortControllers.delete(requestKey);
}
// 處理請求被取消的情況
if (Axios.isCancel(error)) {
console.warn("請求已被取消:", error.message);
return Promise.reject(new Error("請求已被取消"));
}
// 網(wǎng)絡(luò)錯誤處理
if (!(error as AxiosError).response) {
if ((error as any).code === "ECONNABORTED" || (error as AxiosError).message?.includes("timeout")) {
return Promise.reject(new Error("請求超時,請稍后重試"));
}
return Promise.reject(new Error("網(wǎng)絡(luò)錯誤,請檢查網(wǎng)絡(luò)連接"));
}
// HTTP狀態(tài)碼錯誤處理
const status = (error as AxiosError).response?.status;
const commonErrors: Record<number, string> = {
400: "請求參數(shù)錯誤",
401: "未授權(quán),請重新登錄",
403: "權(quán)限不足",
404: "請求的資源不存在",
408: "請求超時",
500: "服務(wù)器內(nèi)部錯誤",
502: "網(wǎng)關(guān)錯誤",
503: "服務(wù)暫不可用",
504: "網(wǎng)關(guān)超時"
};
const message = commonErrors[status] || `請求失?。顟B(tài)碼:${status})`;
return Promise.reject(new Error(message));
};
// 優(yōu)先使用自定義攔截器,否則使用默認攔截器
this.responseInterceptorId = this.instance.interceptors.response.use(customInterceptor || defaultInterceptor, customErrorInterceptor || defaultErrorInterceptor);
}
/** 生成請求唯一標識 */
private getRequestKey(config: AxiosRequestConfig): RequestKey | undefined {
if (!config.url) return undefined;
return `${config.method?.toUpperCase()}-${config.url}`;
}
/** 設(shè)置取消控制器 - 用于取消重復請求或主動取消請求 */
private setupCancelController(config: AxiosRequestConfig, requestKey?: RequestKey): AxiosRequestConfig {
const key = requestKey || this.getRequestKey(config);
if (!key) return config;
// 如果已有相同key的請求,先取消它
this.cancelRequest(key);
const controller = new AbortController();
this.abortControllers.set(key, controller);
return {
...config,
signal: controller.signal
};
}
/** 移除請求攔截器 */
public removeRequestInterceptor(): void {
if (this.requestInterceptorId !== undefined) {
this.instance.interceptors.request.eject(this.requestInterceptorId);
this.requestInterceptorId = undefined; // 重置ID,避免重復移除
}
}
/** 移除響應攔截器 */
public removeResponseInterceptor(): void {
if (this.responseInterceptorId !== undefined) {
this.instance.interceptors.response.eject(this.responseInterceptorId);
this.responseInterceptorId = undefined; // 重置ID,避免重復移除
}
}
/** 動態(tài)設(shè)置請求攔截器 */
public setRequestInterceptor(customInterceptor?: InterceptorsConfig["requestInterceptor"], customErrorInterceptor?: InterceptorsConfig["requestErrorInterceptor"]): void {
this.removeRequestInterceptor();
this.initRequestInterceptor(customInterceptor, customErrorInterceptor);
}
/** 動態(tài)設(shè)置響應攔截器 */
public setResponseInterceptor(customInterceptor?: InterceptorsConfig["responseInterceptor"], customErrorInterceptor?: InterceptorsConfig["responseErrorInterceptor"]): void {
this.removeResponseInterceptor();
this.initResponseInterceptor(customInterceptor, customErrorInterceptor);
}
/** 獲取 Axios 實例 */
public getInstance(): AxiosInstance {
return this.instance;
}
/**
* 取消某個請求
* @param key 請求唯一標識
* @param message 取消原因
* @returns 是否成功取消
*/
public cancelRequest(key: RequestKey, message?: string): boolean {
const controller = this.abortControllers.get(key);
if (controller) {
controller.abort(message || `取消請求: ${String(key)}`);
this.abortControllers.delete(key);
return true;
}
return false;
}
/**
* 取消所有請求
* @param message 取消原因
*/
public cancelAllRequests(message?: string): void {
this.abortControllers.forEach((controller, key) => {
controller.abort(message || `取消所有請求: ${String(key)}`);
});
this.abortControllers.clear();
}
/**
* 判斷是否為取消錯誤
* @param error 錯誤對象
* @returns 是否為取消錯誤
*/
public static isCancel(error: unknown): boolean {
return Axios.isCancel(error);
}
/**
* 睡眠函數(shù)
* @param ms 毫秒數(shù)
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 通用請求方法
* @param method 請求方法
* @param url 請求地址
* @param config 請求配置
* @returns 響應數(shù)據(jù)
*/
public async request<T = any>(method: Method, url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
const { requestKey, retry, ...restConfig } = config || {};
// 設(shè)置合理的默認重試條件
const defaultRetryCondition = (error: AxiosError) => {
// 默認只重試網(wǎng)絡(luò)錯誤或5xx服務(wù)器錯誤
return !error.response || (error.response.status >= 500 && error.response.status < 600);
};
const retryConfig = {
retries: 0,
retryDelay: 1000,
retryCondition: defaultRetryCondition,
...retry
};
let lastError: any;
const key = requestKey || this.getRequestKey({ ...restConfig, method, url });
for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
try {
// 重試前清除舊的AbortController(避免重試請求被誤取消)
if (attempt > 0 && key) {
this.abortControllers.delete(key);
}
const requestConfig = this.setupCancelController({ ...restConfig, method, url }, requestKey);
/* 在這里寫通用請求前的業(yè)務(wù)邏輯 */
// 示例: 記錄請求日志
// console.log(`[${method.toUpperCase()}] ${url}:`, restConfig);
const response = await this.instance.request<ResponseData<T>>(requestConfig);
/* 在這里寫通用請求后的業(yè)務(wù)邏輯 */
// 示例: 記錄響應日志
// console.log(`[${method.toUpperCase()}] ${url} 響應:`, response.data);
return response.data;
} catch (error) {
lastError = error;
// 如果是最后一次嘗試或不滿足重試條件或請求被取消,直接拋出錯誤
if (attempt === retryConfig.retries || !retryConfig.retryCondition(error as AxiosError) || HttpClient.isCancel(error)) {
break;
}
// 延遲后重試
if (retryConfig.retryDelay > 0) {
await this.sleep(retryConfig.retryDelay);
}
}
}
/* 在這里寫請求異常的通用處理邏輯 */
// 示例: 統(tǒng)一錯誤提示
// if (lastError instanceof Error) {
// console.error('請求失敗:', lastError.message);
// }
return Promise.reject(lastError);
}
/**
* GET 請求
* @param url 請求地址
* @param config 請求配置
* @returns 響應數(shù)據(jù)
*/
public get<T = any>(url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
return this.request<T>("get", url, config);
}
/**
* POST 請求
* @param url 請求地址
* @param data 請求數(shù)據(jù)
* @param config 請求配置
* @returns 響應數(shù)據(jù)
*/
public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
return this.request<T>("post", url, { ...config, data });
}
/**
* PUT 請求
* @param url 請求地址
* @param data 請求數(shù)據(jù)
* @param config 請求配置
* @returns 響應數(shù)據(jù)
*/
public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
return this.request<T>("put", url, { ...config, data });
}
/**
* DELETE 請求
* @param url 請求地址
* @param config 請求配置
* @returns 響應數(shù)據(jù)
*/
public delete<T = any>(url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
return this.request<T>("delete", url, config);
}
/**
* PATCH 請求
* @param url 請求地址
* @param data 請求數(shù)據(jù)
* @param config 請求配置
* @returns 響應數(shù)據(jù)
*/
public patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
return this.request<T>("patch", url, { ...config, data });
}
}
// 默認導出實例 - 可直接使用
export const http = new HttpClient();
export default HttpClient;使用指南
1. 基礎(chǔ)請求操作:GET/POST 等常用方法
任何 HTTP 客戶端最核心的功能都是處理基礎(chǔ)請求,HttpClient 對常用的 HTTP 方法進行了友好封裝,同時提供了完整的 TypeScript 類型支持。
首先定義我們需要用到的數(shù)據(jù)類型:
// 用戶信息接口
interface User {
id: number;
name: string;
email: string;
}
// 分頁響應通用接口
interface PageResponse<T> {
list: T[]; // 數(shù)據(jù)列表
total: number; // 總條數(shù)
page: number; // 當前頁碼
size: number; // 每頁條數(shù)
}基礎(chǔ)請求操作示例:
async function basicRequests() {
try {
// 1. GET 請求(帶查詢參數(shù))
const userPage = await http.get<PageResponse<User>>('/api/users', {
params: { page: 1, size: 10 }
});
console.log(`第${userPage.page}頁,共${userPage.total}個用戶:`, userPage.list);
// 2. POST 請求(提交數(shù)據(jù))
const newUser = await http.post<{ id: number }>('/api/users', {
name: '張三',
email: 'zhangsan@example.com'
});
console.log('新增用戶ID:', newUser.id);
// 3. PUT 請求(更新數(shù)據(jù))
const updatedUser = await http.put<User>('/api/users/1', {
id: 1,
name: '張三三',
email: 'zhangsansan@example.com'
});
// 4. DELETE 請求
const deleteRes = await http.delete<{ success: boolean; message?: string }>('/api/users/1');
console.log('刪除結(jié)果:', deleteRes.success);
} catch (error) {
// 統(tǒng)一錯誤處理
console.error('請求失敗:', error instanceof Error ? error.message : error);
}
}關(guān)鍵特性:
- 泛型參數(shù)直接指定響應數(shù)據(jù)類型,獲得完整的類型提示
- 統(tǒng)一的錯誤處理機制,無需在每個請求中重復編寫 try-catch
- 自動處理請求參數(shù)序列化和響應數(shù)據(jù)解析
2. 多實例管理:為不同 API 服務(wù)創(chuàng)建專屬客戶端
在復雜項目中,我們常常需要與多個 API 服務(wù)交互,每個服務(wù)可能有不同的基礎(chǔ)路徑、超時設(shè)置或認證方式。HttpClient 支持創(chuàng)建多個獨立實例,完美解決這個問題。
// 定義商品數(shù)據(jù)接口
interface Product {
id: string;
title: string;
price: number;
stock: number;
}
// 1. 創(chuàng)建用戶服務(wù)專用實例
const userApi = new HttpClient({
baseURL: 'https://api.example.com/user', // 用戶服務(wù)基礎(chǔ)路徑
timeout: 10000 // 超時設(shè)置為10秒
});
// 2. 創(chuàng)建商品服務(wù)專用實例(帶特殊請求頭)
const productApi = new HttpClient({
baseURL: 'https://api.example.com/product', // 商品服務(wù)基礎(chǔ)路徑
headers: { 'X-Product-Token': 'special-token' } // 商品服務(wù)需要的特殊頭部
});
// 使用多實例處理不同服務(wù)的請求
async function useMultiInstances() {
// 調(diào)用用戶服務(wù)API
const userPage = await userApi.get<PageResponse<User>>('/list');
// 調(diào)用商品服務(wù)API
const productPage = await productApi.get<PageResponse<Product>>('/list');
// 兩個實例的配置完全隔離,不會相互影響
}適用場景:
- 前后端分離項目中對接多個微服務(wù)
- 同時需要訪問內(nèi)部API和第三方API
- 不同接口有不同的超時需求(如普通接口5秒,文件上傳60秒)
3. 自定義攔截器:業(yè)務(wù)邏輯與請求處理的解耦
攔截器是處理請求/響應公共邏輯的最佳方式,HttpClient 允許在初始化時配置自定義攔截器,將認證、日志等橫切關(guān)注點與業(yè)務(wù)邏輯分離。
最常見的場景是處理認證邏輯:
// 創(chuàng)建帶權(quán)限驗證的HTTP客戶端實例
const authHttp = new HttpClient(
{ baseURL: 'https://api.example.com/auth' }, // 基礎(chǔ)配置
{
// 請求攔截器:添加認證Token
requestInterceptor: (config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
// 響應攔截器:處理認證失敗
responseInterceptor: (response) => {
// 未授權(quán),自動跳轉(zhuǎn)到登錄頁
if (response.code === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return response;
}
}
);攔截器的典型用途:
- 添加全局認證信息(Token、API Key等)
- 統(tǒng)一處理錯誤碼(如401未授權(quán)、403權(quán)限不足)
- 實現(xiàn)請求/響應日志記錄
- 添加請求時間戳防止緩存
4. 動態(tài)修改攔截器:運行時靈活調(diào)整請求行為
有時候我們需要在運行時根據(jù)業(yè)務(wù)場景動態(tài)改變請求/響應處理邏輯,HttpClient 提供了動態(tài)修改攔截器的能力。
// 定義日志數(shù)據(jù)接口
interface LogData {
id: number;
timestamp: string;
content: string;
}
async function dynamicInterceptors() {
// 場景1:臨時添加日志攔截器
const logInterceptor = (response: AxiosResponse<ResponseData<PageResponse<LogData>>>) => {
console.log(`請求[${response.config.url}]返回${response.data.total}條日志`);
return response;
};
// 設(shè)置新的響應攔截器
http.setResponseInterceptor(logInterceptor);
// 發(fā)送請求時會執(zhí)行新的攔截器
await http.get<PageResponse<LogData>>('/api/logs');
// 場景2:完成日志收集后,恢復默認攔截器
http.setResponseInterceptor();
// 場景3:動態(tài)更新認證信息(如Token刷新后)
const newToken = 'new-auth-token';
http.setRequestInterceptor((config) => {
config.headers.Authorization = `Bearer ${newToken}`;
return config;
});
}實用場景:
- 臨時開啟調(diào)試日志
- Token過期后動態(tài)更新認證信息
- 特定頁面需要特殊的請求頭
- A/B測試時切換不同的API處理邏輯
5. 請求取消:優(yōu)化用戶體驗的關(guān)鍵技巧
在用戶快速操作或頁面切換時,取消無用的請求可以顯著提升性能和用戶體驗。HttpClient 提供了多種靈活的請求取消方式。
5.1 主動取消單個請求
async function cancelSingleRequest() {
const requestKey = 'user-list'; // 定義唯一標識
try {
// 發(fā)起請求時指定requestKey
const promise = http.get<PageResponse<User>>('/api/users', { requestKey });
// 模擬:200ms后取消請求(例如用戶快速切換了頁面)
setTimeout(() => {
http.cancelRequest(requestKey, '數(shù)據(jù)已過時');
}, 200);
const result = await promise;
} catch (error) {
// 判斷是否為取消錯誤
if (HttpClient.isCancel(error)) {
console.log('請求已取消:', error.message);
}
}
}5.2 自動取消重復請求
async function cancelDuplicate() {
// 連續(xù)發(fā)起相同參數(shù)的請求
http.get<PageResponse<User>>('/api/users', { params: { page: 1 } }); // 被取消
http.get<PageResponse<User>>('/api/users', { params: { page: 1 } }); // 被取消
const latestData = await http.get<PageResponse<User>>('/api/users', { params: { page: 1 } }); // 最終生效
}5.3 頁面卸載時取消所有請求
// 在React/Vue等框架的組件卸載鉤子中調(diào)用
function onPageUnmount() {
http.cancelAllRequests('頁面已關(guān)閉');
}
// 或者監(jiān)聽頁面關(guān)閉事件
window.addEventListener('beforeunload', () => {
http.cancelAllRequests('用戶離開頁面');
});帶來的好處:
- 減少不必要的網(wǎng)絡(luò)請求和服務(wù)器負載
- 避免過時數(shù)據(jù)覆蓋最新數(shù)據(jù)
- 防止頁面跳轉(zhuǎn)后仍彈出錯誤提示
- 減少內(nèi)存占用和潛在的內(nèi)存泄漏
6. 文件上傳:大文件處理的最佳實踐
文件上傳是前端開發(fā)中的常見需求,尤其需要注意超時設(shè)置和數(shù)據(jù)格式。HttpClient 可以輕松配置適合文件上傳的參數(shù)。
// 定義上傳結(jié)果接口
interface UploadResult {
url: string; // 上傳后的文件URL
filename: string; // 文件名
size: number; // 文件大小
}
// 處理文件上傳
async function uploadFile(file: File) {
// 創(chuàng)建FormData對象
const formData = new FormData();
formData.append('file', file);
// 可選:添加其他表單字段
formData.append('category', 'document');
formData.append('description', '用戶上傳的文檔');
// 創(chuàng)建上傳專用實例(配置更長的超時)
const uploadHttp = new HttpClient({
timeout: 60000, // 上傳超時設(shè)為60秒
headers: { 'Content-Type': 'multipart/form-data' }
});
try {
const result = await uploadHttp.post<UploadResult>('/api/upload', formData);
console.log('文件上傳成功,訪問地址:', result.url);
return result.url;
} catch (error) {
console.error('文件上傳失敗:', error);
throw error;
}
}上傳優(yōu)化建議:
- 大文件上傳使用專門的實例,設(shè)置較長超時
- 配合進度條展示上傳進度(可通過 Axios 的 onUploadProgress 實現(xiàn))
- 考慮分片上傳大文件(超過100MB的文件)
- 重要文件上傳可配置重試機制
7. 并發(fā)請求處理:高效獲取多源數(shù)據(jù)
實際開發(fā)中經(jīng)常需要同時請求多個接口,然后匯總處理數(shù)據(jù)。HttpClient 結(jié)合 Promise API 可以優(yōu)雅地處理并發(fā)請求。
// 使用Promise.all處理并發(fā)請求
async function handleConcurrentRequests() {
try {
// 同時發(fā)起多個請求
const [userRes, productRes] = await Promise.all([
http.get<User>('/api/users/1'), // 獲取用戶詳情
http.get<PageResponse<Product>>('/api/products') // 獲取商品列表
]);
// 所有請求成功后處理數(shù)據(jù)
console.log('用戶詳情:', userRes);
console.log('商品列表:', productRes.list);
// 可以在這里進行數(shù)據(jù)整合
return {
user: userRes,
products: productRes.list
};
} catch (error) {
// 任何一個請求失敗都會進入這里
console.error('并發(fā)請求失敗:', error);
throw error;
}
}并發(fā)處理技巧:
- 使用
Promise.all處理相互依賴的并發(fā)請求(一失敗全失敗) - 使用
Promise.allSettled處理可以獨立失敗的請求 - 對大量并發(fā)請求進行分批處理,避免瀏覽器限制
- 結(jié)合請求取消機制,在某個關(guān)鍵請求失敗時取消其他請求
8. 請求重試:提升網(wǎng)絡(luò)不穩(wěn)定場景的可靠性
網(wǎng)絡(luò)波動是前端請求失敗的常見原因,HttpClient 內(nèi)置的重試機制可以自動處理這類問題,提升用戶體驗。
8.1 基礎(chǔ)重試配置(使用默認策略)
// 使用默認重試條件
async function basicRetryWithDefaults() {
try {
const result = await http.get<User>('/api/users/1', {
retry: {
retries: 3, // 最多重試3次
retryDelay: 1000 // 每次重試間隔1秒
// 默認策略:只重試網(wǎng)絡(luò)錯誤或5xx服務(wù)器錯誤
}
});
return result;
} catch (error) {
console.error('所有重試都失敗了:', error);
throw error;
}
}8.2 自定義重試條件
// 自定義重試邏輯
async function customRetryCondition(userData: User) {
try {
const result = await http.post<User>('/api/users', userData, {
retry: {
retries: 2, // 重試2次
retryDelay: 500, // 重試間隔500ms
retryCondition: (error) => {
// 自定義條件:網(wǎng)絡(luò)錯誤、超時或5xx錯誤才重試
return !error.response ||
error.response.status === 408 ||
(error.response.status >= 500 && error.response.status < 600);
}
}
});
return result;
} catch (error) {
console.error('重試失敗:', error);
throw error;
}
}重試策略建議:
- 讀操作(GET)適合重試,寫操作(POST/PUT)需謹慎
- 重試次數(shù)不宜過多(通常2-3次),避免加重服務(wù)器負擔
- 使用指數(shù)退避策略(retryDelay 逐漸增加)
- 對明確的客戶端錯誤(如400、401、403)不重試
9. 訪問原始 Axios 實例:兼容特殊需求
雖然 HttpClient 封裝了常用功能,但某些特殊場景可能需要直接使用 Axios 原生 API。HttpClient 提供了獲取原始實例的方法。
// 獲取原始Axios實例
function getOriginalAxiosInstance() {
const axiosInstance = http.getInstance();
// 示例1:使用Axios的cancelToken(舊版取消方式)
const CancelToken = Axios.CancelToken;
const source = CancelToken.source();
axiosInstance.get('/api/special', {
cancelToken: source.token
});
// 取消請求
source.cancel('Operation canceled by the user.');
// 示例2:使用Axios的攔截器API
const myInterceptor = axiosInstance.interceptors.response.use(
response => response,
error => Promise.reject(error)
);
// 移除攔截器
axiosInstance.interceptors.response.eject(myInterceptor);
}適用場景:
- 使用一些 HttpClient 未封裝的 Axios 特性
- 集成依賴原始 Axios 實例的第三方庫
- 處理極特殊的請求場景
- 平滑遷移現(xiàn)有基于 Axios 的代碼
插曲:朋友的無情嘲笑
在我們完成本次封裝前,還有一個小插曲:我得意洋洋地把這個"史上最優(yōu)雅"的封裝發(fā)給朋友炫耀,心想著他肯定會夸我兩句。結(jié)果他發(fā)來了一大段文字...
第一輪攻擊:類型定義問題
朋友: "你這代碼有點問題啊 ?? 你這個 ResponseData 類型擴展性不足:"
// 你的問題代碼
export type ResponseData<T = any> = BaseResponse & T;
// 當 T 中包含 code 或 message 字段時會沖突
interface UserWithCode {
code: string; // 與BaseResponse沖突
name: string;
}
type TestType = ResponseData<UserWithCode>; // code字段變成never類型!我: "不可能!絕對不可能!(曹操.gif)"
然后我測試了一下,果然報錯了... ??
解決方案:
// 修復后的版本 type OmitBaseResponse<T> = Omit<T, keyof BaseResponse>; export type ResponseData<T = any> = BaseResponse & OmitBaseResponse<T>;
第二輪攻擊:請求取消邏輯缺陷
朋友: "還有你這個 setupCancelController 未處理自定義 requestKey 沖突,當用戶傳入自定義 requestKey 時,若與內(nèi)部生成的鍵重復,會導致取消邏輯混亂。"
我: "這... 這應該不會吧?"
朋友: "你看,你的代碼是這樣的:
private setupCancelController(config: AxiosRequestConfig, requestKey?: RequestKey) {
const key = requestKey || this.getRequestKey(config);
// 直接取消,但沒有沖突警告
this.cancelRequest(key);
}如果有重復key怎么辦?建議加個警告。"
我: "但是我這里直接取消重復請求不是挺好的嗎?這是防重復機制啊!"
朋友: "嗯...這個倒是有道理。那算了,這個問題不大。"
第三輪攻擊:重試機制邊界問題
朋友: "但是你這個重試邏輯有問題!重試時未重置 AbortController:"
// 你的問題代碼
for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
// 每次都會調(diào)用setupCancelController,創(chuàng)建新的controller
// 但舊的還在Map中,可能導致重試請求被誤取消
const requestConfig = this.setupCancelController({...restConfig, method, url}, requestKey);
}我: "這... 這是邊緣情況!"
朋友: "邊緣情況也是情況?。∵€有你的 retryCondition 默認值缺失,當用戶未配置時,會默認重試所有錯誤(包括400等客戶端錯誤),不符合預期。"
我: "好吧好吧,我改還不行嗎... ??"
解決方案:
// 修復后的版本
const defaultRetryCondition = (error: AxiosError) => {
// 默認只重試網(wǎng)絡(luò)錯誤或5xx服務(wù)器錯誤
return !error.response || (error.response.status >= 500 && error.response.status < 600);
};
for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
// 重試前清除舊控制器
if (attempt > 0 && key) {
this.abortControllers.delete(key);
}
const requestConfig = this.setupCancelController({...restConfig, method, url}, requestKey);
}第四輪攻擊:攔截器管理問題
朋友: "還有你的攔截器移除邏輯不嚴謹,只通過 interceptorId 移除,但未重置 interceptorId,可能導致后續(xù)重復移除無效:"
// 你的問題代碼
public removeRequestInterceptor(): void {
if (this.requestInterceptorId) {
this.instance.interceptors.request.eject(this.requestInterceptorId);
// 沒有重置ID!
}
}我: "這... 好吧,確實應該重置一下。"
解決方案:
// 修復后的版本
public removeRequestInterceptor(): void {
if (this.requestInterceptorId !== undefined) {
this.instance.interceptors.request.eject(this.requestInterceptorId);
this.requestInterceptorId = undefined; // 重置ID
}
}第五輪攻擊:其他細節(jié)問題
朋友: "哈哈,別急。不過你還有幾個問題:
Content-Type 硬編碼問題 - 默認強制設(shè)置為
application/json,但上傳文件時需要multipart/form-data,需手動覆蓋,不夠靈活。錯誤信息處理冗余 - 響應錯誤攔截器中對錯誤信息的包裝會丟失原始錯誤的詳細信息,不利于調(diào)試。
requestKey 類型聲明不明確 - 定義為
string | symbol,但用戶傳入symbol時,調(diào)試信息顯示不友好。"
我: "停停停!你這是在 code review 還是在找茬?!"
朋友: "當然是 code review 啦,不過后面這幾個確實比較雞蛋里挑骨頭,前面幾個確實需要修復。"
我的反擊與分析
經(jīng)過一番"友好"的討論(主要是我被教育),我冷靜分析了一下:
確實有價值的問題(必須修復):
- ? ResponseData 類型沖突 - 很重要!確實會導致
never類型問題 - ? 重試機制的默認條件缺失 - 重要!應該有合理的默認重試條件
- ? 攔截器ID重置問題 - 中等重要,確實應該重置ID
- ? 重試時AbortController重置 - 中等重要,理論上存在問題
過于苛刻或設(shè)計選擇問題:
- ? requestKey沖突警告 - 當前設(shè)計已經(jīng)通過取消舊請求處理了,警告是多余的
- ? Content-Type硬編碼 - 這是常見的默認設(shè)置,Axios會自動覆蓋FormData
- ? 錯誤信息包裝 - 保留原始錯誤是好的,但當前設(shè)計也合理
- ? 攔截器組合模式 - 當前的覆蓋模式是主流設(shè)計,組合模式會增加復雜性
主觀性問題:
- ?? requestKey類型限制 - symbol支持是特性,不是缺陷
最終我不得不承認:朋友的技術(shù)功底是不錯的,提出了一些確實存在的邊緣問題。但有些建議過于"完美主義",可能會讓代碼變得過于復雜。
修復了前4個重要問題后,朋友終于點頭說:"現(xiàn)在看起來像個正經(jīng)的封裝了!不過你得承認,好的代碼不僅要能跑,還要經(jīng)得起同行的審視。" ??
寫在最后
經(jīng)過這一上午的"激情"編碼 + 朋友的"無情"嘲笑 + 我的"不服氣"修復,這個 HTTP 客戶端封裝終于變得更加健壯了。
從最初的自我感覺良好,到被朋友無情打臉,再到最后的虛心修復,這個過程讓我深刻體會到:
- 沒有完美的代碼 - 總有你想不到的邊緣情況
- Code Review 很重要 - 別人的視角能發(fā)現(xiàn)你的盲點
- 保持開放心態(tài) - 被指出問題是好事,不是壞事
- 朋友很重要 - 能"嘲笑"你代碼的朋友才是真朋友 ??
現(xiàn)在這個封裝不僅解決了日常開發(fā)中的痛點,還具備了企業(yè)級項目的穩(wěn)定性。
核心優(yōu)勢:
- ?? 開箱即用 - 無需復雜配置,默認就很好用
- ?? 高度可定制 - 支持各種業(yè)務(wù)場景的定制需求
- ??? 類型安全 - TypeScript 完美支持,減少運行時錯誤
- ? 性能出色 - 智能去重、重試、取消機制
- ?? 文檔完善 - 詳細的示例和注釋
如果你也在為 HTTP 請求封裝而苦惱,不妨試試這個方案。記住,好的代碼是改出來的,不是寫出來的!
到此這篇關(guān)于前端必看之超優(yōu)雅的Axios封裝實踐的文章就介紹到這了,更多相關(guān)優(yōu)雅的Axios封裝內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript定義函數(shù)的三種實現(xiàn)方法
這篇文章主要介紹了JavaScript定義函數(shù)的三種實現(xiàn)方法的相關(guān)資料,希望通過本文大家能夠掌握三種定義函數(shù)的方法,需要的朋友可以參考下2017-09-09
javascript實現(xiàn)base64 md5 sha1 密碼加密
本篇文章給大家介紹了javascript實現(xiàn)密碼加密,通過base64、md5、sha1文件,調(diào)用相關(guān)方法實現(xiàn)密碼加密,非常簡單,需要的朋友可以參考下2015-09-09
JS實現(xiàn)點擊復選框變更DIV顯示狀態(tài)的示例代碼
下面小編就為大家分享一篇JS實現(xiàn)點擊復選框變更DIV顯示狀態(tài)的示例代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2017-12-12
微信小程序?qū)崿F(xiàn)的一鍵連接wifi功能示例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)的一鍵連接wifi功能,結(jié)合實例形式分析了微信小程序操作WiFi連接的模塊初始化、配置、連接等相關(guān)操作技巧,需要的朋友可以參考下2019-04-04

