封装一个优雅的axios

DailyUse 项目架构(Monorepo + Result Pattern),设计一个"优雅"的 Axios 封装的核心在于:彻底屏蔽 Axios 的实现细节,让业务层只感知 Result<T>。

#type / howto #status / evergreen #tech / dev

[!info] related notes

封装一个优雅的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);
  }
}

验证

  1. 正常请求返回 Result<T>data 字段类型正确
  2. 断网时返回 ResultCode.INTERNAL_ERROR,不会抛出未捕获异常
  3. 后端返回非 200 code 时,result.message 携带后端错误信息
  4. 401 状态码可在此拦截器中统一触发跳转登录页
创建于 2026/2/9 更新于 2026/5/27