2026 版 Vue3+Axios 封装实战:从基础请求到企业级拦截、缓存与重试

96 阅读7分钟

在 Vue3 项目开发中,Axios 作为最主流的 HTTP 请求库,直接裸用往往会导致代码冗余、异常处理混乱、请求逻辑难以维护。尤其在中大型项目中,一套健壮的 Axios 封装方案,能大幅提升开发效率、降低线上问题发生率。本文结合 2026 年前端工程化最佳实践,从基础封装到企业级特性(拦截器、请求缓存、失败重试、取消重复请求),手把手教你封装 Vue3 专属的 Axios 请求库。

一、封装前的准备:明确核心需求

一个合格的 Axios 封装,需要解决以下核心问题:

  • 统一请求基准地址、超时时间、请求头配置;
  • 全局请求 / 响应拦截(Token 携带、状态码统一处理、错误提示);
  • 取消重复请求(避免短时间内多次触发同一接口);
  • 请求缓存(减轻服务端压力,提升页面响应速度);
  • 失败自动重试(网络抖动场景下提高请求成功率);
  • 类型安全(结合 TypeScript 定义请求 / 响应类型);
  • 支持 Vue3 组合式 API,可灵活注入到组件 / Pinia 中。

二、基础封装:搭建核心请求实例

1. 环境准备与依赖安装

确保项目已安装 Axios 和 Vue3,推荐结合 TypeScript 开发:

# 安装依赖
npm install axios --save
npm install @types/axios --save-dev # TS类型声明

2. 目录结构设计

推荐在src/utils/request目录下组织封装代码,结构如下:

src/
├── utils/
│   ├── request/
│   │   ├── index.ts          # 核心封装入口
│   │   ├── interceptors.ts   # 拦截器封装
│   │   ├── cancelRequest.ts  # 取消重复请求
│   │   ├── requestCache.ts   # 请求缓存
│   │   └── types.ts          # 类型定义
├── env.d.ts                  # 环境变量类型
└── stores/                   # Pinia(可选,存储Token)

3. 核心实例封装(index.ts)

先搭建基础的 Axios 实例,配置全局默认参数:

// src/utils/request/types.ts
import type { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';

// 自定义请求配置(扩展Axios原生配置)
export interface RequestConfig extends AxiosRequestConfig {
  // 是否取消重复请求
  cancelRepeat?: boolean;
  // 是否开启缓存
  useCache?: boolean;
  // 缓存过期时间(秒)
  cacheTime?: number;
  // 失败重试次数
  retryCount?: number;
  // 重试间隔(毫秒)
  retryDelay?: number;
  // 是否隐藏错误提示
  hideErrorTip?: boolean;
}

// 通用响应格式(需和后端约定)
export interface ApiResponse<T = any> {
  code: number;
  msg: string;
  data: T;
  success: boolean;
}

// 缓存数据类型
export interface CacheData {
  data: AxiosResponse;
  expireTime: number;
}

// src/utils/request/index.ts
import axios from 'axios';
import { setupInterceptors } from './interceptors';
import { cancelRepeatRequest, removePendingRequest } from './cancelRequest';
import { getCache, setCache, clearCache } from './requestCache';
import type { RequestConfig, ApiResponse, CacheData } from './types';

// 创建Axios实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取基准地址
  timeout: 10000, // 默认超时时间
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
});

// 安装拦截器
setupInterceptors(service);

// 核心请求方法封装
const request = <T = any>(config: RequestConfig): Promise<T> => {
  const { 
    cancelRepeat = true, 
    useCache = false, 
    cacheTime = 60, 
    retryCount = 3, 
    retryDelay = 1000,
    ...axiosConfig 
  } = config;

  // 1. 处理请求缓存
  if (useCache) {
    const cacheKey = JSON.stringify({ url: axiosConfig.url, params: axiosConfig.params, data: axiosConfig.data });
    const cacheData = getCache<CacheData>(cacheKey);
    // 缓存未过期,直接返回缓存数据
    if (cacheData && Date.now() < cacheData.expireTime) {
      return Promise.resolve(cacheData.data.data as T);
    }
  }

  // 2. 取消重复请求
  if (cancelRepeat) {
    removePendingRequest(config); // 移除已存在的重复请求
    cancelRepeatRequest(config);  // 添加当前请求到待取消列表
  }

  // 3. 发送请求(支持重试)
  const sendRequest = (count: number): Promise<T> => {
    return new Promise((resolve, reject) => {
      service.request<ApiResponse<T>>(axiosConfig)
        .then((response) => {
          // 缓存请求结果
          if (useCache) {
            const cacheKey = JSON.stringify({ url: axiosConfig.url, params: axiosConfig.params, data: axiosConfig.data });
            setCache(cacheKey, {
              data: response,
              expireTime: Date.now() + cacheTime * 1000
            });
          }
          // 移除取消请求标记
          if (cancelRepeat) {
            removePendingRequest(config);
          }
          resolve(response.data.data);
        })
        .catch((error: AxiosError) => {
          // 移除取消请求标记
          if (cancelRepeat) {
            removePendingRequest(config);
          }
          // 失败重试逻辑
          if (count > 0 && error.code !== 'ECONNABORTED' && !error.response) {
            setTimeout(() => {
              sendRequest(count - 1).then(resolve).catch(reject);
            }, retryDelay);
            return;
          }
          reject(error);
        });
    });
  };

  return sendRequest(retryCount);
};

