你的Axios还停留在请求拦截器添加header,响应拦截器处理状态码嘛

在前端开发中封装axios是提升代码复用性、统一错误处理逻辑和优化用户体验的重要实践,但如果只是简单的在请求拦截器中添加token,响应拦截器中处理状态码是不能满足真正的封装需求的

那么好的axios封装应该满足那些点,个人觉得应该需要的如下(大佬饶命)

自动附加认证信息(token、cookie)如果是cookie-session方案要注意Set-Cookie大小写问题
js 复制代码
   const setCookie: string = header['Set-Cookie'] || header['set-cookie']
请求与响应拦截,支持统一错误处理
js 复制代码
// 在常量文件中维护特殊状态码 根据实际业务
const SPECIAL_CODE = 5201314
const CODE_200 = 200
const CODE_401 = 401
const CODE_0 = 0
支持取消请求,避免竞态条件

主要处理竞态问题 比如搜索框场景 搜a ab a的搜索请求会被取消

防止重复请求

性能优化 通过构造唯一key以及PendingMap

支持请求自动重试,提高稳定性

多给几次机会防止网络波动影响用户体验

兼容 TypeScript,支持泛型推导

主要看当前项目使用的框架

同一时间只弹出一个错误提示

用户体验

完整代码
js 复制代码
import axios, {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
  AxiosError,
  CancelTokenSource
} from 'axios';
import { ElMessage } from 'element-plus';

// ------ 常量定义 ------ //
const CODE_0 = 0;           // 后端业务成功 code
const CODE_401 = 401;       // 业务层登录失效
const SPECIAL_CODE = 5201314; // 示例特殊业务码

// ------ 扩展 AxiosConfig 类型 ------ //
export interface CustomRequestConfig extends AxiosRequestConfig {
  retry?: number;          // 重试次数
  retryDelay?: number;     // 重试间隔(ms)
}

// ------ 创建 Axios 实例 ------ //
const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 6000,
  withCredentials: true,
});

// ------ 存储 pending 请求的映射(用于去重 & 取消) ------ //
const pendingMap = new Map<string, CancelTokenSource>();

// ------ 全局错误提示开关 ------ //
let isErrorVisible = false;
function showMessage(msg: string) {
  if (!isErrorVisible && msg) {
    isErrorVisible = true;
    ElMessage.error({
      message: msg,
      onClose: () => {
        isErrorVisible = false;
      }
    });
  }
}

// ------ 生成请求唯一 key ------ //
function getReqKey(config: AxiosRequestConfig): string {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}

// ------ 移除 pending & 取消 token ------ //
function removePending(config: AxiosRequestConfig) {
  const key = getReqKey(config);
  if (pendingMap.has(key)) {
    pendingMap.get(key)!.cancel('cancel duplicate request');
    pendingMap.delete(key);
  }
}

