企业级请求层封装实战
🎯 从零构建生产级别的 HTTP 请求库,覆盖拦截器、重试、取消、错误处理等核心场景
1. 请求层架构设计
1.1 整体架构
┌─────────────────────────────────────────────────────────────────────────┐
│ 请求层架构设计 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 业务组件 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ useUserList() useOrderDetail() useProductSearch() │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ API 服务层 (按领域划分) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ userService orderService productService │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ getUser() │ │ getOrder()│ │ search() │ │ │
│ │ │ login() │ │ create() │ │ getDetail()│ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 请求核心层 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ Request Interceptors │ │ │
│ │ │ • Token 注入 │ │ │
│ │ │ • 请求签名 │ │ │
│ │ │ • 请求日志 │ │ │
│ │ │ • 取消重复请求 │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ HTTP Client (Axios) │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ Response Interceptors │ │ │
│ │ │ • 数据解包 │ │ │
│ │ │ • 错误处理 │ │ │
│ │ │ • Token 刷新 │ │ │
│ │ │ • 重试逻辑 │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘1.2 目录结构
src/
├── api/
│ ├── index.ts # 统一导出
│ ├── request.ts # 请求核心 (Axios 实例 + 拦截器)
│ ├── types.ts # 类型定义
│ │
│ ├── interceptors/
│ │ ├── auth.ts # 认证拦截器
│ │ ├── error.ts # 错误处理拦截器
│ │ ├── retry.ts # 重试拦截器
│ │ ├── cancel.ts # 取消请求拦截器
│ │ └── logger.ts # 日志拦截器
│ │
│ └── services/
│ ├── user.ts # 用户服务
│ ├── order.ts # 订单服务
│ └── product.ts # 商品服务2. 核心请求封装
2.1 基础 Axios 实例
typescript
// src/api/request.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { setupAuthInterceptor } from './interceptors/auth';
import { setupErrorInterceptor } from './interceptors/error';
import { setupRetryInterceptor } from './interceptors/retry';
import { setupCancelInterceptor } from './interceptors/cancel';
// 响应数据结构
export interface ApiResponse<T = any> {
code: number;
data: T;
message: string;
}
// 扩展 AxiosRequestConfig
export interface RequestConfig extends AxiosRequestConfig {
// 是否显示全局 Loading
showLoading?: boolean;
// 是否显示错误提示
showError?: boolean;
// 重试次数
retryCount?: number;
// 重试延迟 (ms)
retryDelay?: number;
// 是否取消重复请求
cancelDuplicate?: boolean;
// 跳过 Token
skipAuth?: boolean;
}
class HttpClient {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors() {
// 注意顺序:请求拦截器后添加的先执行,响应拦截器先添加的先执行
setupCancelInterceptor(this.instance);
setupAuthInterceptor(this.instance);
setupRetryInterceptor(this.instance);
setupErrorInterceptor(this.instance);
}
// 通用请求方法
async request<T = any>(config: RequestConfig): Promise<T> {
const response = await this.instance.request<ApiResponse<T>>(config);
return response.data.data;
}
// GET
async get<T = any>(url: string, config?: RequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'GET', url });
}
// POST
async post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'POST', url, data });
}
// PUT
async put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'PUT', url, data });
}
// DELETE
async delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'DELETE', url });
}
// PATCH
async patch<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'PATCH', url, data });
}
}
export const http = new HttpClient();
export default http;2.2 类型定义
typescript
// src/api/types.ts
// 分页请求参数
export interface PaginationParams {
page: number;
pageSize: number;
}
// 分页响应
export interface PaginatedResponse<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
// 通用错误
export class ApiError extends Error {
code: number;
data?: any;
constructor(code: number, message: string, data?: any) {
super(message);
this.name = 'ApiError';
this.code = code;
this.data = data;
}
}
// 错误码枚举
export enum ErrorCode {
SUCCESS = 0,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
VALIDATION_ERROR = 422,
SERVER_ERROR = 500,
// 业务错误码
TOKEN_EXPIRED = 10001,
TOKEN_INVALID = 10002,
USER_NOT_FOUND = 20001,
INSUFFICIENT_BALANCE = 30001,
}3. 拦截器实现
3.1 认证拦截器
typescript
// src/api/interceptors/auth.ts
import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import { RequestConfig } from '../request';
// Token 存储
const TOKEN_KEY = 'access_token';
const REFRESH_TOKEN_KEY = 'refresh_token';
export function getToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
export function setToken(token: string): void {
localStorage.setItem(TOKEN_KEY, token);
}
export function clearToken(): void {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
}
// Token 刷新锁
let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];
function subscribeTokenRefresh(callback: (token: string) => void) {
refreshSubscribers.push(callback);
}
function onTokenRefreshed(token: string) {
refreshSubscribers.forEach((callback) => callback(token));
refreshSubscribers = [];
}
// 刷新 Token
async function refreshToken(): Promise<string> {
const refreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
// 注意:刷新 Token 的请求不能走主实例,否则会循环
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const data = await response.json();
return data.data.accessToken;
}
export function setupAuthInterceptor(instance: AxiosInstance) {
// 请求拦截:注入 Token
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig & RequestConfig) => {
// 某些接口不需要 Token
if (config.skipAuth) {
return config;
}
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截:处理 401
instance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// 401 且不是刷新 Token 的请求
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// 等待 Token 刷新完成
return new Promise((resolve) => {
subscribeTokenRefresh((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(instance(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const newToken = await refreshToken();
setToken(newToken);
onTokenRefreshed(newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return instance(originalRequest);
} catch (refreshError) {
// 刷新失败,跳转登录
clearToken();
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
}3.2 错误处理拦截器
typescript
// src/api/interceptors/error.ts
import { AxiosInstance, AxiosError } from 'axios';
import { ApiError, ErrorCode } from '../types';
import { message } from 'antd'; // 或其他 UI 库
// 错误消息映射
const ERROR_MESSAGES: Record<number, string> = {
[ErrorCode.UNAUTHORIZED]: '登录已过期,请重新登录',
[ErrorCode.FORBIDDEN]: '没有权限访问',
[ErrorCode.NOT_FOUND]: '请求的资源不存在',
[ErrorCode.VALIDATION_ERROR]: '请求参数错误',
[ErrorCode.SERVER_ERROR]: '服务器错误,请稍后重试',
[ErrorCode.TOKEN_EXPIRED]: '登录已过期',
[ErrorCode.USER_NOT_FOUND]: '用户不存在',
};
export function setupErrorInterceptor(instance: AxiosInstance) {
instance.interceptors.response.use(
(response) => {
const { data } = response;
// 业务错误 (code !== 0)
if (data.code !== 0) {
const errorMessage = data.message || ERROR_MESSAGES[data.code] || '请求失败';
// 全局错误提示
const config = response.config as any;
if (config.showError !== false) {
message.error(errorMessage);
}
throw new ApiError(data.code, errorMessage, data.data);
}
return response;
},
(error: AxiosError) => {
// 请求被取消
if (error.code === 'ERR_CANCELED') {
console.log('Request canceled:', error.config?.url);
return Promise.reject(error);
}
// 网络错误
if (!error.response) {
message.error('网络连接失败,请检查网络');
return Promise.reject(new ApiError(-1, '网络错误'));
}
// HTTP 错误
const status = error.response.status;
const errorMessage = ERROR_MESSAGES[status] || `请求失败 (${status})`;
const config = error.config as any;
if (config?.showError !== false && status !== 401) {
message.error(errorMessage);
}
return Promise.reject(new ApiError(status, errorMessage));
}
);
}3.3 重试拦截器
typescript
// src/api/interceptors/retry.ts
import { AxiosInstance, AxiosError } from 'axios';
import { RequestConfig } from '../request';
const DEFAULT_RETRY_COUNT = 3;
const DEFAULT_RETRY_DELAY = 1000;
// 可重试的错误
function isRetryable(error: AxiosError): boolean {
// 网络错误
if (!error.response) return true;
// 5xx 服务器错误
if (error.response.status >= 500) return true;
// 429 Too Many Requests
if (error.response.status === 429) return true;
return false;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function setupRetryInterceptor(instance: AxiosInstance) {
instance.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const config = error.config as RequestConfig & { _retryCount?: number };
if (!config || !isRetryable(error)) {
return Promise.reject(error);
}
const maxRetries = config.retryCount ?? DEFAULT_RETRY_COUNT;
const retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY;
config._retryCount = config._retryCount ?? 0;
if (config._retryCount >= maxRetries) {
return Promise.reject(error);
}
config._retryCount += 1;
console.log(`Retry ${config._retryCount}/${maxRetries}: ${config.url}`);
// 指数退避
const delay = retryDelay * Math.pow(2, config._retryCount - 1);
await sleep(delay);
return instance(config);
}
);
}3.4 取消请求拦截器
typescript
// src/api/interceptors/cancel.ts
import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import { RequestConfig } from '../request';
// 请求缓存 Map
const pendingRequests = new Map<string, AbortController>();
// 生成请求唯一 Key
function generateRequestKey(config: InternalAxiosRequestConfig): string {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
// 取消重复请求
function removePendingRequest(config: InternalAxiosRequestConfig) {
const key = generateRequestKey(config);
if (pendingRequests.has(key)) {
const controller = pendingRequests.get(key)!;
controller.abort();
pendingRequests.delete(key);
}
}
export function setupCancelInterceptor(instance: AxiosInstance) {
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig & RequestConfig) => {
// 不需要取消重复请求
if (config.cancelDuplicate === false) {
return config;
}
// 取消之前的相同请求
removePendingRequest(config);
// 创建新的 AbortController
const controller = new AbortController();
config.signal = controller.signal;
const key = generateRequestKey(config);
pendingRequests.set(key, controller);
return config;
}
);
instance.interceptors.response.use(
(response) => {
// 请求完成,从缓存中移除
const key = generateRequestKey(response.config);
pendingRequests.delete(key);
return response;
},
(error) => {
if (error.config) {
const key = generateRequestKey(error.config);
pendingRequests.delete(key);
}
return Promise.reject(error);
}
);
}
// 导出取消所有请求的方法 (路由切换时使用)
export function cancelAllRequests() {
pendingRequests.forEach((controller) => {
controller.abort();
});
pendingRequests.clear();
}4. API 服务层
4.1 用户服务示例
typescript
// src/api/services/user.ts
import http from '../request';
import { PaginatedResponse, PaginationParams } from '../types';
// 类型定义
export interface User {
id: number;
username: string;
email: string;
avatar: string;
role: 'admin' | 'user';
createdAt: string;
}
export interface LoginParams {
username: string;
password: string;
}
export interface LoginResult {
accessToken: string;
refreshToken: string;
user: User;
}
export interface UpdateUserParams {
username?: string;
email?: string;
avatar?: string;
}
// API 定义
export const userService = {
// 登录
login: (params: LoginParams) =>
http.post<LoginResult>('/auth/login', params, { skipAuth: true }),
// 登出
logout: () =>
http.post('/auth/logout'),
// 获取当前用户
getCurrentUser: () =>
http.get<User>('/user/me'),
// 获取用户列表
getUsers: (params: PaginationParams) =>
http.get<PaginatedResponse<User>>('/users', { params }),
// 获取用户详情
getUserById: (id: number) =>
http.get<User>(`/users/${id}`),
// 更新用户
updateUser: (id: number, data: UpdateUserParams) =>
http.patch<User>(`/users/${id}`, data),
// 删除用户
deleteUser: (id: number) =>
http.delete(`/users/${id}`),
// 上传头像
uploadAvatar: (file: File) => {
const formData = new FormData();
formData.append('file', file);
return http.post<{ url: string }>('/user/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
},
};4.2 使用 React Query/SWR 封装
typescript
// src/hooks/useUser.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userService, User, UpdateUserParams } from '@/api/services/user';
// 查询 Keys
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (params: any) => [...userKeys.lists(), params] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: number) => [...userKeys.details(), id] as const,
me: () => [...userKeys.all, 'me'] as const,
};
// 获取当前用户
export function useCurrentUser() {
return useQuery({
queryKey: userKeys.me(),
queryFn: () => userService.getCurrentUser(),
staleTime: 5 * 60 * 1000, // 5 分钟内不重新获取
});
}
// 获取用户列表
export function useUsers(page: number, pageSize: number) {
return useQuery({
queryKey: userKeys.list({ page, pageSize }),
queryFn: () => userService.getUsers({ page, pageSize }),
placeholderData: (previousData) => previousData, // 保留上一页数据
});
}
// 获取用户详情
export function useUser(id: number) {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => userService.getUserById(id),
enabled: !!id,
});
}
// 更新用户
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: UpdateUserParams }) =>
userService.updateUser(id, data),
onSuccess: (updatedUser, { id }) => {
// 更新缓存
queryClient.setQueryData(userKeys.detail(id), updatedUser);
// 使列表缓存失效
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
// 删除用户
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => userService.deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userKeys.all });
},
});
}5. 高级功能
5.1 请求签名
typescript
// src/api/interceptors/signature.ts
import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import CryptoJS from 'crypto-js';
const APP_KEY = import.meta.env.VITE_APP_KEY;
const APP_SECRET = import.meta.env.VITE_APP_SECRET;
// 生成签名
function generateSignature(params: Record<string, any>, timestamp: number): string {
// 1. 参数排序
const sortedKeys = Object.keys(params).sort();
// 2. 拼接字符串
const queryString = sortedKeys
.map((key) => `${key}=${params[key]}`)
.join('&');
// 3. 加入时间戳和密钥
const signString = `${queryString}×tamp=${timestamp}&secret=${APP_SECRET}`;
// 4. MD5 加密
return CryptoJS.MD5(signString).toString();
}
export function setupSignatureInterceptor(instance: AxiosInstance) {
instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const timestamp = Date.now();
// 合并所有参数
const allParams = {
...config.params,
...(typeof config.data === 'object' ? config.data : {}),
};
// 生成签名
const signature = generateSignature(allParams, timestamp);
// 添加到 Header
config.headers['X-App-Key'] = APP_KEY;
config.headers['X-Timestamp'] = timestamp;
config.headers['X-Signature'] = signature;
return config;
});
}5.2 请求节流
typescript
// src/api/interceptors/throttle.ts
import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
const throttleCache = new Map<string, { promise: Promise<any>; timestamp: number }>();
const THROTTLE_TIME = 500; // 500ms 内的相同请求返回缓存结果
export function setupThrottleInterceptor(instance: AxiosInstance) {
// 自定义 Adapter
const originalAdapter = instance.defaults.adapter;
instance.defaults.adapter = async (config) => {
const key = `${config.method}:${config.url}:${JSON.stringify(config.params)}`;
const now = Date.now();
// 检查缓存
const cached = throttleCache.get(key);
if (cached && now - cached.timestamp < THROTTLE_TIME) {
console.log(`Throttled: ${config.url}`);
return cached.promise;
}
// 发起新请求
const promise = (originalAdapter as any)(config);
throttleCache.set(key, { promise, timestamp: now });
// 清理过期缓存
setTimeout(() => {
throttleCache.delete(key);
}, THROTTLE_TIME);
return promise;
};
}5.3 离线请求队列
typescript
// src/api/offlineQueue.ts
import { RequestConfig } from './request';
interface QueuedRequest {
config: RequestConfig;
resolve: (value: any) => void;
reject: (reason: any) => void;
}
class OfflineQueue {
private queue: QueuedRequest[] = [];
private isOnline = navigator.onLine;
constructor() {
window.addEventListener('online', () => this.onOnline());
window.addEventListener('offline', () => this.onOffline());
}
private onOnline() {
this.isOnline = true;
this.flush();
}
private onOffline() {
this.isOnline = false;
}
// 添加请求到队列
add(config: RequestConfig): Promise<any> {
return new Promise((resolve, reject) => {
this.queue.push({ config, resolve, reject });
this.persist();
});
}
// 执行队列中的请求
async flush() {
const http = (await import('./request')).default;
while (this.queue.length > 0) {
const { config, resolve, reject } = this.queue.shift()!;
try {
const result = await http.request(config);
resolve(result);
} catch (error) {
reject(error);
}
}
this.persist();
}
// 持久化到 localStorage
private persist() {
const configs = this.queue.map((item) => item.config);
localStorage.setItem('offline_queue', JSON.stringify(configs));
}
// 从 localStorage 恢复
restore() {
const stored = localStorage.getItem('offline_queue');
if (stored) {
const configs: RequestConfig[] = JSON.parse(stored);
configs.forEach((config) => this.add(config));
}
}
}
export const offlineQueue = new OfflineQueue();6. 请求层架构 UML
┌─────────────────────────────────────────────────────────────────────────┐
│ 请求层类图 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ HttpClient │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ - instance: AxiosInstance │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ + get<T>(url, config?): Promise<T> │ │
│ │ + post<T>(url, data?, config?): Promise<T> │ │
│ │ + put<T>(url, data?, config?): Promise<T> │ │
│ │ + delete<T>(url, config?): Promise<T> │ │
│ │ + patch<T>(url, data?, config?): Promise<T> │ │
│ │ - setupInterceptors(): void │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ uses │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ <<interface>> │ │
│ │ Interceptor │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ + setup(instance: AxiosInstance): void │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ ┌────────────────────┼────────────────────┐ │
│ │ │ │ │
│ ┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴───────┐ │
│ │ AuthInterceptor│ │ErrorInterceptor│ │RetryInterceptor│ │
│ ├───────────────┤ ├───────────────┤ ├───────────────┤ │
│ │ - getToken() │ │ - showError() │ │ - maxRetries │ │
│ │ - setToken() │ │ - errorMap │ │ - retryDelay │ │
│ │ - refresh() │ └───────────────┘ │ - isRetryable()│ │
│ └───────────────┘ └───────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ ApiService │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ - http: HttpClient │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ + getList(params): Promise<PaginatedResponse> │ │
│ │ + getById(id): Promise<T> │ │
│ │ + create(data): Promise<T> │ │
│ │ + update(id, data): Promise<T> │ │
│ │ + delete(id): Promise<void> │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘7. 面试高频问题
Q1: Axios 拦截器的执行顺序?
javascript
// 请求拦截器:后添加的先执行 (栈)
axios.interceptors.request.use(A);
axios.interceptors.request.use(B);
// 执行顺序: B → A → 发送请求
// 响应拦截器:先添加的先执行 (队列)
axios.interceptors.response.use(C);
axios.interceptors.response.use(D);
// 执行顺序: 接收响应 → C → DQ2: 如何实现 Token 无感刷新?
- 响应拦截器捕获 401 错误
- 使用一个标志位防止并发刷新
- 其他 401 请求加入等待队列
- 刷新成功后,通知队列中的请求重试
- 刷新失败,清除 Token,跳转登录
Q3: 如何取消重复请求?
- 使用 AbortController 创建 signal
- 请求发起时,用 method + url + params 生成唯一 key
- 检查缓存是否存在相同 key 的请求
- 如果存在,abort 旧请求
- 将新请求加入缓存
Q4: 如何实现请求重试?
- 响应拦截器捕获错误
- 判断是否可重试 (网络错误、5xx、429)
- 记录重试次数,超过上限则不重试
- 使用指数退避延迟
- 递归调用 axios(config)
Q5: 请求层封装的核心原则?
- 单一职责: 每个拦截器只负责一件事
- 可配置: 通过 config 控制行为 (showError、retryCount)
- 可扩展: 便于添加新的拦截器
- 类型安全: 完整的 TypeScript 类型定义
- 服务分层: API 服务按领域划分