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 网络请求。
陷阱:同时设置 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 中设为默认。