Skip to content

企业级请求层封装实战

🎯 从零构建生产级别的 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}&timestamp=${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 → D

Q2: 如何实现 Token 无感刷新?

  1. 响应拦截器捕获 401 错误
  2. 使用一个标志位防止并发刷新
  3. 其他 401 请求加入等待队列
  4. 刷新成功后,通知队列中的请求重试
  5. 刷新失败,清除 Token,跳转登录

Q3: 如何取消重复请求?

  1. 使用 AbortController 创建 signal
  2. 请求发起时,用 method + url + params 生成唯一 key
  3. 检查缓存是否存在相同 key 的请求
  4. 如果存在,abort 旧请求
  5. 将新请求加入缓存

Q4: 如何实现请求重试?

  1. 响应拦截器捕获错误
  2. 判断是否可重试 (网络错误、5xx、429)
  3. 记录重试次数,超过上限则不重试
  4. 使用指数退避延迟
  5. 递归调用 axios(config)

Q5: 请求层封装的核心原则?

  1. 单一职责: 每个拦截器只负责一件事
  2. 可配置: 通过 config 控制行为 (showError、retryCount)
  3. 可扩展: 便于添加新的拦截器
  4. 类型安全: 完整的 TypeScript 类型定义
  5. 服务分层: API 服务按领域划分

前端面试知识库