
上上个月,我在我们前端团队的工程规范文档里加了一条规则:新项目禁止引入 axios,老项目在下一次重构周期内逐步移除。
这条规则刚发出去,内部群就炸了。
有人说:用了好几年了,挺稳的,换个锤子啊🔨
也有人发了个表情包,配文:你这不是在革我们的命吗😢?
我理解这些反应。axios 在国内前端圈子里几乎是请求库的代名词。很多人入行第一天就在用,它已经不是一个技术选型的问题了。
但当我拆开这个项目的 node_modules 看了看,然后再打开 MDN 看了看 2026 年的 fetch API,我确认了一件事:大多数项目里的 axios,已经从解决问题的工具变成了不敢删的历史遗留。
今天我就把决策过程摊开,给你们讲一讲👇。
axios 到底解决了什么?那些问题今天还存在吗?
先说公道话,axios 当年确实有它存在的理由。
2016 年前后,原生 fetch 刚落地,基本上属于能用但不好用的状态:不支持请求超时、不支持拦截器、不支持进度监控、不能自动解析 JSON、4xx 和 5xx 不会走 catch。那个年代你要发一个 HTTP 请求,fetch 写起来确实又臭又长。
但 2026 年了,浏览器的 fetch 已经不是当年那个半成品了:

你仔细看这张表。axios 最核心的卖点------拦截器、超时、取消------要么已经被原生 API 覆盖,要么用不到 10 行封装就能实现。 而 axios 做不到的事情------流式响应(SSE、大文件流)------反而是 2026 年越来越多的业务场景真正需要的能力。
axios 在我们项目里制造的 3 个真实问题
我不是因为原生党的洁癖才做这个决定的。
拦截器嵌套成了调试黑洞
我们项目里有 4 层拦截器:鉴权注入、日志埋点、错误统一处理、灰度标记。每一层都在 request 和 response 上各挂了一个拦截器,总共 8 个。

当一个请求出了问题,你去调试的时候会发现:请求从你的业务代码发出去之后,要经过 4 层拦截器的变形,才真正到达网络层。 响应回来之后,又要经过 4 层拦截器的加工,才到达你的 .then()。中间任何一层吞掉了错误、改了 header、或者忘了 return,你都极难定位🤷♂️。
CancelToken 已废弃,但代码库里到处都是
axios 的 CancelToken 在 v0.22.0 就被标记为 deprecated 了,官方推荐改用 AbortController。但我们项目里有几十处 CancelToken 的用法,没人敢动,因为改了怕出事,不改跑着也没问题 。结果就是新代码用 AbortController,老代码用 CancelToken,两套取消机制并存,维护成本直接翻倍 😖。
在 Edge Runtime 和 SSR 场景下踩坑
我们有一部分接口走 Cloudflare Workers。Workers 的运行时是 V8 Isolate,原生支持 fetch,但不支持 XMLHttpRequest 。而 axios 在 Node 环境下底层走的是 http 模块,在浏览器环境下走的是 XMLHttpRequest------两个都不是 Workers 原生支持的。
为了让 axios 在 Workers 里跑起来,我们不得不引入适配层。这完全是在为一个本不需要的抽象层擦屁股。
只需要 30 行代码,替掉整个 axios
下面是我们团队现在用的请求封装。没有花哨的设计模式,就是一个纯函数,逻辑全部可见,没有隐藏的拦截器链:
typescript
// lib/request.ts
interface RequestConfig extends RequestInit {
timeout?: number;
params?: Record<string, string | number | boolean>;
}
interface ApiResponse<T = unknown> {
code: number;
data: T;
message: string;
}
class RequestError extends Error {
status: number;
response: Response;
constructor(message: string, status: number, response: Response) {
super(message);
this.name = 'RequestError';
this.status = status;
this.response = response;
}
}
const BASE_URL = import.meta.env.VITE_API_BASE ?? '';
export async function request<T = unknown>(
url: string,
config: RequestConfig = {},
): Promise<ApiResponse<T>> {
const { timeout = 10_000, params, headers, ...rest } = config;
// 拼接查询参数
const queryString = params
? '?' + new URLSearchParams(
Object.entries(params).map(([k, v]) => [k, String(v)])
).toString()
: '';
// 超时控制
const signal = config.signal ?? AbortSignal.timeout(timeout);
// 鉴权注入
const token = localStorage.getItem('access_token');
const mergedHeaders: HeadersInit = {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers,
};
const response = await fetch(`${BASE_URL}${url}${queryString}`, {
...rest,
headers: mergedHeaders,
signal,
});
// fetch 不会在 4xx/5xx 时 reject,需要手动处理
if (!response.ok) {
throw new RequestError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
response,
);
}
return response.json() as Promise<ApiResponse<T>>;
}
业务代码的调用方式:
typescript
// 普通 GET
const { data: userList } = await request<User[]>('/api/users', {
params: { page: 1, size: 20 },
});
// POST
const { data: result } = await request<CreateResult>('/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'ErpanOmer', role: 'admin' }),
});
// 带手动取消的请求(用于搜索框等竞态场景)
const controller = new AbortController();
const { data } = await request<SearchResult[]>('/api/search', {
params: { keyword: 'react' },
signal: controller.signal,
});
// 需要取消时
controller.abort();
对比一下改造前后的依赖变化:
diff
// package.json
{
"dependencies": {
- "axios": "^1.7.2",
- "axios-retry": "^4.1.0",
}
}
两个包删掉,gzip 后减少约 7 KB 。7 KB 听起来不多,但如果你的项目还挂着 lodash、moment、antd 全量引入,这种顺手减负积少成多,对移动端首屏性能的贡献是实打实的🫡。
fetch 封装必须踩得坑,讲清楚
别急着全盘照抄上面那 30 行。原生 fetch 有几个坑,如果你不处理,上了生产一定会翻车。
fetch 只在网络层失败时 reject
这是 fetch 和 axios 最大的行为差异 。axios 在收到 4xx / 5xx 响应时会直接走 catch,但 fetch 不会。对 fetch 来说,只要服务端返回了响应(哪怕是 500),就算请求成功。

