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 的 封装终极版
/**
* 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等。
目录结构
├── src
│ ├── api
│ │ ├── user.js
│ │ ├── auth.js
│ │ └── ...
│ ├── utils
│ │ ├── axios-request.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;- 统一导出所有接口模块,方便在组件中引入
// 统一导出所有接口模块,方便在组件中引入
import userApi from "./user";
import authApi from "./auth";
export { userApi, authApi };- 组件中使用(Vue)
<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 组件不封装单独使用示例
<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 封装简单版
// 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 即可;