Vue项目中Axios全面封装实战指南

引言

在现代前端开发中,HTTP请求是应用与后端交互的核心环节。Axios作为目前最流行的HTTP客户端库,以其简洁的API和强大的功能受到广泛青睐。然而,直接在项目中裸用Axios会导致代码冗余、维护困难等问题。本文将深入探讨如何在Vue项目中全面封装Axios,打造企业级的HTTP请求解决方案。

一、为什么需要封装Axios?

1.1 直接使用Axios的问题

  • 代码重复:每个请求都需要写完整的配置

  • 维护困难:基础配置分散在各个文件中

  • 缺乏统一错误处理

  • 难以实现请求拦截和响应拦截的统一管理

  • 类型安全缺失(TypeScript项目)

1.2 封装带来的优势

  • 统一配置和管理

  • 提高代码复用性

  • 增强错误处理能力

  • 便于实现请求拦截、身份验证等功能

  • 提升开发效率和代码质量

二、基础封装实现

2.1 项目结构规划

src/

├── api/

│ ├── index.ts # 导出所有API

│ ├── request.ts # Axios封装核心

│ ├── types/ # 类型定义

│ │ ├── request.ts

│ │ └── response.ts

│ ├── modules/ # 模块化API

│ │ ├── user.ts

│ │ └── product.ts

│ └── interceptors/ # 拦截器

│ ├── request.ts

│ └── response.ts

2.2 创建基础请求类

TypeScript 复制代码
// src/api/request.ts
import axios, {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
  InternalAxiosRequestConfig
} from 'axios';
import { RequestConfig, RequestInterceptors, CancelRequestSource } from './types';

class Request {
  // axios实例
  instance: AxiosInstance;
  // 拦截器对象
  interceptorsObj?: RequestInterceptors;
  // 存储取消请求的Map
  cancelRequestSource: CancelRequestSource;
  // 存储所有请求url
  requests: string[];

  constructor(config: RequestConfig) {
    this.instance = axios.create(config);
    this.interceptorsObj = config.interceptors;
    this.cancelRequestSource = {};
    this.requests = [];

    // 请求拦截器
    this.instance.interceptors.request.use(
      (config: InternalAxiosRequestConfig) => {
        // 全局请求拦截器
        console.log('全局请求拦截器');
        
        // 添加取消令牌
        const requestId = this.generateRequestId(config);
        config.cancelToken = new axios.CancelToken(cancel => {
          this.cancelRequestSource[requestId] = cancel;
        });
        this.requests.push(requestId);

        // 自定义请求拦截器
        if (this.interceptorsObj?.requestInterceptors) {
          config = this.interceptorsObj.requestInterceptors(config);
        }
        
        return config;
      },
      (error: any) => {
        return Promise.reject(error);
      }
    );

    // 使用实例拦截器
    this.instance.interceptors.request.use(
      this.interceptorsObj?.requestInterceptors,
      this.interceptorsObj?.requestInterceptorsCatch
    );

    // 响应拦截器
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        const requestId = this.generateRequestId(response.config);
        this.removeRequest(requestId);
        
        // 全局响应拦截器
        console.log('全局响应拦截器');
        
        // 自定义响应拦截器
        if (this.interceptorsObj?.responseInterceptors) {
          response = this.interceptorsObj.responseInterceptors(response);
        }
        
        return response.data;
      },
      (error: any) => {
        error.config && this.removeRequest(this.generateRequestId(error.config));
        
        // 全局错误处理
        if (this.interceptorsObj?.responseInterceptorsCatch) {
          return this.interceptorsObj.responseInterceptorsCatch(error);
        }
        
        // HTTP状态码错误处理
        if (error.response) {
          this.handleHttpError(error.response.status, error.response.data);
        }
        
        // 请求被取消
        if (axios.isCancel(error)) {
          console.log('请求已被取消:', error.message);
          return Promise.reject(new Error('请求已被取消'));
        }
        
        return Promise.reject(error);
      }
    );

    // 使用实例响应拦截器
    this.instance.interceptors.response.use(
      this.interceptorsObj?.responseInterceptors,
      this.interceptorsObj?.responseInterceptorsCatch
    );
  }

  /**
   * 生成请求唯一标识
   */
  private generateRequestId(config: AxiosRequestConfig): string {
    return `${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`;
  }

  /**
   * 移除已完成/取消的请求
   */
  private removeRequest(requestId: string): void {
    const requestIndex = this.requests.indexOf(requestId);
    if (requestIndex > -1) {
      this.requests.splice(requestIndex, 1);
    }
    delete this.cancelRequestSource[requestId];
  }

  /**
   * 取消所有请求
   */
  public cancelAllRequests(): void {
    Object.keys(this.cancelRequestSource).forEach(requestId => {
      this.cancelRequestSource[requestId]('取消所有未完成请求');
    });
    this.requests = [];
    this.cancelRequestSource = {};
  }

  /**
   * 取消指定请求
   */
  public cancelRequest(requestId: string): void {
    if (this.cancelRequestSource[requestId]) {
      this.cancelRequestSource[requestId](`取消请求: ${requestId}`);
      this.removeRequest(requestId);
    }
  }

  /**
   * 处理HTTP错误
   */
  private handleHttpError(status: number, data: any): void {
    const errorMap: Record<number, string> = {
      400: '请求错误',
      401: '未授权,请重新登录',
      403: '拒绝访问',
      404: '请求的资源不存在',
      408: '请求超时',
      500: '服务器内部错误',
      501: '服务未实现',
      502: '网关错误',
      503: '服务不可用',
      504: '网关超时',
      505: 'HTTP版本不受支持'
    };

    const message = errorMap[status] || `连接错误${status}`;
    console.error(`HTTP错误 ${status}: ${message}`, data);
    
    // 401错误跳转到登录页
    if (status === 401) {
      // router.push('/login');
      // 可以在这里清除用户信息
    }
  }

  /**
   * 通用请求方法
   */
  request<T = any>(config: RequestConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      // 单个请求的拦截器
      if (config.interceptors?.requestInterceptors) {
        config = config.interceptors.requestInterceptors(config as any);
      }

      this.instance
        .request<any, T>(config)
        .then(res => {
          // 单个响应的拦截器
          if (config.interceptors?.responseInterceptors) {
            res = config.interceptors.responseInterceptors(res);
          }
          resolve(res);
        })
        .catch(err => {
          reject(err);
        });
    });
  }

  /**
   * GET请求
   */
  get<T = any>(config: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' });
  }

  /**
   * POST请求
   */
  post<T = any>(config: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' });
  }

  /**
   * PUT请求
   */
  put<T = any>(config: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'PUT' });
  }

  /**
   * DELETE请求
   */
  delete<T = any>(config: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' });
  }

  /**
   * PATCH请求
   */
  patch<T = any>(config: RequestConfig): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH' });
  }
}

