Axios 完全指南

Axios 是前端最流行的 HTTP 请求库,基于 Promise,同时支持浏览器和 Node.js。 项目中应创建实例而非直接使用 axios,便于统一配置: 拦截器是 Axios 的核心功能,用于统一处理 Token 注入、错误处理、Loading 状态等。 浏览器在发送 FormData 时会自动设置 Content-Type: multipart/form-data; boundary=...,手动设置会破坏 boundary 参数导致后端解析失败: 创建 Axios 实例而非直接用全局 axios:全局 axios 的配置修改影响所有请求,用

分享

官方文档:https://axios-http.com/zh/
最后更新:2026-03-29


1. 基础概念

Axios 是什么

Axios 是前端最流行的 HTTP 请求库,基于 Promise,同时支持浏览器和 Node.js。

特性 Axios fetch API
浏览器兼容性 IE11+(内部用 XHR) 现代浏览器
拦截器 内置
自动 JSON 序列化 需手动处理
超时控制 内置 需 AbortController
请求取消 AbortController / CancelToken AbortController
上传进度 内置
错误处理 HTTP 4xx/5xx 自动抛出 需手动判断

安装

npm install axios

2. 基础使用

直接调用

import axios from "axios";

// GET
const { data } = await axios.get("/api/users", {
  params: { page: 1, keyword: "alice" },
});

// POST
const { data } = await axios.post("/api/users", {
  name: "Alice",
  email: "[email protected]",
});

// PUT / PATCH / DELETE
await axios.put("/api/users/1", { name: "Bob" });
await axios.patch("/api/users/1", { name: "Bob" });
await axios.delete("/api/users/1");

Response 对象

属性 类型 说明
data T 响应体(自动 JSON 解析)
status number HTTP 状态码
statusText string 状态文本
headers object 响应头
config object 请求配置
request XMLHttpRequest 原始请求对象

3. 创建 axios 实例(核心)

项目中应创建实例而非直接使用 axios,便于统一配置:

// src/lib/request.ts
import axios, { type AxiosResponse } from "axios";

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL ?? "/api",
  timeout: 15000,
  headers: {
    "Content-Type": "application/json",
  },
});

export default request;

创建实例的配置参数

参数 类型 说明
baseURL string 基础 URL,所有请求自动拼接
timeout number 超时时间(ms),超时抛出 ECONNABORTED
headers object 默认请求头
params object 默认查询参数(追加到每次请求)
withCredentials boolean 跨域请求时是否携带 cookie
responseType string 响应类型:json/blob/arraybuffer/stream
paramsSerializer function 自定义查询参数序列化
validateStatus function 决定哪些状态码视为成功
maxRedirects number 最大重定向次数

4. 拦截器(Interceptors)

拦截器是 Axios 的核心功能,用于统一处理 Token 注入、错误处理、Loading 状态等。

请求拦截器 — 注入 Token

// src/lib/request.ts
import router from "@/router";
import { useAuthStore } from "@/stores/auth";

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    const auth = useAuthStore();
    if (auth.token) {
      config.headers.Authorization = `Bearer ${auth.token}`;
    }
    return config;
  },
  (error) => Promise.reject(error),
);

响应拦截器 — 统一错误处理

// 响应拦截器
request.interceptors.response.use(
  (response) => {
    // 2xx 响应:直接返回
    // 如果后端统一用 {code, message, data} 包装,在这里解包
    const { code, message, data } = response.data;
    if (code !== 200) {
      ElMessage.error(message);       // 显示错误提示
      return Promise.reject(message);
    }
    return data;  // 只返回业务数据
  },
  async (error) => {
    if (error.response) {
      const { status } = error.response;

      if (status === 401) {
        // Token 过期,尝试刷新
        try {
          await refreshToken();
          // 重试原始请求
          return request(error.config);
        } catch {
          useAuthStore().logout();
          router.push("/login");
        }
      } else if (status === 403) {
        ElMessage.error("权限不足");
        router.push("/403");
      } else if (status === 404) {
        ElMessage.error("资源不存在");
      } else if (status >= 500) {
        ElMessage.error("服务器内部错误,请稍后重试");
      }
    } else if (error.code === "ECONNABORTED") {
      ElMessage.error("请求超时,请检查网络");
    } else if (!navigator.onLine) {
      ElMessage.error("网络已断开,请检查网络连接");
    }

    return Promise.reject(error);
  },
);

多个拦截器的执行顺序

请求拦截器:后添加的先执行(栈)
响应拦截器:先添加的先执行(队列)

5. Token 自动刷新(无感刷新)

// src/lib/request.ts
let isRefreshing = false;
let failedQueue: Array<{ resolve: (token: string) => void; reject: (err: unknown) => void }> = [];

function processQueue(error: unknown, token: string | null = null) {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) reject(error);
    else resolve(token!);
  });
  failedQueue = [];
}

request.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 已在刷新中,排队等待
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then((token) => {
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return request(originalRequest);
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const newToken = await doRefreshToken();
        useAuthStore().setToken(newToken);
        processQueue(null, newToken);
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        return request(originalRequest);
      } catch (refreshError) {
        processQueue(refreshError, null);
        useAuthStore().logout();
        router.push("/login");
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  },
);

6. 请求取消

import axios from "axios";

// 使用 AbortController(推荐)
const controller = new AbortController();

const response = await request.get("/users", {
  signal: controller.signal,
});

// 取消请求
controller.abort();

// 在 Vue/React 组件中(路由切换时取消未完成的请求)
// Vue
onUnmounted(() => controller.abort());

// React
useEffect(() => {
  const controller = new AbortController();
  fetchData(controller.signal);
  return () => controller.abort();
}, []);

7. 文件上传与下载

