引言
在现代前端开发中,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 安全考虑
-
Token管理:使用HttpOnly Cookie存储敏感token
-
CSRF防护:添加CSRF Token到请求头
-
请求限流:防止API被恶意调用
-
参数验证:对请求参数进行严格验证
7.2 性能优化
-
请求合并:合并短时间内相同请求
-
缓存策略:合理使用缓存减少请求
-
懒加载:按需加载API模块
-
压缩传输:启用Gzip压缩
7.3 错误处理策略
-
分级处理:区分网络错误、业务错误、系统错误
-
友好提示:用户友好的错误消息
-
错误上报:错误日志收集和分析
-
自动恢复:网络恢复后自动重连
八、总结
本文详细介绍了Vue项目中Axios的全面封装方案,从基础封装到高级功能实现,涵盖了:
-
基础请求类的创建与配置
-
拦截器的灵活使用
-
类型安全的TypeScript支持
-
请求取消、重试等高级功能
-
模块化API管理
-
Mock数据支持
-
性能监控与测试策略
通过这样的封装,我们可以实现:
-
统一的请求管理和错误处理
-
更好的代码组织和复用
-
增强的开发体验和调试能力
-
更高的应用稳定性和性能
封装的程度需要根据项目实际情况进行调整,避免过度设计。建议在项目初期就建立良好的请求封装规范,随着项目发展逐步完善和优化。大家还有什么更好的实践可以在评论区交流。