这是一个基于 Axios 封装的高质量 HttpClient。它包含了拦截器(自动加 Token、统一错误处理)、请求重试、文件上传/下载支持,并且是类型安全的。
后端接口返回的标准格式为:
javascript
{
"code": 200,
"msg": "success",
"data": { ... }
}
1. 准备工作
需要安装 axios:
javascript
npm install axios
2. 封装代码 (src/utils/http.ts)
javascript
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError, Method } from 'axios';
// 定义响应数据结构
interface ResponseData<T = any> {
code: number;
msg: string;
data: T;
}
// 定义请求配置扩展(可选,用于添加自定义字段)
interface RequestConfig extends AxiosRequestConfig {
noLoading?: boolean; // 是否不显示 loading
noToast?: boolean; // 是否不显示错误提示
}
// 自定义 HTTP 错误类
class HttpError extends Error {
public code: number;
public isShow?: boolean;
constructor(message: string, code: number, isShow?: boolean) {
super(message);
this.name = 'HttpError';
this.code = code;
this.isShow = isShow;
}
}
// 实例化
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_API || '/api',
timeout: 10000, // 10秒超时
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 1. 添加 Token (示例,实际从 localStorage 或 pinia 获取)
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 2. 请求日志 (开发环境)
if (import.meta.env.DEV) {
console.log(`[Request] ${config.method?.toUpperCase()} ${config.url}`, config.data || config.params);
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse<ResponseData>) => {
const res = response.data;
// 业务状态码判断 (假设 200 为成功,500 为服务器错误,401 为未授权)
if (res.code === 200) {
return res.data; // 直接返回 data,方便使用
} else {
// 错误处理
const errorMsg = res.msg || 'Error';
// 判断是否需要提示用户(部分接口不需要 toast 提示)
const isShow = response.config.isShow !== false;
return Promise.reject(new HttpError(errorMsg, res.code, isShow));
}
},
(error: AxiosError<ResponseData>) => {
// 处理 HTTP 错误 (401, 403, 500 等)
let message = 'Network Error';
let code = -1;
if (error.response) {
const { status, data } = error.response;
code = data?.code || status;
switch (status) {
case 400:
message = data?.msg || 'Bad Request';
break;
case 401:
message = 'Unauthorized, please login again';
// 401 跳转逻辑可放此处
break;
case 403:
message = 'Forbidden';
break;
case 404:
message = 'Not Found';
break;
case 500:
message = 'Internal Server Error';
break;
default:
message = data?.msg || `Error ${status}`;
}
} else if (error.message.includes('timeout')) {
message = 'Request Timeout';
}
return Promise.reject(new HttpError(message, code, true));
}
);
// 核心:通用请求方法
const request = <T = any>(config: RequestConfig): Promise<T> => {
return service.request(config);
};
// 扩展常用方法
export const http = {
get: <T = any>(url: string, params?: any, config?: RequestConfig) =>
request<T>({ ...config, method: 'GET' as Method, url, params }),
post: <T = any>(url: string, data?: any, config?: RequestConfig) =>
request<T>({ ...config, method: 'POST' as Method, url, data }),
put: <T = any>(url: string, data?: any, config?: RequestConfig) =>
request<T>({ ...config, method: 'PUT' as Method, url, data }),
delete: <T = any>(url: string, params?: any, config?: RequestConfig) =>
request<T>({ ...config, method: 'DELETE' as Method, url, params }),
patch: <T = any>(url: string, data?: any, config?: RequestConfig) =>
request<T>({ ...config, method: 'PATCH' as Method, url, data }),
// 文件上传
upload: <T = any>(url: string, file: File, onUploadProgress?: (progressEvent: any) => void, config?: RequestConfig) => {
const formData = new FormData();
formData.append('file', file);
return request<T>({ ...config, method: 'POST' as Method, url, data: formData, headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress });
},
// 文件下载
download: <T = any>(url: string, params?: any, filename?: string) => {
return service.get(url, { params, responseType: 'blob' }).then((res: AxiosResponse) => {
const blob = new Blob([res.data]);
const link = document.createElement('a');
const objectUrl = URL.createObjectURL(blob);
link.href = objectUrl;
link.download = filename || `download-${Date.now()}.xlsx`;
link.click();
URL.revokeObjectURL(objectUrl);
});
}
};
export default http;
3. 如何使用
页面或 API 模块中导入使用:
javascript
import http from '@/utils/http';
// 1. GET 请求
http.get('/user/info', { id: 1 });
// 2. POST 请求
http.post('/user/login', { username: 'admin', password: '123456' });
// 3. PUT 请求
http.put('/user/update', { id: 1, name: 'newName' });
// 4. DELETE 请求
http.delete('/user/delete', { id: 1 });
// 5. 文件上传 (带进度条)
const handleUpload = () => {
http.upload('/upload', file, (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(percentCompleted);
});
};
// 6. 文件下载
http.download('/export', { type: 'excel' }, '用户列表.xlsx');
// 7. 处理错误
try {
const res = await http.get('/user/info');
console.log(res);
} catch (error: any) {
console.error(error.message); // 拦截器里定义的错误信息
// if (error.code === 401) router.push('/login');
}
封装亮点
- 类型安全:使用泛型
<T>,调用接口时可以精确指定返回数据类型。 - 统一响应处理:拦截器自动提取
data,无需每个接口都写response.data.data。 - 错误分类:区分了 HTTP 错误(如 404)和业务错误(如后端返回的 code: 500),并封装了
HttpError。 - 便利方法:直接暴露
get,post,put,delete,upload,download,代码更简洁。 - 进度条支持:上传时支持
onUploadProgress回调。