Skip to content

Axios 网络请求封装

文档基础信息

  • 技术栈:Vue3 + TypeScript + Pinia + Axios + Arco Design Vue
  • 封装核心:基于原生 Axios 高阶封装,深度整合前端业务通用能力
  • 适用场景:中后台管理系统/各类 Vue3 项目的标准化网络请求层开发
  • 核心特点:强类型约束、功能全覆盖、开箱即用、无侵入式扩展、贴合企业级开发规范
  • 文档说明:本文档包含「封装说明、配置说明、完整使用示例、高级功能、最佳实践」

一、封装概述

1.1 封装介绍

本封装是对 axios 的企业级完整封装,基于 Vue3 + TS 生态,在原生 Axios 能力之上,整合了前端开发中所有高频必备的请求层能力,将请求相关的通用逻辑(Token 注入、请求拦截、响应处理、错误提示、Loading 状态、重复请求拦截、请求取消、缓存策略、Token 无感刷新)全部封装到独立类中,业务侧仅需调用简洁的 API 即可完成请求调用,无需重复开发通用逻辑。

封装遵循「高内聚、低耦合」原则:内部封装复杂逻辑,对外暴露极简的调用方式;同时保留 Axios 原生配置的完全透传,支持业务侧按需自定义配置,兼顾「标准化」与「灵活性」。

1.2 核心功能特性 ✅ 全量支持

✅ TypeScript 强类型全覆盖,无 any 松散类型,所有请求/响应/配置均有精准类型约束,开发阶段校验错误

✅ 统一请求/响应拦截器,全局注入 Token 认证头,统一处理后端返回格式

✅ 基于「请求计数」的智能全局 Loading,多请求合并 Loading 无闪烁,请求完成自动关闭

✅ 重复请求自动拦截,防止用户高频点击导致的接口重复调用,支持开关配置

✅ 基于 Axios 官方推荐的 AbortController 实现请求取消(替代废弃的 CancelToken),支持单/全量取消请求

✅ 可配置的请求缓存策略,支持缓存有效期、强制刷新缓存,自动清理过期缓存,减少无效请求

Token 过期无感刷新,结合请求队列,刷新 Token 后自动重试排队请求,用户无感知,无需手动刷新页面

✅ 统一的异常处理体系,区分 HTTP 错误、业务错误、网络错误、重复请求、请求取消等场景,统一提示文案

✅ 完整的 RESTful 风格请求方法封装:GET/POST/PUT/DELETE + 专属文件上传方法 upload

✅ 支持 Pinia 用户状态持久化,Token/用户信息自动缓存到 localStorage,页面刷新不丢失

✅ 环境变量动态配置请求根地址,无缝适配开发/测试/生产多环境切换

✅ 内存友好设计,自动清理过期缓存、无用请求控制器,无内存泄漏风险

二、核心封装类:HttpRequest 详细说明

2.1 类的核心设计

HttpRequest 是本次封装的核心类,采用类封装+单例模式设计:

  1. 内部创建独立的 Axios 实例,避免全局污染,支持创建多个实例适配不同接口域名
  2. 所有通用逻辑(拦截器、Loading、缓存、取消请求)均封装为私有方法,对外不可见
  3. 对外暴露简洁的公共请求方法(get/post/put/delete/upload),业务侧直接调用
  4. 所有配置项均有默认值,业务侧按需传入即可,无需关心内部实现

2.2 核心自定义配置项 IRequestConfig

封装扩展了 Axios 原生配置,新增了前端业务必备的自定义配置项,所有请求方法均可传入该配置,用于控制当前请求的行为,所有配置项均为可选,未传则使用默认值。

配置项完整定义

typescript
interface IRequestConfig {
  showLoading: boolean; // 是否显示全局加载中动画,默认 true
  showToast: boolean; // 是否显示错误提示弹窗,默认 true
  cancelable: boolean; // 当前请求是否可被取消,默认 true
  preventDuplicate: boolean; // 是否拦截重复请求,默认 true
  requestId: string; // 请求唯一标识,自动生成,无需手动配置
  cache: number; // 缓存有效期(毫秒),0/负数=不缓存,默认 0
  forceRefresh: boolean; // 是否强制刷新缓存,跳过缓存校验,默认 false
}

配置项说明