文件上传(带进度)

async function uploadFile(file: File, onProgress?: (percent: number) => void) {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("filename", file.name);

  const { data } = await request.post("/upload", formData, {
    headers: { "Content-Type": "multipart/form-data" },
    onUploadProgress: (progressEvent) => {
      if (progressEvent.total) {
        const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
        onProgress?.(percent);
      }
    },
  });
  return data;
}

文件下载

async function downloadFile(url: string, filename: string) {
  const response = await request.get(url, {
    responseType: "blob",
  });

  const blob = new Blob([response.data]);
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = filename;
  link.click();
  URL.revokeObjectURL(link.href);
}

8. 类型封装(TypeScript)

// src/lib/request.ts

// 统一响应体结构
interface ApiResponse<T = unknown> {
  code: number;
  message: string;
  data: T;
}

// 封装泛型请求函数
async function get<T>(url: string, params?: object): Promise<T> {
  const { data } = await request.get<ApiResponse<T>>(url, { params });
  return data.data;
}

async function post<T>(url: string, body?: object): Promise<T> {
  const { data } = await request.post<ApiResponse<T>>(url, body);
  return data.data;
}

export { get, post };
// src/api/user.ts
import { get, post } from "@/lib/request";

interface User {
  id: number;
  name: string;
  email: string;
}

export const userApi = {
  list: (params?: { page?: number; keyword?: string }) =>
    get<User[]>("/users", params),

  getById: (id: number) =>
    get<User>(`/users/${id}`),

  create: (data: Omit<User, "id">) =>
    post<User>("/users", data),
};

9. 与 TanStack Query 配合

// src/hooks/useUserQuery.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/vue-query";
import { userApi } from "@/api/user";

export function useUserList(params: Ref<{ page: number; keyword: string }>) {
  return useQuery({
    queryKey: computed(() => ["users", params.value]),
    queryFn: () => userApi.list(params.value),
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: userApi.create,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users"] }),
  });
}

10. 最佳实践

API 按模块分层

src/
  api/
    user.ts        # userApi.list / userApi.create ...
    article.ts
    order.ts
  lib/
    request.ts     # axios 实例 + 拦截器
  hooks/
    useUserQuery.ts  # 封装 TanStack Query

错误处理分层

  • 网络/HTTP 错误:在响应拦截器统一处理(toast 提示 / 跳转登录)
  • 业务错误(code !== 200):在拦截器处理,或在调用处 try/catch
  • 不要在每个 API 调用处重复写错误处理

11. 踩坑与注意事项

baseURL 末尾 / 与请求路径开头 / 的关系

// baseURL = "https://api.example.com/v1"(无末尾斜杠)
axios.get("/users")   // → https://api.example.com/users(v1 被覆盖!)
axios.get("users")    // → https://api.example.com/v1/users(正确)

// baseURL = "https://api.example.com/v1/"(有末尾斜杠,推荐)
axios.get("/users")   // → https://api.example.com/users(仍有问题)
axios.get("users")    // → https://api.example.com/v1/users(正确)

上传文件不要手动设置 Content-Type

浏览器在发送 FormData 时会自动设置 Content-Type: multipart/form-data; boundary=...,手动设置会破坏 boundary 参数导致后端解析失败:

// 错误
headers: { "Content-Type": "multipart/form-data" }

// 正确:不设置,让浏览器自动处理
// 或删除 Content-Type(让实例默认值被覆盖)
delete config.headers["Content-Type"];

最佳实践

创建 Axios 实例而非直接用全局 axios:全局 axios 的配置修改影响所有请求,用 axios.create 创建实例隔离不同 baseURL 和默认头,每个服务一个实例:

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' },
});

在响应拦截器中统一处理认证失效:401 响应的重试/跳转逻辑写一次,而非散落在每个请求中:

apiClient.interceptors.response.use(
  res => res,
  err => {
    if (err.response?.status === 401) router.push('/login');
    return Promise.reject(err);
  }
);

请求取消用 AbortController:Axios 已原生支持 signal 参数(Axios 1.x),React 组件卸载时取消未完成请求,防止内存泄漏和 state 更新错误:

useEffect(() => {
  const controller = new AbortController();
  axios.get('/api/data', { signal: controller.signal });
  return () => controller.abort();
}, []);

不要在请求拦截器中捕获错误:请求拦截器的 rejected 回调只处理请求构造阶段的错误,网络错误和服务端错误要在响应拦截器的 rejected 回调中处理。

类型化 axios.get<ResponseType>() 的泛型:指定响应类型让 TypeScript 推断 res.data 的类型,避免到处 as unknown as T


常见陷阱

陷阱:axios.defaults 修改影响测试中的全局状态

现象: 某个测试修改了 axios.defaults.headers,导致后续测试请求头不符合预期。
原因: axios.defaults 是全局单例,测试间共享状态。
解决: 测试中永远不用全局 axios,用 axios.create() 创建隔离实例,配合 axios-mock-adapter mock 网络请求。

陷阱:同时设置 cancelTokensignal 冲突

现象: 升级到 Axios 1.x 后取消请求行为异常。
原因: cancelToken(旧 API)和 signal(新 API)同时使用时行为未定义,Axios 1.x 已废弃 cancelToken
解决: 只用 signal: controller.signal,删除所有 cancelToken 相关代码。

陷阱:POST 请求自动序列化对象但服务端收到字符串

现象: 发送对象时服务端报 Unexpected token { in JSON,实际收到的是 [object Object]
原因: 未设置 Content-Type: application/json 时,部分框架会将 data 作为 form-encode 处理,嵌套对象序列化为 [object Object]
解决: 显式设置 Content-Type: application/json,或在 axios.createheaders 中设为默认。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog