封装一个完整的HttpClient.ts

这是一个基于 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');
}

封装亮点

  1. 类型安全:使用泛型 <T>,调用接口时可以精确指定返回数据类型。
  2. 统一响应处理:拦截器自动提取 data,无需每个接口都写 response.data.data
  3. 错误分类:区分了 HTTP 错误(如 404)和业务错误(如后端返回的 code: 500),并封装了 HttpError
  4. 便利方法:直接暴露 get, post, put, delete, upload, download,代码更简洁。
  5. 进度条支持:上传时支持 onUploadProgress 回调。
相关推荐
打小就很皮...4 小时前
《在 React/Vue 项目中引入 Supademo 实现交互式新手指引》
前端·supademo·新手指引
C澒4 小时前
系统初始化成功率下降排查实践
前端·安全·运维开发
摘星编程5 小时前
React Native + OpenHarmony:自定义useFormik表单处理
javascript·react native·react.js
C澒5 小时前
面单打印服务的监控检查事项
前端·后端·安全·运维开发·交通物流
pas1365 小时前
39-mini-vue 实现解析 text 功能
前端·javascript·vue.js
qq_532453535 小时前
使用 GaussianSplats3D 在 Vue 3 中构建交互式 3D 高斯点云查看器
前端·vue.js·3d
Swift社区5 小时前
Flutter 路由系统,对比 RN / Web / iOS 有什么本质不同?
前端·flutter·ios
2601_949833396 小时前
flutter_for_openharmony口腔护理app实战+我的实现
开发语言·javascript·flutter
雾眠气泡水@6 小时前
前端:解决同一张图片由于页面大小不统一导致图片模糊
前端
开发者小天6 小时前
python中计算平均值
开发语言·前端·python