配置项类型默认值核心作用使用场景
showLoadingbooleantrue控制当前请求是否触发全局 Loading列表查询/表单提交等需要加载状态的场景,无需加载则设为 false
showToastbooleantrue控制请求失败时是否自动弹出错误提示静默请求(如埋点、状态同步)设为 false,避免弹窗干扰;需要自定义提示也设为 false
cancelablebooleantrue开启则当前请求可被取消,关闭则不可取消页面切换时需要取消的请求保留 true,核心提交请求可设为 false
preventDuplicatebooleantrue开启则拦截相同请求的重复调用按钮点击触发的请求保留 true,避免重复提交;实时查询类请求可设为 false
cachenumber0缓存有效期,单位「毫秒」静态数据(字典/配置)、低频更新数据(用户信息)设为 5*60*1000(5 分钟)等
forceRefreshbooleanfalse强制刷新缓存,无视缓存有效期手动触发刷新数据时设为 true,保证获取最新数据

2.3 类内部核心默认配置

以下配置在类内部定义,可直接修改数值适配项目需求,无需改动核心逻辑:

typescript
private requestTimeout: number = 50000; // 普通请求超时时间:50秒
private cancelMaxCount: number = 10;    // 最多缓存10个可取消请求控制器,避免内存溢出
private cacheCleanupInterval = 5*60*1000; // 缓存自动清理间隔:5分钟
private defaultRequestConfig: IRequestConfig = { ... }; // 上述自定义配置的默认值

三、完整使用指南

3.1 导入方式(两种,按需选择)

封装默认导出全局单例实例,项目中 99% 的场景使用该实例即可;同时导出类本身,支持创建多实例适配不同域名。

typescript
// ✅ 推荐方式:全局导入默认实例,项目中所有请求统一使用该实例
import request from "@/utils/HttpRequest";

// ✅ 按需方式:导入类,创建自定义配置的新实例(适配多域名/不同超时配置)
import { HttpRequest } from "@/utils/HttpRequest";
const customRequest = new HttpRequest({ timeout: 60000 });

3.2 基础请求方法(RESTful 完整封装)

所有请求方法均为 异步方法,支持 async/await 语法;同时是 泛型方法,传入泛型即可获得响应数据的精准类型推导,完美适配 TS 开发。

✨ 核心特性:所有请求的返回值,直接是后端响应的 data 字段,无需业务侧手动 res.data 解析,封装类已自动解包!

✅ 1. GET 请求 - request.get<T>(url, params?, config?)

适用于查询数据、列表分页、无修改的请求,参数会自动拼接到 URL 上。

  • 参数 1:url 接口地址(必填,无需拼接 baseURL)
  • 参数 2:params 请求参数(可选,Params 类型)
  • 参数 3:config 自定义配置(可选,IRequestConfig + Axios 原生配置)
  • 返回值:Promise<T> T 为后端返回的 data 数据类型

使用示例

typescript
// ① 基础GET请求:无参数
const res = await request.get("/system/dict/list");

// ② 带参数GET请求:查询指定字典
const params = { dictType: "user_status" };
const res = await request.get("/system/dict/get", params);

// ③ 带泛型+自定义配置:分页查询用户列表,关闭Loading,开启缓存5分钟
import type { UserItem } from "@/models/user";
import type { BasePaginatorResponse } from "@/models/base";

const params = { page: 1, size: 10, keyword: "admin" };
const res = await request.get<BasePaginatorResponse<UserItem>>(
  "/system/user/page",
  params,
  { showLoading: false, cache: 5 * 60 * 1000 }
);
// res 直接是分页数据,包含 list + paginator
console.log(res.list, res.paginator.total_record);

✅ 2. POST 请求 - request.post<T>(url, data?, config?)

适用于提交数据、新增资源、表单提交,参数作为请求体传递,封装默认请求方法为 POST

  • 参数与 GET 一致,区别是第二个参数为data(请求体)
  • 所有特性与 GET 一致,支持泛型、自定义配置

使用示例

typescript
// ① 基础POST请求:登录提交
const loginData = { username: "admin", password: "123456" };
await request.post("/auth/login", loginData);

// ② 带泛型+自定义配置:新增用户,关闭重复请求拦截,关闭错误提示
import type { UserInfo } from "@/models/user";

const userData = { name: "测试用户", phone: "13800138000" };
const res = await request.post<UserInfo>("/system/user/add", userData, {
  preventDuplicate: false,
  showToast: false,
});

✅ 3. PUT 请求 - request.put<T>(url, data?, config?)

适用于修改/更新资源,RESTful 规范中 PUT 代表全量更新,用法与 POST 完全一致。

typescript
// 修改用户信息
const userData = { id: 1, name: "修改后的名称", status: 1 };
await request.put("/system/user/update", userData);

✅ 4. DELETE 请求 - request.delete<T>(url, data?, config?)

适用于删除资源,支持传参(如批量删除的 id 列表),用法与 POST 一致。

typescript
// 单条删除
await request.delete("/system/user/remove", { id: 1 });

// 批量删除
await request.delete("/system/user/batchRemove", { ids: [1, 2, 3] });

