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