export default Request;

2.3 类型定义

TypeScript 复制代码
// src/api/types/request.ts
import {
  AxiosRequestConfig,
  AxiosResponse,
  InternalAxiosRequestConfig
} from 'axios';

export interface RequestInterceptors {
  // 请求拦截器
  requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
  requestInterceptorsCatch?: (error: any) => any;
  
  // 响应拦截器
  responseInterceptors?: (response: AxiosResponse) => AxiosResponse;
  responseInterceptorsCatch?: (error: any) => any;
}

export interface RequestConfig extends AxiosRequestConfig {
  interceptors?: RequestInterceptors;
  // 是否显示loading
  showLoading?: boolean;
  // 是否显示错误消息
  showError?: boolean;
  // 请求重试配置
  retryConfig?: {
    retry: number;
    delay: number;
  };
}

export interface CancelRequestSource {
  [index: string]: () => void;
}

// 分页请求参数
export interface PaginationParams {
  page: number;
  pageSize: number;
  [key: string]: any;
}

// 分页响应数据
export interface PaginationResponse<T = any> {
  list: T[];
  total: number;
  page: number;
  pageSize: number;
  totalPage: number;
}

// 基础响应结构
export interface BaseResponse<T = any> {
  code: number;
  data: T;
  message: string;
  success: boolean;
}

三、高级功能实现

3.1 请求重试机制

TypeScript 复制代码
// 在request方法中添加重试逻辑
private async requestWithRetry<T = any>(
  config: RequestConfig, 
  retryCount: number = 0
): Promise<T> {
  const maxRetry = config.retryConfig?.retry || 3;
  const delay = config.retryConfig?.delay || 1000;
  
  try {
    return await this.request<T>(config);
  } catch (error) {
    if (retryCount < maxRetry && this.shouldRetry(error)) {
      await this.sleep(delay);
      return this.requestWithRetry(config, retryCount + 1);
    }
    throw error;
  }
}

private shouldRetry(error: any): boolean {
  // 网络错误或5xx错误重试
  return !error.response || 
         error.response.status >= 500 || 
         error.code === 'ECONNABORTED';
}

private sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

3.2 请求节流与防抖

TypeScript 复制代码
// 请求缓存与防抖
class RequestCache {
  private cache: Map<string, { data: any; timestamp: number }> = new Map();
  private pendingRequests: Map<string, Promise<any>> = new Map();
  private readonly CACHE_DURATION = 5 * 60 * 1000; // 5分钟