所以上面那段 if (!response.ok) 的判断是生命线 ,绝对不能省。漏了这一步,你的 try/catch 会安静地吞掉所有服务端错误,页面一片祥和,数据全是空的 🤷♂️。
Token 过期后的无感刷新
如果你的系统有 Token 刷新机制,在 axios 里通常是挂一个 response 拦截器来做。在 fetch 封装里同样可以实现,而且逻辑更加透明:
typescript
// 在 request 函数内部,response.ok 判断之前加入
if (response.status === 401) {
const refreshed = await tryRefreshToken();
if (refreshed) {
// 用新 Token 重发原请求
const newToken = localStorage.getItem('access_token');
const retryHeaders = { ...mergedHeaders, Authorization: `Bearer ${newToken}` };
const retryResponse = await fetch(`${BASE_URL}${url}${queryString}`, {
...rest,
headers: retryHeaders,
signal, // 注意:复用原来的 signal,保证外层取消仍然生效
});
if (!retryResponse.ok) {
throw new RequestError('Retry failed', retryResponse.status, retryResponse);
}
return retryResponse.json() as Promise<ApiResponse<T>>;
}
// 刷新失败,踢回登录页
window.location.href = '/login';
throw new RequestError('Session expired', 401, response);
}
这段逻辑在 axios 的拦截器里也能做,但区别是:拦截器里的逻辑是隐藏的,你需要打开一个单独的文件去找;而函数封装里的逻辑是线性的,你从上往下读就能看到完整的请求生命周期。
并发请求时的 Token 刷新竞态
如果页面同时发了 5 个请求,其中第 1 个发现 Token 过期开始刷新,此时第 2 到第 5 个请求怎么办?如果它们也各自触发刷新,就会同时发出 5 个 refresh 请求,导致旧的 Refresh Token 被覆盖、多端登录态混乱。
兜底方案是加一个刷新锁:
typescript
let refreshPromise: Promise<boolean> | null = null;
async function tryRefreshToken(): Promise<boolean> {
// 如果已经有一个刷新请求在飞,复用它的 Promise
if (refreshPromise) return refreshPromise;
refreshPromise = (async () => {
try {
const res = await fetch(`${BASE_URL}/api/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refreshToken: localStorage.getItem('refresh_token'),
}),
});
if (!res.ok) return false;
const { data } = await res.json();
localStorage.setItem('access_token', data.accessToken);
localStorage.setItem('refresh_token', data.refreshToken);
return true;
} catch {
return false;
} finally {
refreshPromise = null; // 刷新完成,释放锁
}
})();
return refreshPromise;
}
这段代码保证:无论同时有多少个请求触发了 401,refresh 只会发一次,所有后续请求都在等同一个 Promise。 这一层在 axios 拦截器里同样需要自己写,它并不会帮你解决。
AbortSignal.timeout() 的浏览器兼容
AbortSignal.timeout() 是 2022 年才进入规范的 API。在极少数需要兼容老旧 WebView(比如部分低端安卓设备的内嵌浏览器)的场景下,需要手动 polyfill:
typescript
function createTimeoutSignal(ms: number): AbortSignal {
// 优先使用原生实现
if ('timeout' in AbortSignal) {
return AbortSignal.timeout(ms);
}
// 降级方案
const controller = new AbortController();
setTimeout(() => controller.abort(new DOMException('Timeout', 'TimeoutError')), ms);
return controller.signal;
}
绝大多数现代浏览器已经支持,但兜底一定要写!
什么时候 axios 仍要使用?
我不是要把 axios 一棒子打死。以下场景它仍然有优势:

- 上传进度监控 :
fetch目前没有原生的上传进度 API(下载进度可以用ReadableStream),如果你的业务有大文件上传且需要显示进度条,XMLHttpRequest的upload.onprogress目前仍然是唯一方案。 - 遗留项目迁移成本过高 :如果你的项目有上百处
axios调用且工作良好,纯粹为了干净去替换,投入产出比不划算。规范约束新代码即可。
但对于 2026 年的新项目来说,fetch 就是那个你不需要安装的、零成本的、全运行时兼容的标准答案。
好了,今天就分享到这里🙌
喜欢我的文章,也欢迎关注我的微信公众号:【前端技术官】。
主要分享:前端架构 · AI 编程 · 职场认知 · 开发者成长

微信扫码关注 👆
不定期更新,不刷屏,聊点真正有用的干货。