3.3 专属文件上传方法 - request.upload<T>(url, formData, config?)

封装了文件上传的所有通用配置,开箱即用,无需手动配置请求头,是项目中上传文件的唯一推荐方式。

核心特性

  1. 自动设置请求头:Content-Type: multipart/form-data,适配文件上传格式
  2. 超时时间默认设置为 2 分钟(120000ms),满足大文件上传需求
  3. 支持传入自定义配置,覆盖默认配置
  4. 支持 FormData 多字段传递(文件+业务参数)

使用示例

typescript
// 单文件上传
const uploadFile = async (file: File) => {
  const formData = new FormData();
  formData.append("file", file); // 文件字段
  formData.append("bizType", "avatar"); // 业务参数
  const res = await request.upload("/system/file/upload", formData);
  return res.fileUrl;
};

// 多文件上传 + 自定义配置:关闭Loading,开启错误提示
const uploadMultiFile = async (files: File[]) => {
  const formData = new FormData();
  files.forEach((file) => formData.append("files", file));
  const res = await request.upload("/system/file/batchUpload", formData, {
    showLoading: false,
  });
  return res.fileUrls;
};

3.4 取消所有未完成请求 - request.cancelAbortControllersAll()

封装提供了全局取消请求的方法,用于取消当前所有处于「待完成」状态的可取消请求,是内存优化的必用方法

核心使用场景

  1. 页面切换/路由跳转时,取消当前页面的所有未完成请求,避免接口返回后操作已销毁的组件
  2. 退出登录时,取消所有待完成的请求,防止敏感数据泄露
  3. 搜索框防抖时,取消上一次的搜索请求,只保留最新的请求

最佳实践:在路由守卫中使用

typescript
// router/index.ts
import { createRouter } from 'vue-router';
import request from '@/utils/HttpRequest';

const router = createRouter({ ... });

// 全局路由守卫:离开当前页时取消所有请求
router.beforeEach((to, from) => {
  if (from.path !== to.path) {
    request.cancelAbortControllersAll();
  }
});

export default router;

3.5 自定义配置组合使用(高频实用场景)

业务开发中最常用的就是「组合自定义配置」,满足各类个性化需求,以下是高频组合示例,直接复制使用即可:

typescript
// ✅ 场景1:静态数据查询,开启缓存10分钟,关闭Loading,关闭错误提示
await request.get("/system/config/getAll", null, {
  showLoading: false,
  showToast: false,
  cache: 10 * 60 * 1000,
});

// ✅ 场景2:表单提交,关闭重复请求拦截,关闭缓存,强制提交
await request.post("/system/form/submit", formData, {
  preventDuplicate: false,
  cache: 0,
});

// ✅ 场景3:手动刷新数据,开启强制刷新缓存,无视缓存有效期
await request.get("/system/data/refresh", params, {
  forceRefresh: true,
});

// ✅ 场景4:静默请求(埋点),关闭所有提示和Loading,关闭重复请求拦截
await request.post("/system/log/record", logData, {
  showLoading: false,
  showToast: false,
  preventDuplicate: false,
});

四、高级功能详解

4.1 Token 自动注入 + 过期无感刷新

这是本次封装的核心能力之一,完美解决了「Token 过期后用户操作无感知」的问题,也是中后台系统的必备功能,所有逻辑均已封装完成,业务侧无需写任何代码,开箱即用。

完整执行流程

  1. Token 自动注入:请求拦截器中自动从 Pinia 的 useUserStore 读取 Token,注入到请求头 Authorization 中,业务侧无需手动配置
  2. 过期判断:响应拦截器检测到后端返回的 codeResponseCode.AUTH_EXPIRED = '0200002' 时,判定为 Token 过期
  3. 防并发刷新:通过 isRefreshingToken 标记是否正在刷新 Token,避免多次调用刷新接口
  4. 请求队列缓存:Token 过期时,将所有正在发起的请求存入 refreshQueue 队列,保持请求的 Promise 处于 pending 状态
  5. 刷新处理:调用 Pinia 中的 refreshToken() 方法刷新 Token,刷新成功后:
    • 更新 Pinia 中的 Token
    • 自动遍历队列,重新执行所有排队的请求
    • 请求成功后,将结果返回给业务侧,用户无感知
  6. 刷新失败:若刷新 Token 失败(如 refreshToken 也过期),则清空队列,调用 logout() 方法退出登录,重置用户状态

关键依赖

该功能依赖 Pinia 的 useUserStore 中必须实现的 refreshToken() 方法

4.2 重复请求拦截机制

解决的问题

用户短时间内多次点击按钮发起相同请求,导致后端重复处理、前端接收重复数据、页面状态异常的问题。