  async request<T>(
    key: string,
    requestFn: () => Promise<T>,
    useCache: boolean = true
  ): Promise<T> {
    // 检查缓存
    if (useCache) {
      const cached = this.cache.get(key);
      if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
        return cached.data;
      }
    }

    // 检查是否有相同请求正在处理
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key)!;
    }

    // 执行请求
    const promise = requestFn().then(data => {
      if (useCache) {
        this.cache.set(key, { data, timestamp: Date.now() });
      }
      this.pendingRequests.delete(key);
      return data;
    }).catch(error => {
      this.pendingRequests.delete(key);
      throw error;
    });

    this.pendingRequests.set(key, promise);
    return promise;
  }

  clearCache(): void {
    this.cache.clear();
  }

  removeCache(key: string): void {
    this.cache.delete(key);
  }
}

3.3 文件上传与下载

TypeScript 复制代码
// 文件上传封装
export const uploadFile = (
  url: string,
  file: File,
  onProgress?: (progress: number) => void
): Promise<any> => {
  const formData = new FormData();
  formData.append('file', file);

  return request({
    url,
    method: 'POST',
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    onUploadProgress: (progressEvent) => {
      if (onProgress && progressEvent.total) {
        const percentCompleted = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        );
        onProgress(percentCompleted);
      }
    }
  });
};

// 文件下载封装
export const downloadFile = async (
  url: string,
  filename?: string
): Promise<void> => {
  const response = await request({
    url,
    method: 'GET',
    responseType: 'blob'
  });

  const blob = new Blob([response]);
  const downloadUrl = window.URL.createObjectURL(blob);
  const link = document.createElement('a');
  
  link.href = downloadUrl;
  link.download = filename || 'download';
  document.body.appendChild(link);
  link.click();
  
  document.body.removeChild(link);
  window.URL.revokeObjectURL(downloadUrl);
};

四、完整配置与使用

4.1 创建请求实例

TypeScript 复制代码
// src/api/index.ts
import Request from './request';
import type { RequestConfig } from './types';

// 环境配置
const env = process.env.NODE_ENV;
const baseURL = env === 'development' 
  ? 'http://localhost:3000/api'
  : 'https://api.example.com';

// 创建请求实例
const request = new Request({
  baseURL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json;charset=UTF-8'
  },
  interceptors: {
    // 请求拦截器
    requestInterceptors: (config) => {
      // 添加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;
    },
    
    // 响应拦截器
    responseInterceptors: (response) => {
      const { data } = response;
      
      // 处理业务错误
      if (data.code !== 200) {
        // 根据业务code进行错误处理
        handleBusinessError(data.code, data.message);
        return Promise.reject(data);
      }
      
      return response;
    },
    
    // 响应错误拦截器
    responseInterceptorsCatch: (error) => {
      // 统一错误处理
      showErrorMessage(error);
      return Promise.reject(error);
    }
  }
});

// 导出常用方法
export const http = {
  request: <T = any>(config: RequestConfig) => request.request<T>(config),
  get: <T = any>(url: string, params?: any, config?: RequestConfig) =>
    request.get<T>({ url, params, ...config }),
  post: <T = any>(url: string, data?: any, config?: RequestConfig) =>
    request.post<T>({ url, data, ...config }),
  put: <T = any>(url: string, data?: any, config?: RequestConfig) =>
    request.put<T>({ url, data, ...config }),
  delete: <T = any>(url: string, params?: any, config?: RequestConfig) =>
    request.delete<T>({ url, params, ...config }),
  upload: uploadFile,
  download: downloadFile
};

// 导出取消请求方法
export const cancelAllRequests = () => request.cancelAllRequests();
export const cancelRequest = (requestId: string) => request.cancelRequest(requestId);

4.2 模块化API管理

TypeScript 复制代码
// src/api/modules/user.ts
import { http } from '..';
import type { 
  BaseResponse, 
  PaginationParams, 
  PaginationResponse 
} from '../types';

// 用户相关接口
export const userApi = {
  // 登录
  login: (data: { username: string; password: string }) =>
    http.post<BaseResponse<{ token: string }>>('/user/login', data),
  
  // 获取用户信息
  getUserInfo: (userId: string) =>
    http.get<BaseResponse<UserInfo>>(`/user/${userId}`),
  
  // 分页获取用户列表
  getUserList: (params: PaginationParams & { keyword?: string }) =>
    http.get<BaseResponse<PaginationResponse<UserInfo>>>('/user/list', params),
  
  // 更新用户信息
  updateUser: (userId: string, data: Partial<UserInfo>) =>
    http.put<BaseResponse<void>>(`/user/${userId}`, data),
  
  // 删除用户
  deleteUser: (userId: string) =>
    http.delete<BaseResponse<void>>(`/user/${userId}`),
  
  // 上传头像
  uploadAvatar: (file: File) =>
    http.upload('/user/avatar', file)
};