// 封装常用请求方法
export const get = <T = any>(url: string, config?: RequestConfig): Promise<T> => {
  return request<T>({ ...config, method: 'GET', url });
};

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

export const put = <T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> => {
  return request<T>({ ...config, method: 'PUT', url, data });
};

export const del = <T = any>(url: string, config?: RequestConfig): Promise<T> => {
  return request<T>({ ...config, method: 'DELETE', url });
};

// 导出实例(便于特殊场景直接使用)
export default service;

三、核心特性封装:拦截器、取消请求、缓存、重试

1. 拦截器封装(interceptors.ts)

拦截器是 Axios 封装的核心,负责统一处理请求头、响应状态、错误提示:

// src/utils/request/interceptors.ts
import { ElMessage } from 'element-plus'; // 假设使用Element Plus做提示
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { useUserStore } from '@/stores/user'; // Pinia存储Token
import type { RequestConfig, ApiResponse } from './types';

// 请求拦截器
const requestInterceptor = (config: RequestConfig) => {
  const userStore = useUserStore();
  // 携带Token(根据项目实际存储方式调整)
  if (userStore.token) {
    config.headers.Authorization = `Bearer ${userStore.token}`;
  }
  // 自定义请求头扩展
  config.headers['X-Request-From'] = 'Vue3-Admin';
  return config;
};

// 请求错误拦截
const requestErrorInterceptor = (error: AxiosError) => {
  ElMessage.error('请求发送失败,请检查网络');
  return Promise.reject(error);
};

// 响应拦截器
const responseInterceptor = (response: AxiosResponse<ApiResponse>) => {
  const { data } = response;
  // 后端约定的成功码(示例:200为成功)
  if (data.code !== 200) {
    // 业务错误提示(非200且未隐藏提示)
    if (!response.config.hideErrorTip) {
      ElMessage.error(data.msg || '请求失败');
    }
    // 特殊状态码处理(如401未登录、403权限不足)
    switch (data.code) {
      case 401:
        const userStore = useUserStore();
        userStore.clearToken();
        window.location.href = '/login'; // 跳转到登录页
        break;
      case 403:
        ElMessage.warning('暂无权限操作');
        break;
    }
    return Promise.reject(data);
  }
  return response;
};

// 响应错误拦截
const responseErrorInterceptor = (error: AxiosError) => {
  const config = error.config as RequestConfig;
  // 隐藏提示则不弹出
  if (config?.hideErrorTip) {
    return Promise.reject(error);
  }
  // 网络错误/超时
  if (!error.response) {
    ElMessage.error('网络异常,请检查网络连接');
  } else {
    // HTTP状态码错误
    const status = error.response.status;
    switch (status) {
      case 404:
        ElMessage.error('请求接口不存在');
        break;
      case 500:
        ElMessage.error('服务器内部错误,请稍后重试');
        break;
      default:
        ElMessage.error(`请求失败,状态码:${status}`);
    }
  }
  return Promise.reject(error);
};

// 安装拦截器
export const setupInterceptors = (instance: AxiosInstance) => {
  // 请求拦截
  instance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
  // 响应拦截
  instance.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
};

2. 取消重复请求(cancelRequest.ts)

避免短时间内多次触发同一接口,导致服务端压力增大或数据错乱:

// src/utils/request/cancelRequest.ts
import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
import type { RequestConfig } from './types';

// 存储待取消的请求
const pendingRequests = new Map<string, AbortController>();

// 生成请求唯一标识
const generateRequestKey = (config: RequestConfig): string => {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('-');
};

// 取消重复请求
export const cancelRepeatRequest = (config: RequestConfig) => {
  const requestKey = generateRequestKey(config);
  // 如果已有相同请求,先取消
  if (pendingRequests.has(requestKey)) {
    const controller = pendingRequests.get(requestKey);
    controller?.abort();
    pendingRequests.delete(requestKey);
  }
  // 创建新的AbortController
  const controller = new AbortController();
  config.signal = controller.signal;
  pendingRequests.set(requestKey, controller);
};

