Skip to content

Axios 封装

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

核心

  • 基于 Promise:支持  .then().catch()  或  async/await

  • 支持浏览器和 Node.js:在浏览器中使用  XMLHttpRequest,在 Node.js 中使用  http  模块

  • 请求/响应拦截器:可以在请求发出前或响应返回后进行处理

  • 自动转换数据:JSON 自动序列化/反序列化

  • 支持取消请求:通过  CancelToken  或  AbortController

  • 支持并发请求:可以同时发送多个请求并统一处理结果

Axios 的核心功能

  • 请求方法: get、post、put、delete、patch  等

  • 请求配置: 可以设置  baseURL、headers、timeout、params  等

  • 拦截器: request/response  拦截器,可统一添加 token、错误处理、日志等

  • 数据转换: 自动将请求数据序列化,响应数据 JSON 解析

  • 错误处理: 支持 HTTP 状态码判断,status / statusText / data

  • 并发控制: axios.all 或  Promise.all  同时处理多个请求

  • 请求取消: 支持中断请求,避免重复请求或组件卸载时请求泄漏

Axios 的 封装终极版

js
/**
 * axios-request.js
 * -------------------
 * Axios 完整封装(兼容 axios v0.21+ 和 v0.22+)
 *
 * 特点:
 * 1. 单个 axios 实例,支持 baseURL / 超时 / headers
 * 2. 请求/响应拦截器(可扩展)
 * 3. 自动携带 token,401 自动刷新 token,支持并发队列
 * 4. 请求去重(相同请求返回同一 Promise)
 * 5. GET 简易缓存(可选,支持过期时间)
 * 6. 自动重试(指数退避,可配置)
 * 7. 并发请求限制(Semaphore)
 * 8. 支持取消请求(AbortController + CancelToken)
 * 9. 上传 / 下载封装
 * 10. 内置 401 / 403 / 404 / 500 全局错误处理
 */

import axios from "axios";

// -------------------- 默认配置 --------------------
const DEFAULT_CONFIG = {
  baseURL: "", // 基础 URL
  timeout: 15000, // 请求超时
  headers: { "Content-Type": "application/json" },
  retry: 2, // 默认重试次数
  retryDelay: 300, // 重试延迟(ms)
  cacheTTL: 10 * 1000, // GET 缓存有效期(ms)
  concurrency: 10, // 并发限制
};

// -------------------- 内部状态 --------------------
const state = {
  authToken: localStorage.getItem("token") || null, // 用户 token
  refreshTokenFn: null, // 刷新 token 回调函数
  isRefreshing: false, // 是否正在刷新 token
  refreshQueue: [], // 刷新 token 队列
  pendingRequests: new Map(), // 请求去重 Map
  cache: new Map(), // GET 请求缓存 Map
  semaphoreCounter: 0, // 并发控制计数
};

// -------------------- 工具函数 --------------------
function requestKey(config) {
  // 根据请求生成唯一 key,用于去重/缓存
  const { method = "get", url = "", params, data } = config;
  return [
    method.toLowerCase(),
    url,
    JSON.stringify(params || ""),
    JSON.stringify(data || ""),
  ].join("&");
}
function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}
async function retryWithBackoff(fn, retries, delay) {
  // 重试逻辑(指数退避)
  let attempt = 0;
  while (true) {
    try {
      return await fn();
    } catch (err) {
      if (attempt >= retries) throw err;
      await sleep(delay * Math.pow(2, attempt));
      attempt += 1;
    }
  }
}
async function acquireSlot(maxConcurrency) {
  // 并发控制(Semaphore)
  while (state.semaphoreCounter >= maxConcurrency) await sleep(50);
  state.semaphoreCounter += 1;
}
function releaseSlot() {
  state.semaphoreCounter = Math.max(0, state.semaphoreCounter - 1);
}

// -------------------- Axios 实例 --------------------
const instance = axios.create({
  baseURL: DEFAULT_CONFIG.baseURL,
  timeout: DEFAULT_CONFIG.timeout,
  headers: DEFAULT_CONFIG.headers,
});