// 用户信息类型
export interface UserInfo {
  id: string;
  username: string;
  email: string;
  avatar: string;
  createdAt: string;
  updatedAt: string;
}

4.3 Vue组件中使用

TypeScript 复制代码
<template>
  <div>
    <button @click="fetchUser">获取用户信息</button>
    <button @click="uploadFile">上传文件</button>
    <button @click="cancelRequest">取消请求</button>
  </div>
</template>

<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
import { userApi, cancelAllRequests } from '@/api';

const loading = ref(false);
const userData = ref<any>(null);

// 获取用户信息
const fetchUser = async () => {
  try {
    loading.value = true;
    const response = await userApi.getUserInfo('123');
    userData.value = response.data;
  } catch (error) {
    console.error('获取用户信息失败:', error);
  } finally {
    loading.value = false;
  }
};

// 上传文件
const uploadFile = async (event: Event) => {
  const file = (event.target as HTMLInputElement).files?.[0];
  if (!file) return;

  try {
    await userApi.uploadAvatar(file, (progress) => {
      console.log(`上传进度: ${progress}%`);
    });
  } catch (error) {
    console.error('上传失败:', error);
  }
};

// 组件卸载时取消所有请求
onUnmounted(() => {
  cancelAllRequests();
});
</script>

五、高级配置与优化

5.1 环境配置管理

TypeScript 复制代码
// src/config/env.ts
export interface EnvConfig {
  baseURL: string;
  timeout: number;
  retryCount: number;
  [key: string]: any;
}

const envConfigs: Record<string, EnvConfig> = {
  development: {
    baseURL: 'http://localhost:3000/api',
    timeout: 30000,
    retryCount: 3,
    enableMock: true
  },
  test: {
    baseURL: 'https://test-api.example.com',
    timeout: 15000,
    retryCount: 2,
    enableMock: false
  },
  production: {
    baseURL: 'https://api.example.com',
    timeout: 10000,
    retryCount: 1,
    enableMock: false
  }
};

export const getEnvConfig = (): EnvConfig => {
  const env = process.env.NODE_ENV || 'development';
  return envConfigs[env] || envConfigs.development;
};

5.2 Mock数据支持

TypeScript 复制代码
// src/api/mock/index.ts
import MockAdapter from 'axios-mock-adapter';
import request from '../request';

// 创建Mock适配器
export const mock = new MockAdapter(request.instance, { delayResponse: 500 });

// 用户相关Mock
mock.onPost('/user/login').reply(200, {
  code: 200,
  data: { token: 'mock-token-123456' },
  message: '登录成功',
  success: true
});

mock.onGet(/\/user\/\d+/).reply(config => {
  const userId = config.url?.split('/').pop();
  return [200, {
    code: 200,
    data: {
      id: userId,
      username: 'mockuser',
      email: 'mock@example.com',
      avatar: 'https://example.com/avatar.jpg'
    },
    message: '获取成功',
    success: true
  }];
});

// 只在开发环境启用Mock
if (process.env.NODE_ENV === 'development') {
  // 根据配置决定是否启用Mock
  const { enableMock } = getEnvConfig();
  if (enableMock) {
    console.log('Mock数据已启用');
  } else {
    mock.restore();
  }
}

5.3 性能监控与日志

TypeScript 复制代码
// 性能监控装饰器
function performanceMonitor(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = async function(...args: any[]) {
    const startTime = performance.now();
    const requestId = `request_${Date.now()}_${Math.random()}`;
    
    try {
      console.log(`[${requestId}] 开始请求: ${propertyKey}`);
      const result = await originalMethod.apply(this, args);
      const endTime = performance.now();
      
      console.log(`[${requestId}] 请求成功,耗时: ${(endTime - startTime).toFixed(2)}ms`);
      return result;
    } catch (error) {
      const endTime = performance.now();
      console.error(`[${requestId}] 请求失败,耗时: ${(endTime - startTime).toFixed(2)}ms`, error);
      throw error;
    }
  };
  
  return descriptor;
}

// 在API类中使用
class UserService {
  @performanceMonitor
  async getUserInfo(userId: string) {
    return userApi.getUserInfo(userId);
  }
}

六、测试策略

6.1 单元测试