// 移除待取消请求
export const removePendingRequest = (config: RequestConfig) => {
  const requestKey = generateRequestKey(config);
  pendingRequests.delete(requestKey);
};

// 清空所有待取消请求(如路由切换时)
export const clearAllPendingRequests = () => {
  pendingRequests.forEach((controller) => {
    controller.abort();
  });
  pendingRequests.clear();
};

3. 请求缓存(requestCache.ts)

对 GET 请求等幂接口做缓存,减少重复请求:

// src/utils/request/requestCache.ts
import type { CacheData } from './types';

// 缓存容器(可替换为localStorage实现持久化)
const cacheMap = new Map<string, CacheData>();

// 获取缓存
export const getCache = <T = CacheData>(key: string): T | null => {
  if (cacheMap.has(key)) {
    return cacheMap.get(key) as T;
  }
  return null;
};

// 设置缓存
export const setCache = (key: string, data: CacheData) => {
  cacheMap.set(key, data);
  // 自动清理过期缓存
  setTimeout(() => {
    cacheMap.delete(key);
  }, data.expireTime - Date.now());
};

// 清除指定缓存
export const clearCache = (key: string) => {
  cacheMap.delete(key);
};

// 清空所有缓存
export const clearAllCache = () => {
  cacheMap.clear();
};

四、Vue3 中实战使用

1. 接口统一管理

推荐在src/api目录下按模块管理接口,保证代码可维护性:

// src/api/user.ts
import { get, post } from '@/utils/request';

// 用户登录
export const login = (data: { username: string; password: string }) => {
  return post<{ token: string; userId: number }>('/user/login', data, {
    cancelRepeat: true,
    hideErrorTip: false
  });
};

// 获取用户信息(开启缓存,缓存5分钟)
export const getUserInfo = (userId: number) => {
  return get<{ name: string; avatar: string; roles: string[] }>(`/user/info/${userId}`, {
    useCache: true,
    cacheTime: 300
  });
};

2. 组件中使用(组合式 API)

<template>
  <div class="user-page">
    <el-button @click="getUserInfo">获取用户信息</el-button>
    <div v-if="userInfo">用户名:{{ userInfo.name }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { getUserInfo } from '@/api/user';
import { ElMessage } from 'element-plus';

const userInfo = ref<any>(null);

// 获取用户信息
const getUserInfo = async () => {
  try {
    const res = await getUserInfo(1001);
    userInfo.value = res;
  } catch (error) {
    ElMessage.error('获取用户信息失败');
    console.error(error);
  }
};
</script>

3. 路由切换时清空请求

在路由守卫中清空待取消的请求和缓存,避免内存泄漏:

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { clearAllPendingRequests } from '@/utils/request/cancelRequest';
import { clearAllCache } from '@/utils/request/requestCache';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [/* 路由配置 */]
});

// 路由切换时清空请求和缓存
router.beforeEach((to, from, next) => {
  clearAllPendingRequests();
  clearAllCache();
  next();
});

export default router;

五、企业级优化建议

  1. 环境隔离:通过 Vite 环境变量区分开发 / 测试 / 生产环境的接口地址,避免手动修改;
  2. 请求日志:对接前端监控平台(如 Sentry),记录失败请求的详细信息(URL、参数、状态码),便于线上问题排查;
  3. 限流控制:对高频接口(如搜索)添加请求限流,避免短时间内触发过多请求;
  4. TypeScript 强化:为所有接口定义严格的请求 / 响应类型,避免 any 类型滥用;
  5. Mock 数据集成:结合 Vite-plugin-mock 实现本地 Mock,脱离后端联调也能开发;
  6. 性能监控:统计接口响应时间,对慢接口(如响应 > 2s)做告警或优化。

六、总结

一套完善的 Axios 封装,是 Vue3 项目工程化的重要组成部分。本文从基础实例搭建到企业级特性封装,覆盖了请求拦截、取消重复请求、缓存、重试等核心场景,既保证了代码的复用性和可维护性,又能解决实际开发中的各类痛点。

在 2026 年的前端开发中,Axios 封装不再只是 “能发请求就行”,而是要结合工程化、类型安全、用户体验等维度综合考量。希望这套封装方案能帮你在项目中少走弯路,打造更健壮的请求层体系。

最后,封装方案需根据团队实际业务调整(如 UI 组件库、状态管理库的差异),核心思路是 “统一配置、统一处理、灵活扩展”,让请求逻辑成为项目的 “基础设施”,而非重复造轮子的负担。