// -------------------- 请求拦截器 --------------------
instance.interceptors.request.use(
  (config) => {
    // 自动带 token
    if (state.authToken) {
      config.headers = config.headers || {};
      config.headers.Authorization = `Bearer ${state.authToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// -------------------- 响应拦截器 --------------------
instance.interceptors.response.use(
  (res) => res,
  async (error) => {
    const original = error.config;
    if (!original) return Promise.reject(error);
    const status = error.response?.status;
    // -------------------- 401 未授权 --------------------
    if (status === 401 && typeof state.refreshTokenFn === "function") {
      if (state.isRefreshing) {
        // 如果已经在刷新 token,将请求加入队列等待
        return new Promise((resolve, reject) =>
          state.refreshQueue.push({ resolve, reject, config: original })
        );
      }
      state.isRefreshing = true;
      try {
        const newTokenData = await state.refreshTokenFn();
        if (!newTokenData?.token) throw new Error("刷新 token 失败");
        state.authToken = newTokenData.token;
        // 刷新后重新发起队列中的请求
        state.refreshQueue.forEach((q) => {
          q.config.headers = q.config.headers || {};
          q.config.headers.Authorization = `Bearer ${state.authToken}`;
          instance.request(q.config).then(q.resolve).catch(q.reject);
        });
        state.refreshQueue.length = 0;
        // 当前请求也重新发起
        original.headers = original.headers || {};
        original.headers.Authorization = `Bearer ${state.authToken}`;
        return instance.request(original);
      } catch (e) {
        state.refreshQueue.forEach((q) => q.reject(e));
        state.refreshQueue.length = 0;
        state.authToken = null;
        return Promise.reject(e);
      } finally {
        state.isRefreshing = false;
      }
    }
    // -------------------- 403 / 404 / 500 全局处理 --------------------
    if (status === 403) {
      console.warn("没有权限访问,请联系管理员");
    }
    if (status === 404) {
      console.warn("请求的资源不存在");
    }
    if (status === 500) {
      console.error("服务器错误,请稍后重试");
    }
    return Promise.reject(error);
  }
);

// -------------------- 取消请求兼容处理 --------------------
function attachCancelSupport(config, opts = {}) {
  // axios >=0.22 支持 signal,0.21 使用 CancelToken
  if (config.signal) return config;
  if (typeof axios.CancelToken !== "undefined" && opts.controller) {
    config.cancelToken = new axios.CancelToken((c) => {
      opts.controller.cancel = c;
    });
  }
  return config;
}

// -------------------- 原始请求封装 --------------------
async function rawRequest(config, opts = {}) {
  const {
    dedupe = true,
    cache = false,
    cacheTTL = DEFAULT_CONFIG.cacheTTL,
    retry = DEFAULT_CONFIG.retry,
    retryDelay = DEFAULT_CONFIG.retryDelay,
    concurrency = DEFAULT_CONFIG.concurrency,
    controller = null,
  } = opts;
  const key = requestKey(config);
  // -------------------- GET 缓存 --------------------
  if (cache && config.method?.toLowerCase() === "get") {
    const c = state.cache.get(key);
    if (c && Date.now() - c.ts < cacheTTL) return c.data;
  }
  // -------------------- 请求去重 --------------------
  if (dedupe && state.pendingRequests.has(key)) {
    return state.pendingRequests.get(key);
  }
  const promise = (async () => {
    await acquireSlot(concurrency);
    try {
      const doRequest = () =>
        instance
          .request(attachCancelSupport(config, { controller }))
          .then((r) => r.data);
      const res = await retryWithBackoff(doRequest, retry, retryDelay);
      if (cache && config.method?.toLowerCase() === "get") {
        state.cache.set(key, { ts: Date.now(), data: res });
      }
      return res;
    } finally {
      releaseSlot();
      state.pendingRequests.delete(key);
    }
  })();
  state.pendingRequests.set(key, promise);
  return promise;
}

// -------------------- 对外 API --------------------
const api = {
  // 基础设置
  setBaseURL(url) {
    instance.defaults.baseURL = url;
  },
  setTimeout(ms) {
    instance.defaults.timeout = ms;
  },
  setAuthToken(token) {
    localStorage.setItem("token", token);
    state.authToken = token;
  },
  getAuthToken() {
    return state.authToken || localStorage.getItem("token");
  },
  clearAuthToken() {
    localStorage.removeItem("token");
    state.authToken = null;
  },
  setRefreshHandler(fn) {
    state.refreshTokenFn = fn;
  },
  // 拦截器
  addRequestInterceptor(f, r) {
    return instance.interceptors.request.use(f, r);
  },
  addResponseInterceptor(f, r) {
    return instance.interceptors.response.use(f, r);
  },
  removeInterceptor(id, type = "request") {
    if (type === "request") instance.interceptors.request.eject(id);
    else instance.interceptors.response.eject(id);
  },
  // 原始请求
  request(config, opts = {}) {
    return rawRequest(config, opts);
  },
  // 常用请求方法
  get(
    url,
    {
      params = {},
      headers = {},
      signal = null,
      cache = false,
      cacheTTL = null,
      retry = null,
      dedupe = true,
      controller = null,
    } = {}
  ) {
    return rawRequest(
      { url, method: "get", params, headers, signal },
      {
        cache,
        cacheTTL: cacheTTL || DEFAULT_CONFIG.cacheTTL,
        retry: retry ?? DEFAULT_CONFIG.retry,
        dedupe,
        controller,
      }
    );
  },
  post(
    url,
    data,
    {
      params = {},
      headers = {},
      signal = null,
      retry = null,
      dedupe = false,
      controller = null,
    } = {}
  ) {
    return rawRequest(
      { url, method: "post", data, params, headers, signal },
      { retry: retry ?? DEFAULT_CONFIG.retry, dedupe, controller }
    );
  },
  put(url, data, opts = {}) {
    return rawRequest(
      {
        url,
        method: "put",
        data,
        headers: opts.headers || {},
        params: opts.params || {},
        signal: opts.signal || null,
      },
      opts
    );
  },
  delete(url, { params = {}, headers = {}, signal = null } = {}) {
    return rawRequest({ url, method: "delete", params, headers, signal });
  },
  // 上传文件
  upload(
    url,
    files = {},
    {
      fields = {},
      headers = {},
      onProgress = null,
      signal = null,
      controller = null,
    } = {}
  ) {
    const form = new FormData();
    Object.keys(fields || {}).forEach((k) => form.append(k, fields[k]));
    if (Array.isArray(files))
      files.forEach((f) => form.append(f.name || "file", f.file));
    else Object.keys(files).forEach((k) => form.append(k, files[k]));
    return rawRequest(
      {
        url,
        method: "post",
        data: form,
        headers: { ...headers, "Content-Type": "multipart/form-data" },
        onUploadProgress: onProgress ? (ev) => onProgress(ev) : undefined,
        signal,
      },
      { dedupe: false, controller }
    );
  },
  // 下载文件
  async download(
    url,
    {
      params = {},
      filename = null,
      headers = {},
      signal = null,
      controller = null,
    } = {}
  ) {
    const res = await rawRequest(
      { url, method: "get", params, headers, responseType: "blob", signal },
      { dedupe: false, retry: 0, controller }
    );
    const blob = res instanceof Blob ? res : new Blob([res]);
    if (typeof window !== "undefined" && filename) {
      const link = document.createElement("a");
      link.href = URL.createObjectURL(blob);
      link.download = filename;
      document.body.appendChild(link);
      link.click();
      link.remove();
      URL.revokeObjectURL(link.href);
    }
    return blob;
  },
  // 缓存管理
  clearCache(key = null) {
    if (!key) state.cache.clear();
    else state.cache.delete(key);
  },
  // 暴露原始 axios 实例
  instance,
};

/*
 *
 * 这里注意三点
 * 1. 使用的时候 登陆完成后 组件里面 一定要用api.setAuthToken(token) 保存token,退出的时候也得用api.clearAuthToken() 清除token
 * 2. 封装你自己的刷新token方法
 * 3. 基础BaseURL
 */

/* 我这里就拿BaseURL,和token  举例 */

// 设置 BaseUrl

const BaseUrl = import.meta.env.VITE_BASE_URL;
api.setBaseURL(BaseUrl);

// 设置 refreshToken

// api.setRefreshHandler(async() => {})

export default api;

总结

  • 完整封装:支持 GET/POST/PUT/DELETE/UPLOAD/DOWNLOAD 等常用方法。

  • 请求优化:去重、缓存、重试、并发限制。

  • 中断请求:兼容 Axios 0.21 的 CancelToken 和 0.22+的 AbortController.signal。

  • Token 自动刷新:处理 401 场景,带并发队列。

  • 全局错误处理:内置 401/403/404/500 提示,支持自定义拦截器。

  • 扩展性强:可添加请求/响应拦截器、缓存策略、上传下载进度等。

  • 开箱即用:只需引入 api 并调用 api.get/post/... 即可。

使用

  • 在项目中 还会再做一层接口请求封装

  • src/api/ 目录下分模块管理接口,例如:user.js、auth.js 等。

  • 每个文件只负责描述“某个业务模块的接口”,组件里调用时只关心 userApi.getUserList(),而不用关心 URL、请求方法、headers 等。

目录结构

bash
├── src
   ├── api
   ├── user.js
   ├── auth.js
   └── ...
   ├── utils
   ├── axios-request.js

示例封装

js
// src/api/user.js

import api from "@/utils/axios-request";
const userApi = {
  // 获取用户列表(带分页,支持缓存)
  getUserList(params) {
    return api.get("/users", { params, cache: true });
  },
  // 获取单个用户详情
  getUserDetail(id) {
    return api.get(`/users/${id}`);
  },
  // 创建用户
  createUser(data) {
    return api.post("/users", data);
  },
  // 更新用户
  updateUser(id, data) {
    return api.put(`/users/${id}`, data);
  },
  // 删除用户
  deleteUser(id) {
    return api.delete(`/users/${id}`);
  },
};
export default userApi;
  • 统一导出所有接口模块,方便在组件中引入
js
// 统一导出所有接口模块,方便在组件中引入

import userApi from "./user";
import authApi from "./auth";

export { userApi, authApi };
  • 组件中使用(Vue)
html
<script>
  import { userApi, authApi } from "@/api";
  export default {
    name: "UserPage",
    data() {
      return {
        users: [],
      };
    },
    async created() {
      // 获取用户列表
      this.users = await userApi.getUserList({ page: 1, size: 10 });
      // 登录示例
      const loginResp = await authApi.login({
        username: "test",
        password: "123456",
      });
      console.log("登录结果", loginResp);
    },
  };
</script>

总结

  • 所有请求都走 axios-request.js,有 token、重试、缓存等统一机制。

  • 组件层代码简洁:直接 userApi.getUserList() 就能拿到数据。

  • 如果后端接口路径改了,只需改 api/user.js,组件不用动。

Vue 组件不封装单独使用示例

vue
<template>
  <div>
    <h2>Axios 封装使用示例</h2>
    <button @click="fetchData">GET 数据</button>
    <button @click="postData">POST 数据</button>
    <button @click="cancelRequest">取消请求</button>
    <button @click="uploadFile">上传文件</button>
    <button @click="downloadFile">下载文件</button>
    <pre>{{ result }}</pre>
  </div>
</template>
<script>
import api from "@/utils/axios-request.js";
export default {
  data() {
    return {
      result: null,
      controller: new AbortController(), // 用于取消请求
    };
  },
  methods: {
    // GET 请求示例(带缓存)
    async fetchData() {
      try {
        const res = await api.get("/api/user/list", {
          cache: true,
          cacheTTL: 5000,
        });
        this.result = res;
      } catch (err) {
        console.error(err);
      }
    },
    // POST 请求示例(带 token)
    async postData() {
      api.setAuthToken("你的 token");
      try {
        const res = await api.post("/api/user/add", { name: "张三", age: 18 });
        this.result = res;
      } catch (err) {
        console.error(err);
      }
    },
    // 取消请求示例
    async cancelRequest() {
      const controller = this.controller;
      const promise = api.get("/api/user/list", { signal: controller.signal });
      setTimeout(() => controller.abort(), 100); // 100ms 后取消请求
      try {
        const res = await promise;
        this.result = res;
      } catch (err) {
        console.log("请求被取消", err.message);
      }
    },
    // 文件上传示例
    async uploadFile() {
      const fileInput = document.createElement("input");
      fileInput.type = "file";
      fileInput.onchange = async (e) => {
        const file = e.target.files[0];
        const res = await api.upload("/api/upload", [{ file, name: "file" }]);
        console.log("上传成功", res);
      };
      fileInput.click();
    },
    // 文件下载示例
    async downloadFile() {
      await api.download("/api/download/file", { filename: "test.xlsx" });
      console.log("下载完成");
    },
  },
  mounted() {
    // 设置 401 刷新 token 回调
    api.setRefreshHandler(async () => {
      // 模拟刷新 token
      const res = await api.post("/api/refresh-token", { refreshToken: "xxx" });
      return { token: res.token };
    });
  },
};
</script>

Axios 封装简单版

js
// axios-simple-request.js

import axios from "axios";

// -------------------- 内部状态 --------------------
const state = {
  authToken: null,
  baseURL: "",
  cache: new Map(),
};

// -------------------- 工具函数 --------------------
function requestKey(url, params) {
  return `${url}?${JSON.stringify(params || {})}`;
}
function handleStatus(res) {
  const code = res.status;
  if (code === 200) return res.data;
  if (code === 401) {
    window.location.href = "/login";
    console.warn("401 未授权");
    return null;
  }
  if (code === 403) {
    window.location.href = "/login";
    console.warn("403 无权限");
    return null;
  }
  if (code === 404) {
    window.location.href = "/";
    console.warn("404 资源未找到");
    return null;
  }
  throw new Error(`请求异常,状态码: ${code}`);
}

// -------------------- Axios 实例 --------------------
const instance = axios.create({
  baseURL: state.baseURL,
  timeout: 15000,
  headers: { "Content-Type": "application/json" },
});
instance.interceptors.request.use((config) => {
  if (state.authToken)
    config.headers.Authorization = `Bearer ${state.authToken}`;
  return config;
});

// -------------------- 基础请求方法 --------------------
async function request({
  url,
  method = "get",
  data = {},
  params = {},
  files = null,
  fields = {},
  cacheTTL = 0,
}) {
  const key = requestKey(url, params);
  // GET 缓存处理
  if (method.toLowerCase() === "get" && cacheTTL > 0) {
    const cached = state.cache.get(key);
    if (cached && Date.now() - cached.ts < cacheTTL) return cached.data;
  }
  // 上传文件
  if (method.toLowerCase() === "upload") {
    const form = new FormData();
    Object.keys(fields).forEach((k) => form.append(k, fields[k]));
    if (Array.isArray(files))
      files.forEach((f) => form.append(f.name || "file", f.file));
    else if (files) Object.keys(files).forEach((k) => form.append(k, files[k]));
    try {
      const res = await instance.post(url, form, {
        headers: { "Content-Type": "multipart/form-data" },
      });
      return handleStatus(res);
    } catch (err) {
      console.error(err);
      throw err;
    }
  }
  // 普通请求
  try {
    const res = await instance.request({ url, method, data, params });
    const result = handleStatus(res);
    if (method.toLowerCase() === "get" && cacheTTL > 0)
      state.cache.set(key, { data: result, ts: Date.now() });
    return result;
  } catch (err) {
    console.error(err);
    throw err;
  }
}

// -------------------- 对外 API --------------------
const api = {
  setBaseURL(url) {
    state.baseURL = url;
    instance.defaults.baseURL = url;
  },
  setToken(token) {
    state.authToken = token;
  },
  clearToken() {
    state.authToken = null;
  },
  get(url, params = {}, cacheTTL = 0) {
    return request({ url, method: "get", params, cacheTTL });
  },
  post(url, data = {}, params = {}) {
    return request({ url, method: "post", data, params });
  },
  put(url, data = {}, params = {}) {
    return request({ url, method: "put", data, params });
  },
  delete(url, params = {}) {
    return request({ url, method: "delete", params });
  },
  upload(url, files, fields = {}) {
    return request({ url, method: "upload", files, fields });
  },
};

export default api;

总结

  • 所有请求方法都统一通过 request() 处理,代码简洁;

  • GET 请求支持缓存与过期时间;

  • 自动带 token,方便身份验证;

  • 支持文件上传(统一接口 method: 'upload');

  • 全局处理 200/401/403/404;

  • 易用性强,调用 api.get/post/put/delete/upload 即可;