TypeScript 复制代码
// tests/unit/request.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import Request from '@/api/request';
import axios from 'axios';

describe('Request Class', () => {
  let request: Request;
  
  beforeEach(() => {
    request = new Request({
      baseURL: 'http://test.com',
      timeout: 1000
    });
  });

  it('should create axios instance with correct config', () => {
    expect(request.instance.defaults.baseURL).toBe('http://test.com');
    expect(request.instance.defaults.timeout).toBe(1000);
  });

  it('should handle GET request successfully', async () => {
    const mockData = { id: 1, name: 'test' };
    vi.spyOn(request.instance, 'request').mockResolvedValue({ data: mockData });
    
    const result = await request.get({ url: '/test' });
    expect(result).toEqual(mockData);
  });

  it('should handle request errors', async () => {
    vi.spyOn(request.instance, 'request').mockRejectedValue({
      response: { status: 500, data: { message: 'Server Error' } }
    });
    
    await expect(request.get({ url: '/test' })).rejects.toThrow();
  });

  it('should cancel requests', () => {
    const requestId = 'test-request';
    const cancelToken = new axios.CancelToken(cancel => {
      request.cancelRequestSource[requestId] = cancel;
    });
    
    request.cancelRequest(requestId);
    expect(request.cancelRequestSource[requestId]).toBeUndefined();
  });
});

6.2 集成测试

TypeScript 复制代码
// tests/integration/userApi.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { userApi } from '@/api/modules/user';

const server = setupServer(
  rest.post('http://test.com/user/login', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        code: 200,
        data: { token: 'mock-token' },
        message: '登录成功',
        success: true
      })
    );
  })
);

describe('User API Integration', () => {
  beforeAll(() => server.listen());
  afterAll(() => server.close());
  afterEach(() => server.resetHandlers());

  it('should login successfully', async () => {
    const response = await userApi.login({
      username: 'testuser',
      password: 'password123'
    });
    
    expect(response.code).toBe(200);
    expect(response.data.token).toBe('mock-token');
    expect(response.success).toBe(true);
  });
});

七、最佳实践与注意事项

7.1 安全考虑

  1. Token管理:使用HttpOnly Cookie存储敏感token

  2. CSRF防护:添加CSRF Token到请求头

  3. 请求限流:防止API被恶意调用

  4. 参数验证:对请求参数进行严格验证

7.2 性能优化

  1. 请求合并:合并短时间内相同请求

  2. 缓存策略:合理使用缓存减少请求

  3. 懒加载:按需加载API模块

  4. 压缩传输:启用Gzip压缩

7.3 错误处理策略

  1. 分级处理:区分网络错误、业务错误、系统错误

  2. 友好提示:用户友好的错误消息

  3. 错误上报:错误日志收集和分析

  4. 自动恢复:网络恢复后自动重连

八、总结

本文详细介绍了Vue项目中Axios的全面封装方案,从基础封装到高级功能实现,涵盖了:

  1. 基础请求类的创建与配置

  2. 拦截器的灵活使用

  3. 类型安全的TypeScript支持

  4. 请求取消、重试等高级功能

  5. 模块化API管理

  6. Mock数据支持

  7. 性能监控与测试策略

通过这样的封装,我们可以实现:

  • 统一的请求管理和错误处理

  • 更好的代码组织和复用

  • 增强的开发体验和调试能力

  • 更高的应用稳定性和性能

封装的程度需要根据项目实际情况进行调整,避免过度设计。建议在项目初期就建立良好的请求封装规范,随着项目发展逐步完善和优化。大家还有什么更好的实践可以在评论区交流。

相关推荐
heyCHEEMS2 小时前
为什么放弃 v-if 选择 v-show?为什么组件越用越卡?
前端
百罹鸟2 小时前
【react 高频面试题—核心原理篇】:useEffect 的依赖项如果是数组或对象(引用类型),会有什么问题?如何解决?
前端·react.js·面试
hibear2 小时前
Smart Ticker - 支持任意字符的高性能文本差异动画滚动组件
前端·vue.js·react.js
脱氧核糖核酸2 小时前
2026了你还只会写点prompt?从AI提示词到可控自动化的演进之路
前端
HabaraAi2 小时前
记一次发现 DataTransfer 的 getData 的有趣问题
前端
a17798877122 小时前
print.js打印
前端·javascript·html
小林攻城狮2 小时前
前端实时语音转写:原生 MediaRecorder API 实践
前端·vue.js
Sport2 小时前
用全会,问全废:CSS高频面试题
前端·javascript·面试
Maxkim2 小时前
「✍️JS原子笔记 」零基础吃透 Proxy 数据响应式
前端·javascript·面试