axios 封装
安装 axios 依赖
js
npm i axios封装
创建config/whitelist.ts 文件
- 白名单不用验证token的接口
ts
/**
* 接口白名单配置
* 支持精确匹配和前缀通配符匹配
* 优先级:环境变量 > 默认配置
*/
// 默认白名单(所有环境通用)
const DEFAULT_WHITELIST: string[] = [
"/api/login",
"/api/register",
"/api/refresh",
"/public/*",
"/captcha",
];
// 从环境变量读取白名单(逗号分隔)
function getEnvWhitelist(): string[] {
const envList = import.meta.env.VITE_API_WHITELIST;
if (!envList) {
return [];
}
return envList
.split(",")
.map((s: string) => s.trim())
.filter(Boolean);
}
// 合并后的白名单
export const API_WHITELIST: string[] = [
...DEFAULT_WHITELIST,
...getEnvWhitelist(),
];
/**
* 判断路径是否在白名单内
* @param url 请求地址(不含 baseURL)
* @returns 是否放行
*/
export function isWhitelisted(url: string): boolean {
if (!url) {
return false;
}
// 提取路径部分(去掉 baseURL 和查询参数)
const path = url.replace(/^https?:\/\/[^/]+/, "").split("?")[0];
for (const pattern of API_WHITELIST) {
if (pattern.endsWith("/*")) {
// 前缀通配符匹配
const prefix = pattern.slice(0, -2);
if (path.startsWith(prefix)) {
return true;
}
} else {
// 精确匹配
if (path === pattern) {
return true;
}
}
}
return false;
}
/**
* 动态添加白名单(运行时可用)
*/
export function addWhitelist(paths: string | string[]): void {
const list = Array.isArray(paths) ? paths : [paths];
list.forEach((p) => {
if (!API_WHITELIST.includes(p)) {
API_WHITELIST.push(p);
}
});
}
/**
* 动态移除白名单
*/
export function removeWhitelist(paths: string | string[]): void {
const list = Array.isArray(paths) ? paths : [paths];
list.forEach((p) => {
const idx = API_WHITELIST.indexOf(p);
if (idx > -1) {
API_WHITELIST.splice(idx, 1);
}
});
}创建 request.js 文件
ts
/**
* 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";
import { isWhitelisted } from "../config/whitelist";
// -------------------- 默认配置 --------------------
const DEFAULT_CONFIG = {
baseURL: "", // 基础 URL
timeout: 15000, // 请求超时
headers: { "Content-Type": "application/json" },
retry: 2, // 默认重试次数
retryDelay: 300, // 重试延迟(ms)
cacheTTL: 10 * 1000, // GET 缓存有效期(ms)
concurrency: 10, // 并发限制
};
// -------------------- 内部状态 --------------------
interface RefreshQueueItem {
resolve: (value: unknown) => void;
reject: (reason?: any) => void;
config: any;
}
const state: {
authToken: string | null;
refreshTokenFn: null | (() => Promise<{ token: string }>);
isRefreshing: boolean;
refreshQueue: RefreshQueueItem[];
pendingRequests: Map<string, Promise<any>>;
cache: Map<string, { ts: number; data: any }>;
semaphoreCounter: number;
} = {
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: any) {
// 根据请求生成唯一 key,用于去重/缓存
const { method = "get", url = "", params, data } = config;
return [
method.toLowerCase(),
url,
JSON.stringify(params || ""),
JSON.stringify(data || ""),
].join("&");
}
function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
async function retryWithBackoff(fn: any, retries: number, delay: number) {
// 重试逻辑(指数退避)
let attempt = 0;
while (true) {
try {
return await fn();
} catch (err) {
if (attempt >= retries) throw err;
await sleep(delay * 2 ** attempt);
attempt += 1;
}
}
}
async function acquireSlot(maxConcurrency: any) {
// 并发控制(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 验证
const url = config.url || "";
if (isWhitelisted(url)) {
return 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 未授权 --------------------
// 白名单接口的 401 不触发刷新 token,直接返回未授权错误
const reqUrl = original.url || "";
if (status === 401 && isWhitelisted(reqUrl)) {
return Promise.reject(error);
}
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: any, opts: { controller?: any } = {}) {
// 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;
}
// -------------------- 原始请求封装 --------------------
interface RawRequestOpts {
dedupe?: boolean;
cache?: boolean;
cacheTTL?: number;
retry?: number;
retryDelay?: number;
concurrency?: number;
controller?: any;
}
async function rawRequest(config: any, opts: RawRequestOpts = {}) {
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 --------------------
interface CommonOpts {
params?: Record<string, any>;
headers?: Record<string, any>;
signal?: AbortSignal | null;
retry?: number | null;
dedupe?: boolean;
controller?: any;
cache?: boolean;
cacheTTL?: number | null;
}
const api = {
// 基础设置
setBaseURL(url: string) {
instance.defaults.baseURL = url;
},
setTimeout(ms: number) {
instance.defaults.timeout = ms;
},
setAuthToken(token: string) {
localStorage.setItem("token", token);
state.authToken = token;
},
getAuthToken() {
return state.authToken || localStorage.getItem("token");
},
clearAuthToken() {
localStorage.removeItem("token");
state.authToken = null;
},
setRefreshHandler(fn: () => Promise<{ token: string }>) {
state.refreshTokenFn = fn;
},
// 拦截器
addRequestInterceptor(f: any, r?: any) {
return instance.interceptors.request.use(f, r);
},
addResponseInterceptor(f: any, r?: any) {
return instance.interceptors.response.use(f, r);
},
removeInterceptor(id: number, type: "request" | "response" = "request") {
if (type === "request") instance.interceptors.request.eject(id);
else instance.interceptors.response.eject(id);
},
// 原始请求
request(config: any, opts: RawRequestOpts = {}) {
return rawRequest(config, opts);
},
// 常用请求方法
get(
url: string,
{
params = {},
headers = {},
signal = null,
cache = false,
cacheTTL = null,
retry = null,
dedupe = true,
controller = null,
}: CommonOpts = {},
) {
return rawRequest(
{ url, method: "get", params, headers, signal },
{
cache,
cacheTTL: cacheTTL || DEFAULT_CONFIG.cacheTTL,
retry: retry ?? DEFAULT_CONFIG.retry,
dedupe,
controller,
},
);
},
post(
url: string,
data: any,
{
params = {},
headers = {},
signal = null,
retry = null,
dedupe = false,
controller = null,
}: CommonOpts = {},
) {
return rawRequest(
{ url, method: "post", data, params, headers, signal },
{ retry: retry ?? DEFAULT_CONFIG.retry, dedupe, controller },
);
},
put(url: string, data: any, opts: CommonOpts = {}) {
return rawRequest(
{
url,
method: "put",
data,
headers: opts.headers || {},
params: opts.params || {},
signal: opts.signal || null,
},
{
cache: opts.cache,
cacheTTL: opts.cacheTTL ?? DEFAULT_CONFIG.cacheTTL,
retry: opts.retry ?? DEFAULT_CONFIG.retry,
dedupe: opts.dedupe,
controller: opts.controller,
},
);
},
delete(
url: string,
{ params = {}, headers = {}, signal = null }: CommonOpts = {},
) {
return rawRequest({ url, method: "delete", params, headers, signal });
},
// 上传文件
upload(
url: string,
files: any = {},
{
fields = {},
headers = {},
onProgress = null,
signal = null,
controller = null,
}: CommonOpts & {
fields?: Record<string, any>;
onProgress?: ((ev: any) => void) | null;
} = {},
) {
const form = new FormData();
Object.keys(fields || {}).forEach((k) => form.append(k, fields[k]));
if (Array.isArray(files))
files.forEach((f: any) => 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: any) => onProgress(ev) : undefined,
signal,
},
{ dedupe: false, controller },
);
},
// 下载文件
async download(
url: string,
{
params = {},
filename = null,
headers = {},
signal = null,
controller = null,
}: CommonOpts & { filename?: string | 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: string | null = 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;创建 api 文件注意路径需要对上
bash
data 就是 post get 就是 paramsjs
import request from "@/api/request";
export const loginApi = (data) => {
return request({
url: "/api/login",
method: "post",
data,
});
};
export const getList = (params) => {
return request({
url: "/api/getlist",
method: "get",
params,
});
};- 使用
html
<button @click="handleget">测试get接口</button>
<button @click="handlepost">测试post接口</button>
<script>
import { loginApi, loginApi2 } from "@/api/loginapi/loginapi.js";
const handleget = () => {
loginApi2({
username: "admin",
password: "123456",
}).then((res) => {
console.log(res);
});
};
const handlepost = () => {
loginApi({
username: "admin",
password: "123456",
}).then((res) => {
console.log(res);
});
};
</script>