> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Axios 完全指南
- URL: https://blog.vercanti.com/axios-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:13.000Z
- Updated: 2026-08-28T14:58:20.000Z
- Description: Axios 是前端最流行的 HTTP 请求库，基于 Promise，同时支持浏览器和 Node.js。 项目中应创建实例而非直接使用 axios，便于统一配置： 拦截器是 Axios 的核心功能，用于统一处理 Token 注入、错误处理、Loading 状态等。 浏览器在发送 FormData 时会自动设置 Content-Type: multipart/form-data; boundary=...，手动设置会破坏 boundary 参数导致后端解析失败： 创建 Axios 实例而非直接用全局 axios：全局 axios 的配置修改影响所有请求，用
- Author: yellowdog
- Tags: 前端开发

> 官方文档：<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 自动抛出             | 需手动判断             |

### 安装

```bash
npm install axios

```

---

## 2\. 基础使用

### 直接调用

```ts
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: "alice@example.com",
});

// 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`，便于统一配置：

```ts
// 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

```ts
// 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),
);

```

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

```ts
// 响应拦截器
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 自动刷新（无感刷新）

```ts
// 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\. 请求取消

```ts
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\. 文件上传与下载

### 文件上传（带进度）

```ts
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;
}

```

### 文件下载

```ts
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）

```ts
// 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 };

```

```ts
// 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 配合

```ts
// 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 末尾 / 与请求路径开头 / 的关系

```ts
// 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 参数导致后端解析失败：

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

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

```

---

## 最佳实践

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

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

```

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

```ts
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 更新错误：

```ts
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 网络请求。

### 陷阱：同时设置 `cancelToken` 和 `signal` 冲突

**现象：** 升级到 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.create` 的 `headers` 中设为默认。

---

## 参见

- [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)
- [React完全指南](https://blog.vercanti.com/react-wan-quan-zhi-nan/)
- [TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)
- [TanStack Query 完全指南](https://blog.vercanti.com/tanstack-query-wan-quan-zhi-nan/)