// ------ 请求拦截 ------ //
service.interceptors.request.use(
  (config: CustomRequestConfig) => {
    // 1. 自动注入 Token
    const token = localStorage.getItem('token');
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`;
    }

    // 2. 去重 & 取消前一个同 key 请求
    removePending(config);
    const source = axios.CancelToken.source();
    config.cancelToken = source.token;
    pendingMap.set(getReqKey(config), source);

    return config;
  },
  (error) => Promise.reject(error)
);

// ------ 响应拦截 ------ //
service.interceptors.response.use(
  async (response: AxiosResponse) => {
    // 收到响应后,先清理 pending
    removePending(response.config);

    // 处理 Set-Cookie 大小写兼容
    const setCookie = response.headers['set-cookie'] || response.headers['Set-Cookie'];
    if (setCookie) {
      console.log('Set-Cookie:', setCookie);
    }

    // HTTP 状态检查
    if (response.status === 200) {
      const res = response.data;
      // 业务 code 处理
      if (typeof res.code === 'number' && res.code !== CODE_0) {
        if (res.code === CODE_401) {
          showMessage('您未登录或登录已过期,请重新登录');
          // TODO: 跳转登录页
        } else if (res.code === SPECIAL_CODE) {
          showMessage(res.message || '出现特殊错误');
        } else {
          showMessage(res.message || '请求返回错误');
        }
        return Promise.reject(new Error(res.message));
      }
      return res.data;
    } else {
      showMessage(`网络错误:${response.status}`);
      return Promise.reject(new Error(`Status code: ${response.status}`));
    }
  },
  async (error: AxiosError) => {
    const config = error.config as CustomRequestConfig;
    // 1. 清理 pending
    if (config) removePending(config);

    // 2. 主动取消
    if (axios.isCancel(error)) {
      console.error('请求被取消:' + error.message);
      return Promise.reject(error);
    }

    // 3. 自动重试
    if (config && config.retry && config.retry > 0) {
      config.retry!--;
      await new Promise(res => setTimeout(res, config.retryDelay || 1000));
      return service(config);
    }

    // 4. 详细的状态码 & 网络错误处理
    let message = '';
    if (error.response) {
      switch (error.response.status) {
        case 302: message = '接口重定向了!'; break;
        case 400: message = '参数不正确!'; break;
        case 401: message = '您未登录,或者登录已经超时,请先登录!'; break;
        case 403: message = '您没有权限操作!'; break;
        case 404: message = `请求地址出错: ${error.response.config.url}`; break;
        case 408: message = '请求超时!'; break;
        case 409: message = '系统已存在相同数据!'; break;
        case 500: message = '服务器内部错误!'; break;
        case 501: message = '服务未实现!'; break;
        case 502: message = '网关错误!'; break;
        case 503: message = '服务不可用!'; break;
        case 504: message = '服务暂时无法访问,请稍后再试!'; break;
        case 505: message = 'HTTP版本不受支持!'; break;
        default:  message = '异常问题,请联系管理员!'; break;
      }
    }
    // 超时 & 离线提示
    if (error.message.includes('timeout')) message = '网络请求超时!';
    if (error.message.includes('Network')) {
      message = window.navigator.onLine ? '服务端异常!' : '您断网了!';
    }
    showMessage(message);

    return Promise.reject(error);
  }
);

// ------ 泛型 request 封装 ------ //
export function request<T = any>(config: CustomRequestConfig): Promise<T> {
  return service.request<any, T>(config);
}

// ------ RESTful 方法 ------ //
export function get<T = any>(
  url: string,
  params?: object,
  config?: Omit<CustomRequestConfig, 'url' | 'method' | 'params'>
): Promise<T> {
  return request<T>({ url, method: 'GET', params, ...config });
}

export function post<T = any>(
  url: string,
  data?: any,
  config?: Omit<CustomRequestConfig, 'url' | 'method' | 'data'>
): Promise<T> {
  return request<T>({ url, method: 'POST', data, ...config });
}

export function delete<T = any>(
  url: string,
  params?: object,
  config?: Omit<CustomRequestConfig, 'url' | 'method' | 'params'>
): Promise<T> {
  return request<T>({ url, method: 'DELETE', params, ...config });
}

欢迎大佬们给出实际业务中还需要注意的点以及坑,学习~

相关推荐
天天扭码5 分钟前
很全面的前端面试题——计算机网络篇(上)
前端·网络协议·面试
JSON_L8 分钟前
Vue 详情模块 3
前端·javascript·vue.js
哀木18 分钟前
关于 one drive 上传功能的前端接入(未完成)
前端
大熊学员1 小时前
HTML与JavaScript的羁绊
前端·css·html
Mike_Wuzy1 小时前
【前端】CSS基础知识及基本应用
前端·css
啃火龙果的兔子1 小时前
WebView 中控制光标
前端
流星先生!1 小时前
前端小数点处理
开发语言·前端·javascript
不是二师兄的八戒2 小时前
PDF转图片工具技术文档(命令行版本)
前端·python·pdf
拾光拾趣录2 小时前
🔥9道题穿透JS底层:堆栈/异步/执行栈连环问,第5题99%人翻车?📉
前端·面试
雪芽蓝域zzs2 小时前
uniapp 数组的用法
前端·javascript·uni-app