实现原理

  1. 每个请求通过 generateRequestId 生成唯一标识,生成规则:请求方法(大写):接口地址:排序后的参数字符串
  2. 排序参数的目的:保证 {a:1,b:2}{b:2,a:1} 是同一个请求,避免参数顺序不同导致的重复请求
  3. 维护 pendingRequests Set 集合,存储正在处理中的请求 ID
  4. 请求发起前,检查 ID 是否存在于集合中,存在则抛出「正在处理中,请勿重复操作」异常,拦截请求
  5. 请求完成(成功/失败)后,自动从集合中移除该请求 ID

4.3 智能缓存策略

解决的问题

对高频调用、数据更新频率低的接口(如字典数据、系统配置、用户基础信息)进行缓存,减少接口请求次数,提升页面加载速度,降低服务器压力。

核心规则(完全自动,无需业务侧干预)

  1. 只有当 cache > 0 时,才会开启缓存
  2. 请求发起前,先检查缓存是否存在且未过期,存在则直接返回缓存数据,不发起真实请求
  3. 请求成功后,自动将响应数据存入缓存,有效期为配置的 cache 时长
  4. 缓存过期后,自动删除缓存并重新发起请求
  5. 开启 forceRefresh: true 时,跳过缓存校验,强制重新请求并更新缓存
  6. 封装会启动 5 分钟定时器,自动清理所有过期缓存,避免内存溢出

4.4 统一错误处理体系

封装对所有可能的异常场景进行了统一捕获和分类处理,返回标准化的错误提示,所有错误均会被 Promise.reject,业务侧可按需手动捕获处理。

错误类型全覆盖

  1. 重复请求错误:提示「正在处理中,请勿重复操作」
  2. 请求取消错误:静默失败,不弹出任何提示
  3. HTTP 状态码错误:400(参数错误)、401(未授权)、403(拒绝访问)、404(资源不存在)、5xx(服务器错误)
  4. 网络错误:请求已发送但无响应,提示「网络连接失败,请检查网络设置」
  5. 业务错误:后端返回的非成功业务码(如code!=='0'),提示后端返回的message信息
  6. 配置错误:请求配置错误,提示「请求配置错误」

五、最佳实践

✅ 5.1 接口层抽离

将所有接口请求抽离到独立的 @/api 目录下,按业务模块划分文件,业务组件中只调用接口方法,不直接写请求逻辑,优点:统一管理、复用性高、便于维护、修改接口时无需改动业务组件

示例:@/api/system/user.ts

typescript
import request from "@/utils/HttpRequest";
import type { Params } from "@/models/base";
import type { BasePaginatorResponse } from "@/models/base";
import type { UserItem, UserInfo } from "@/models/user";

// 用户分页查询
export const getUserPageApi = (params: Params) => {
  return request.get<BasePaginatorResponse<UserItem>>(
    "/system/user/page",
    params
  );
};

// 新增用户
export const addUserApi = (data: Params) => {
  return request.post<UserInfo>("/system/user/add", data);
};

// 修改用户
export const updateUserApi = (data: Params) => {
  return request.put<UserInfo>("/system/user/update", data);
};

// 删除用户
export const deleteUserApi = (data: Params) => {
  return request.delete("/system/user/remove", data);
};

组件中使用

typescript
import { getUserPageApi } from "@/api/system/user";

const getList = async () => {
  const res = await getUserPageApi({ page: 1, size: 10 });
  userList.value = res.list;
  total.value = res.paginator.total_record;
};

