封装一个优雅的axios
DailyUse 项目架构(Monorepo + Result Pattern),设计一个"优雅"的 Axios 封装的核心在于:彻底屏蔽 Axios 的实现细节,让业务层只感知 Result<T>。
#type / howto
#status / evergreen
#tech / dev
[!info] related notes
- Axios 错误模型
- Axios 实例
- Axios 拦截器
- Axios 封装模式
- Axios [!info] 相关笔记
- 上位概念: Axios
- 相关原子: Axios 实例, Axios 拦截器, Axios 错误模型, Axios 封装模式
封装一个优雅的axios
HTTP 客户端应该像一个”防腐层”,把所有的 HTTP 状态码、网络异常、超时都转换成统一的 Result.fail。
目标
封装 Axios 使业务层无需关心 HTTP 细节,所有请求返回 Result<T>,不抛异常。
前置条件
- Axios 1.x
- TypeScript 项目
- 后端统一返回格式
{ code: number, data: T, message: string }
步骤
1. 定义 Result 类型
// types/result.ts
export enum ResultCode {
SUCCESS = 200,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
INTERNAL_ERROR = 500,
TIMEOUT = 408,
}
export interface Result<T = any> {
code: ResultCode;
data: T;
message: string;
}
export function ok<T>(data: T, message = 'success'): Result<T> {
return { code: ResultCode.SUCCESS, data, message };
}
export function fail<T = never>(code: ResultCode, message: string): Result<T> {
return { code, data: undefined as T, message };
}
2. 创建 Axios 实例
// utils/request.ts
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
import { ResultCode, ok, fail, type Result } from '@/types/result';
const instance: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
headers: { 'Content-Type': 'application/json' },
});
3. 请求拦截器
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
4. 响应拦截器(核心:异常归一化)
instance.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response;
// 后端统一格式 { code, data, message }
if (data.code === ResultCode.SUCCESS) {
return ok(data.data, data.message);
}
// 业务错误
return fail(data.code, data.message || '请求失败');
},
(error) => {
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
return fail(ResultCode.TIMEOUT, '请求超时,请重试');
}
if (!error.response) {
return fail(ResultCode.INTERNAL_ERROR, '网络异常,请检查连接');
}
const { status } = error.response;
const codeMap: Record<number, ResultCode> = {
401: ResultCode.UNAUTHORIZED,
403: ResultCode.FORBIDDEN,
404: ResultCode.NOT_FOUND,
500: ResultCode.INTERNAL_ERROR,
};
return fail(codeMap[status] ?? ResultCode.INTERNAL_ERROR, error.response.data?.message ?? '服务器异常');
}
);
5. 封装类型安全的请求方法
function request<T>(config: AxiosRequestConfig): Promise<Result<T>> {
return instance.request(config) as Promise<Result<T>>;
}
export function get<T>(url: string, params?: Record<string, any>, config?: AxiosRequestConfig): Promise<Result<T>> {
return request<T>({ ...config, method: 'GET', url, params });
}
export function post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<Result<T>> {
return request<T>({ ...config, method: 'POST', url, data });
}
export function put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<Result<T>> {
return request<T>({ ...config, method: 'PUT', url, data });
}
export function del<T>(url: string, config?: AxiosRequestConfig): Promise<Result<T>> {
return request<T>({ ...config, method: 'DELETE', url });
}
6. 业务层使用
import { get, post } from '@/utils/request';
import { ResultCode } from '@/types/result';
async function fetchUser(id: number) {
const result = await get<User>(`/users/${id}`);
if (result.code === ResultCode.SUCCESS) {
console.log(result.data.name); // IDE 自动推断 User 类型
} else {
showToast(result.message);
}
}
验证
- 正常请求返回
Result<T>,data字段类型正确 - 断网时返回
ResultCode.INTERNAL_ERROR,不会抛出未捕获异常 - 后端返回非 200 code 时,
result.message携带后端错误信息 - 401 状态码可在此拦截器中统一触发跳转登录页