✅ 5.2 合理使用缓存

  • 静态数据(字典、系统配置、省市县数据):设置缓存时长 10*60*1000(10 分钟)
  • 低频更新数据(用户信息、角色权限):设置缓存时长 5*60*1000(5 分钟)
  • 高频更新数据(实时列表、统计数据):关闭缓存(cache:0
  • 手动刷新数据时,开启 forceRefresh: true

✅ 5.3 路由守卫中取消请求

在全局路由守卫中调用 request.cancelAbortControllersAll(),页面切换时取消所有未完成请求,避免内存泄漏和无效的接口回调。

✅ 5.4 自定义错误处理

对需要个性化错误提示的请求,关闭 showToast: false,手动捕获异常并处理:

typescript
try {
  await request.post("/system/user/add", data, { showToast: false });
  Message.success("新增用户成功!");
} catch (error) {
  Message.error("新增失败:用户名已存在");
}

六、注意事项

  1. ⚠️ 响应状态码是字符串类型:你的 ResponseCodeSUCCESS = '0'AUTH_EXPIRED = '0200002',均为字符串,封装中已做字符串匹配,请勿修改为数字,否则 Token 刷新等逻辑失效。
  2. ⚠️ Pinia 持久化配置:用户状态已配置持久化到 localStorage,页面刷新后 Token 不会丢失,无需手动处理。
  3. ⚠️ 请求 ID 自动生成:无需手动配置 requestId,封装会自动生成,保证唯一性。
  4. ⚠️ DELETE 请求传参:封装中 DELETE 请求的参数是放在 data 中的,符合 RESTful 规范,无需拼接到 URL。
  5. ⚠️ FormData 处理:封装已对 FormData 做了特殊处理,上传文件时直接传入即可,无需序列化。
  6. ⚠️ 缓存是内存缓存:缓存基于前端内存,页面刷新后缓存会清空,适合短期缓存,不适合持久化数据。
  7. ⚠️ Token 刷新依赖:必须保证 useUserStore 中的 refreshToken() 方法返回 Promise,否则 Token 过期后无法自动刷新。

七、总结

本次封装是一套企业级、标准化、无侵入式的 Vue3+TS Axios 请求解决方案,整合了前端开发中所有高频的请求层能力,所有逻辑均已封装完成,业务侧只需关注「调用接口」本身,无需重复开发通用逻辑。

封装的核心优势是:强类型约束保证开发质量、功能全覆盖满足所有业务场景、极简调用方式提升开发效率、灵活配置支持个性化需求,完全适配中后台管理系统的开发规范,可直接在生产环境中使用。


完整代码

ts
// src/utils/HttpRequest.ts
import axios from "axios";
import type {
  AxiosError,
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
} from "axios";
import { Message, type MessageReturn } from "@arco-design/web-vue";
import type { BaseResponse, Params } from "@/models/base";
import { ResponseCode } from "@/constants/responseCode";
import { useUserStore } from "@/stores/user";

interface IRequestConfig {
  // 加载状态控制
  showLoading: boolean;
  // 提示控制
  showToast: boolean;
  // 可取消性
  cancelable: boolean;
  // 重复请求处理
  preventDuplicate: boolean;
  // 请求唯一标识
  requestId: string;
  // 缓存 - 修改为缓存时间(毫秒),0或负数表示不缓存
  cache: number;
  // 强制刷新缓存(跳过缓存检查)
  forceRefresh: boolean;
}

// 定义缓存项接口
interface CacheItem<T = unknown> {
  data: T;
  expireTime: number; // 过期时间戳(毫秒)
  timestamp: number; // 创建时间戳(毫秒)
}

// 扩展 AxiosRequestConfig 以包含自定义配置
interface CustomAxiosRequestConfig extends AxiosRequestConfig {
  requestConfig?: Partial<IRequestConfig>;
}

// 修改队列项类型
interface RefreshQueueItem<T = unknown> {
  resolve: (value: T) => void;
  reject: (reason?: T) => void;
  retryFn: () => Promise<T>;
}

class HttpRequest {
  private axiosInstance: AxiosInstance;
  private loadingInstance: MessageReturn | null = null;
  private requestTimeout: number = 50000; // 请求的超时时间50秒
  private pendingRequests: Set<string> = new Set(); // 请求队列,用于防止重复请求
  private activeRequestsCount: number = 0; // 当前活跃的请求数量(用于控制loading)
  private cancelMaxCount: number = 10; // 取消的最大请求数量
  private abortControllers = new Map<string, AbortController>(); // 用于取消请求
  private cacheMap = new Map<string, CacheItem>(); //   // 缓存存储
  private cacheCleanupInterval = 5 * 60 * 1000; // 默认缓存清理间隔(5分钟)
  private cacheCleanupTimer: number | null = null; //   // 缓存清理定时器
  private refreshQueue: Array<RefreshQueueItem> = []; // 刷新队列
  // 请求默认配置
  private defaultRequestConfig: IRequestConfig = {
    showLoading: true,
    showToast: true,
    cancelable: true,
    preventDuplicate: true,
    requestId: "",
    cache: 0,
    forceRefresh: false,
  };
  private isRefreshingToken: boolean = false; // 是否正在刷新Token

  constructor(config?: AxiosRequestConfig) {
    // 创建唯一的axios实例
    this.axiosInstance = axios.create({
      baseURL: import.meta.env.VITE_API_HOST,
      timeout: this.requestTimeout,
      headers: {
        "Content-Type": "application/json;charset=UTF-8",
        Accept: "application/json",
      },
      ...config,
    });
    // 配置拦截器
    this.configureRequestInterceptors(this.axiosInstance);
    this.configureResponseInterceptors(this.axiosInstance);
    // 启动缓存清理定时器
    this.startCacheCleanup();
  }

  // 开启loading
  private openLoading(showLoading: boolean) {
    if (showLoading) {
      this.activeRequestsCount++;
      if (this.activeRequestsCount === 1) {
        this.loadingInstance = Message.loading({
          duration: 0,
          content: "Loading...",
        });
      }
    }
  }

  // 关闭loading
  private closeLoading(showLoading: boolean) {
    if (showLoading) {
      this.activeRequestsCount--;
      if (this.activeRequestsCount <= 0) {
        this.loadingInstance?.close();
        this.loadingInstance = null;
        this.activeRequestsCount = 0;
      }
    }
  }

  // 添加token
  private setToken(config: AxiosRequestConfig) {
    const userStore = useUserStore();
    if (config.headers && userStore.state.token) {
      config.headers["Authorization"] = userStore.state.token;
    }
  }

  // ==================== 接口取消方法 ====================

  // 收集可取消的请求
  private collectCancelableRequest(config: CustomAxiosRequestConfig) {
    const requestConfig = config.requestConfig as IRequestConfig;
    if (requestConfig.cancelable) {
      // 添加取消函数
      const controller = new AbortController();
      config.signal = controller.signal;
      if (this.abortControllers.size >= this.cancelMaxCount) {
        const firstKey = this.abortControllers.keys().next().value;
        this.abortControllers.delete(firstKey!);
      }
      this.abortControllers.set(requestConfig.requestId, controller);
    }
  }

  // 取消所有未完成的请求
  public cancelAbortControllersAll() {
    this.abortControllers.forEach((controller) => controller.abort());
    this.abortControllers.clear();
  }

  // 移除取消的请求
  private removeAbortControllers(requestId: string) {
    this.abortControllers.delete(requestId);
  }

  // ==================== 缓存相关方法 ====================

  // 获取缓存数据
  private getCache<T>(requestId: string): T | undefined {
    const cacheItem = this.cacheMap.get(requestId);
    if (!cacheItem) {
      return undefined;
    }

    // 检查缓存是否过期
    if (Date.now() > cacheItem.expireTime) {
      // 缓存已过期,删除并返回 undefined
      this.cacheMap.delete(requestId);
      return undefined;
    }

    return cacheItem.data as T;
  }

  // 设置缓存
  private setCache<T>(requestId: string, data: T, cacheTime: number): void {
    const now = Date.now();
    const cacheItem: CacheItem<T> = {
      data,
      expireTime: now + cacheTime,
      timestamp: now,
    };

    this.cacheMap.set(requestId, cacheItem);
  }

  // 启动缓存清理定时器
  private startCacheCleanup() {
    // 清除已存在的定时器
    if (this.cacheCleanupTimer) {
      clearInterval(this.cacheCleanupTimer);
    }

    this.cacheCleanupTimer = setInterval(() => {
      this.cleanupExpiredCache();
    }, this.cacheCleanupInterval);
  }

  // 清理过期缓存
  private cleanupExpiredCache() {
    const now = Date.now();
    for (const [requestId, cacheItem] of this.cacheMap.entries()) {
      if (now > cacheItem.expireTime) {
        this.cacheMap.delete(requestId);
      }
    }
  }

  // 配置请求拦截器
  private configureRequestInterceptors(instance: AxiosInstance) {
    instance.interceptors.request.use(
      (config) => {
        this.setToken(config);
        const requestConfig = (config as CustomAxiosRequestConfig)
          .requestConfig as IRequestConfig;
        if (requestConfig.preventDuplicate) {
          // 如果已经存在相同的请求,抛出错误
          if (this.pendingRequests.has(requestConfig.requestId)) {
            return Promise.reject(new Error("exception:repeat"));
          }
          this.pendingRequests.add(requestConfig.requestId);
        }
        this.openLoading(requestConfig.showLoading);
        this.collectCancelableRequest(config);
        return config;
      },
      (error) => {
        // 捕获请求拦截器的错误
        return Promise.reject(error);
      }
    );
  }

  // ==================== 登录过期方法 ====================

  // 认证失败,重新登录
  private handleAuthenticationExpiry(config: AxiosResponse["config"]) {
    /**
     * 登录过期后,之前的Promise状态继续保持pending,直到登录成功或者失败
     */
    if (this.isRefreshingToken) {
      return new Promise((resolve, reject) => {
        this.addToRefreshQueue({
          resolve,
          reject,
          retryFn: () => this.request(config),
        });
      });
    } else {
      this.isRefreshingToken = true;
      return new Promise((resolve, reject) => {
        this.addToRefreshQueue({
          resolve,
          reject,
          retryFn: () => this.request(config),
        });
        const userStore = useUserStore();
        userStore.refreshToken().then(
          () => {
            this.isRefreshingToken = false;
            this.processRefreshQueue();
          },
          (error) => {
            // 重新登录失败
            reject(error);
            this.isRefreshingToken = false;
            this.clearRefreshQueue(error);
            userStore.logout();
          }
        );
      });
    }
  }

  // 加入刷新队列
  private addToRefreshQueue(refreshQueue: RefreshQueueItem) {
    this.refreshQueue.push(refreshQueue);
  }

  // 刷新队列处理
  private processRefreshQueue() {
    while (this.refreshQueue.length > 0) {
      const { retryFn, resolve, reject } =
        this.refreshQueue.shift() as RefreshQueueItem;
      retryFn().then(resolve, reject);
    }
  }

  // 刷新异常处理
  private clearRefreshQueue(error: unknown) {
    while (this.refreshQueue.length > 0) {
      const { reject } = this.refreshQueue.shift() as RefreshQueueItem;
      reject(error);
    }
  }

  // 配置响应拦截器
  private configureResponseInterceptors(instance: AxiosInstance) {
    instance.interceptors.response.use(
      (res: AxiosResponse) => {
        const requestConfig = (res.config as CustomAxiosRequestConfig)
          .requestConfig as IRequestConfig;
        if (`${res.data?.code}` === ResponseCode.SUCCESS) {
          return Promise.resolve(res.data.data);
        } else if (`${res.data?.code}` === ResponseCode.AUTH_EXPIRED) {
          this.finishRequest(requestConfig);
          return this.handleAuthenticationExpiry(res.config);
        }
        if (requestConfig.showToast) {
          Message.warning(res.data.message ?? "接口异常");
        }

        return Promise.reject(res.data);
      },
      (error: AxiosError) => {
        const errorMessage = this.handleResponseError(error);
        return Promise.reject(new Error(errorMessage));
      }
    );
  }

  // 错误处理
  private handleResponseError(error: AxiosError | Error): string {
    let showToast: boolean = true;
    let errorMessage: string = "请求失败";
    if (axios.isCancel(error)) {
      showToast = false;
      errorMessage = "请求已取消";
    } else if (axios.isAxiosError(error)) {
      const config = error.config as CustomAxiosRequestConfig;
      if (config && config.requestConfig) {
        const requestConfig = config.requestConfig as IRequestConfig;
        showToast = requestConfig.showToast;
      }
      if (error?.response) {
        const { status, data } = error.response;
        const responseData = data as BaseResponse;
        switch (status) {
          case 400:
            errorMessage = responseData?.message || "请求参数错误";
            break;
          case 401:
            errorMessage = "未授权,请重新登录";
            break;
          case 403:
            errorMessage = "拒绝访问";
            break;
          case 404:
            errorMessage = "请求的资源不存在";
            break;
          case 500:
            errorMessage = "服务器内部错误";
          case 502:
          case 503:
          case 504:
            errorMessage = "服务器暂时不可用,请稍后重试";
            break;
          default:
            errorMessage = responseData?.message || `请求失败 (${status})`;
        }
      } else if (error.request) {
        errorMessage = "网络连接失败,请检查网络设置";
      } else {
        errorMessage = error.message || "请求配置错误";
      }
    } else if (error?.message === "exception:repeat") {
      errorMessage = "正在处理中,请勿重复操作";
    } else {
      errorMessage = typeof error === "string" ? error : "请求异常,请稍后重试";
    }
    if (showToast) {
      Message.warning(errorMessage);
    }
    return errorMessage;
  }

  // 生成请求唯一标识
  private generateRequestId(axiosConfig: AxiosRequestConfig): string {
    const method = (axiosConfig.method || "GET").toUpperCase();
    const url = axiosConfig.url || "";

    let params = method === "GET" ? axiosConfig.params : axiosConfig.data;

    if (params instanceof FormData) {
      // FormData 不好序列化,可以使用特殊标识
      params = "[FormData]";
    } else if (params instanceof URLSearchParams) {
      params = Object.fromEntries(params.entries());
    }

    const paramsStr = params ? this.simpleStableStringify(params) : "";

    return `${method}:${url}:${paramsStr}`.toLocaleLowerCase();
  }

  // 参数序列化
  private simpleStableStringify(obj: Params): string {
    if (!obj || typeof obj !== "object") {
      return JSON.stringify(obj);
    }

    if (Array.isArray(obj)) {
      return `[${obj
        .map((item) => this.simpleStableStringify(item))
        .join(",")}]`;
    }

    const sortedKeys = Object.keys(obj).sort();
    const parts = sortedKeys.map((key) => {
      return `${key}:${this.simpleStableStringify(obj[key] as Params)}`;
    });

    return `{${parts.join(",")}}`;
  }

  // 统一的请求收尾清理方法
  private finishRequest(requestConfig: IRequestConfig) {
    // 移除取消控制器
    if (requestConfig.cancelable) {
      this.removeAbortControllers(requestConfig.requestId);
    }
    // 移除重复请求标记
    if (requestConfig.preventDuplicate) {
      this.pendingRequests.delete(requestConfig.requestId);
    }
    // 关闭loading
    this.closeLoading(requestConfig.showLoading);
  }

  // ==================== 公共请求方法 ====================
  public async request<T>(axiosConfig: CustomAxiosRequestConfig): Promise<T> {
    // 设置默认请求方法为 POST
    if (!axiosConfig.method) {
      axiosConfig.method = "post";
    }
    // 合并配置
    const mergedConfig: CustomAxiosRequestConfig = {
      ...axiosConfig,
      requestConfig: {
        ...this.defaultRequestConfig,
        ...axiosConfig.requestConfig,
      },
    };

    const requestConfig = mergedConfig.requestConfig as IRequestConfig;
    requestConfig.requestId = this.generateRequestId(mergedConfig);

    // 检查缓存
    if (requestConfig.cache > 0 && !requestConfig.forceRefresh) {
      const cachedData = this.getCache<T>(requestConfig.requestId);

      if (cachedData !== undefined) {
        // 如果有缓存且未过期,直接返回缓存数据
        return Promise.resolve(cachedData);
      }
    }

    try {
      const response = await this.axiosInstance.request<T, T>(mergedConfig);

      // 如果配置了缓存,存储响应数据
      if (requestConfig.cache > 0) {
        this.setCache<T>(
          requestConfig.requestId,
          response,
          requestConfig.cache
        );
      }

      return response;
    } finally {
      this.finishRequest(requestConfig);
    }
  }
  public get<T>(
    url: string,
    params?: Params,
    config?: CustomAxiosRequestConfig
  ) {
    return this.request<T>({
      url,
      method: "get",
      params,
      ...config,
    });
  }

  public post<T>(
    url: string,
    data?: Params,
    config?: CustomAxiosRequestConfig
  ) {
    return this.request<T>({
      url,
      method: "post",
      data,
      ...config,
    });
  }

  public put<T>(url: string, data?: Params, config?: CustomAxiosRequestConfig) {
    return this.request<T>({
      url,
      method: "put",
      data,
      ...config,
    });
  }

  public delete<T>(
    url: string,
    data?: Params,
    config?: CustomAxiosRequestConfig
  ) {
    return this.request<T>({
      url,
      method: "delete",
      data,
      ...config,
    });
  }

  public upload<T>(
    url: string,
    formData: FormData,
    config?: CustomAxiosRequestConfig
  ) {
    return this.request<T>({
      url,
      method: "post",
      data: formData,
      headers: {
        "Content-Type": "multipart/form-data",
        ...config?.headers,
      },
      timeout: 120000, // 上传设置2分钟超时
      ...config,
    });
  }
}

// 创建默认实例
const defaultHttpRequest = new HttpRequest();

// 也可以导出类,以便需要时创建新实例
export { HttpRequest };
export default defaultHttpRequest;
ts
// src/stores/user.ts
import { loginApi, refreshTokenApi } from "@/api/auth";
import type { Params } from "@/models/base";
import { defineStore } from "pinia";
import { computed, reactive } from "vue";

const defaultState = {
  username: "",
  token: "",
  refreshToken: "",
  userId: "",
};

export const useUserStore = defineStore(
  "user",
  () => {
    // State
    const state = reactive({
      ...defaultState,
    });

    // Getters
    const isLogin = computed(() => Boolean(state.token));

    // Actions
    const login = async (data: Params) => {
      const res = await loginApi(data);
      Object.assign(state, res);
    };

    const refreshToken = async () => {
      const res = await refreshTokenApi(state.refreshToken);
      Object.assign(state, res);
    };

    const logout = () => {
      Object.assign(state, defaultState);
    };

    return { state, isLogin, login, refreshToken, logout };
  },
  {
    persist: {
      storage: localStorage,
      key: "user-state",
      pick: ["state"],
    },
  }
);
ts
// src/constants/responseCode.ts

// 在常量文件中定义-必须是字符串
export const ResponseCode = {
  SUCCESS: "0", // 请求成功
  AUTH_EXPIRED: "0200002", // 登录过期
} as const;
ts
// src/models/base.ts
// 基础响应数据格式
export type BaseResponse<T = unknown> = {
  code: string;
  message: string;
  data: T;
};

// 基础分页响应数据格式
export type BasePaginatorResponse<T = unknown> = BaseResponse<{
  list: Array<T>;
  paginator: {
    total_page: number;
    total_record: number;
    current_page: number;
    page_size: number;
  };
}>;

// 请求参数
export interface Params {
  [key: string]: unknown;
}