前端开发实战笔记:为什么从 Axios 到 TanStack Query,是工程化演进的必然?

关键词:Axios / TanStack Query / React Query / 前端工程化 / 数据缓存 / 接口抽象 / 并发控制 / 失效策略 / 中后台系统 / 长期维护


引言:从「手写请求」到「数据编排」的转折

有一家做 B 端中后台的团队,技术栈很典型:React + TypeScript + Axios + 自己封一层 request.ts

前两年业务快速增长,迭代飞快,大家都觉得这套组合简单、高效、够用

直到系统规模上来,问题开始密集出现:

  • 同一个接口被不同页面各自请求、各自缓存、各自处理 loading 和错误
  • 列表、详情、弹窗之间的数据不同步,刷新页面才能「自愈」
  • 分页、筛选、轮询、预加载逻辑散落在几十个组件里,没人敢动
  • 埋点、重试、降级、超时策略,一半靠约定,一半靠运气

最后他们不得不做一个决策:把「请求」当成「数据源」,而不是工具函数,从手写 Axios 封装,走向 TanStack Query 这一类「数据获取框架」。

现实里,类似的迁移并不少见:

  • 很多早期用 Axios + Redux-Saga / MobX 的项目,后面把「服务端数据」统一迁到 React Query / TanStack Query
  • 一些重前后端分离的 SaaS,把请求层收口到 Query 层,BFF、前端、移动端共享一套「数据契约」
  • 一些大厂内部中台体系里,Axios 只负责传输,真正的「接口使用规范」全部写进 Query 层
  • 甚至 Vue 生态也有 Vue Query、@tanstack/vue-query,走的是同一条路:从请求工具到数据编排层

问题从来不是「Axios 不行」,而是当系统变大之后,它本来不想管的那些东西,被无限放大

这不是工具之争,而是:你把「服务端状态」当成什么来看。是一次调用,还是一个长期存在、需要被管理的资源。


一、黄金时代:只有 Axios 的那些年

1.1 手写封装带来的掌控感

大部分团队的起点都长这样:

ts 复制代码
// apiClient.ts
import axios from 'axios';

export const apiClient = axios.create({
  baseURL: '/api',
  timeout: 10000,
});

apiClient.interceptors.request.use((config) => {
  // 加 token、traceId、统一 header 等
  return config;
});

apiClient.interceptors.response.use(
  (resp) => resp.data,
  (error) => {
    // 统一错误处理 / 弹 Toast / 上报
    return Promise.reject(error);
  },
);

再往上封一层:

ts 复制代码
// userApi.ts
import { apiClient } from './apiClient';

export const getUser = (id: string) =>
  apiClient.get(`/users/${id}`);

export const updateUser = (id: string, payload: UpdateUserDto) =>
  apiClient.put(`/users/${id}`, payload);

组件里直接用:

tsx 复制代码
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
  getUser(userId).then(setUser);
}, [userId]);
  • 一切都很直观get / post / put,「我调用,我拿结果」。
  • 封装成本很低:几百行就能搞出一套「全局请求体系」。
  • 前期非常高效 :新接口只要在 xxxApi.ts 里加一个函数,回调里写逻辑就完事。

这就是很多团队的「Axios 黄金时代」。

1.2 灵活自由背后的隐形复杂度

问题是,你用 Axios 做了两件事:

  • 把「网络请求」当工具函数用
  • 把「服务端状态」当本地变量用

前期这么干没问题,但这些东西很容易失控:

  • 每个组件都可以随手发请求:useEffect + axios 到处都是
  • loading / error 各写一遍,风格不统一,还不一定都处理
  • 数据生命周期全靠「人脑记忆」:需要刷新就 refetch,不需要就不管

当系统小的时候,这叫「灵活」;当系统大了,这叫「不可预测」。

「用 Axios,你拥有的是自由;

用多了,你发现自由本身就是复杂度。」


二、当系统变大后,一切都变了:请求从「工具」变成「物理规律」

2.1 并发和重复请求:带宽被你自己打爆

典型场景:

  • 页面 A 列表请求 /users
  • 点击某行进入详情页 B,再请求一次 /users/:id
  • 右侧抽屉 C 也要同一个详情,再来一次 /users/:id

如果你只是「在需要的地方发请求」,很快就会出现:

  • 同一资源被多处重复请求:浪费带宽和服务端资源
  • 手动去重很难做统一:各处 useRef + cancalToken,代码风格完全不一样
  • 滚动加载、预加载、轮询、并发组合请求,每一个都是专项工程

本质上:你在「点状」地用 Axios,而不是「面状」地管理数据源。

2.2 数据过期与一致性:什么时候该刷新?

当业务变复杂,你会碰到这些问题:

  • A 页面改了用户信息,B 页面并不知道,要不要刷新?
  • Tab1 改了筛选条件,Tab2 里的图表要不要跟着更新?
  • 详情弹窗关闭后,后面的列表是否需要重查一次?

如果你用的是「组件内发请求 + 本地 state」,这些问题很难统一回答,因为:

  • 没有统一的「资源视角」:系统不知道「当前有哪些资源在用」「哪些已经 stale」
  • 没有统一的「失效策略」 :只能在「某个操作之后」凭经验 reload 一下
  • 没有统一的「观察点」:也就谈不上可观测性和调试

「当你开始问『该不该重新请求?』的时候,

说明你需要的已经不是一个请求库,而是一个数据生命周期管理器。」

2.3 加载与错误状态:被复制粘贴吃掉的可维护性

几乎每个组件里,都能看到类似的模式:

tsx 复制代码
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
  setLoading(true);
  getUser(userId)
    .then(setUser)
    .catch(setError)
    .finally(() => setLoading(false));
}, [userId]);

当你有:

  • 50+ 页面
  • 200+ 接口
  • 10+ 人同时改 UI

你会发现:

  • loading / error 的逻辑到处都是 copy / paste
  • 有人会忘记 .finally,有人会在 error 里直接 message.error
  • 出现「只加载不展示错误」或者「只展示错误不加载」的奇怪状态

这些都是工程债,但在 Axios 这个层面,你几乎无从约束。


三、TanStack Query:克制到近乎啰嗦的艺术

3.1 从「请求一次」到「声明数据源」

TanStack Query(React Query)干的事非常直接:

把「服务端状态」抽出来,变成一个可以被声明、缓存、共享、失效和观察的「资源」。

一个最简单的例子:

tsx 复制代码
import { useQuery } from '@tanstack/react-query';
import { getUser } from '@/apis/user';

function UserDetail({ userId }: { userId: string }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => getUser(userId),
    staleTime: 5 * 60 * 1000, // 5 分钟内视为新鲜,不重新请求
  });

  // ...
}

这里有几个工程上的关键点:

  • queryKey 是资源 ID['user', userId] 把「这个用户」当成一个资源,而不仅是一次调用
  • 缓存与新鲜度有配置staleTime 把「什么时候该重新请求」从组件逻辑里抽出来
  • 加载与错误状态集中管理isLoading / error 的行为在整个项目里趋于一致

这就是它看起来有点「啰嗦」的地方:你必须先把资源「命名」清楚,再去用它

3.2 约束带来的工程收益:去重、缓存、失效统一化

一旦有了统一的 Query 层,你可以获得一堆「不再重复造轮子」的能力:

  • 请求去重 :同一个 queryKey 在同一时间段内多次触发,TanStack Query 会自动合并
  • 缓存复用:页面 A 请求过的用户数据,页面 B 直接复用,甚至都不需要重新发请求
  • 失效策略统一queryClient.invalidateQueries(['user']) 就可以让全局所有用户相关的数据按约定策略刷新
  • 重试 / 超时 / 退避:这些都被放进一套可配置的「物理规律」,而不是一家一个写法

简单对比一下:

tsx 复制代码
// Axios 风格(伪代码)
useEffect(() => {
  getUser(userId).then(setUser);
}, [userId]);

// TanStack Query 风格
const { data: user } = useUserQuery(userId);

useUserQuery 里面可以内置:

  • 缓存时间
  • 失败重试
  • 错误上报
  • 依赖的其他 Query(比如需要先拿到 token)

这些东西一旦配好了,调用方就再也不用关心细节

3.3 把「服务端状态」从本地状态管理里拆出去

很多团队在用 Redux / MobX / Zustand,碰到一个经典问题:

  • 这个 state 是放全局 store 里,还是放组件内?
  • 这个数据是「长期配置」,还是服务端返回的数据快照?

TanStack Query 的核心观点是:

「服务端状态」是一类完全不同的状态。它有自己的生命周期、更新规律和一致性需求。

你不应该用同一套工具(Redux 等)去管理「按钮是否高亮」和「用户列表数据」这两种不同的东西。

更合理的分工是:

  • TanStack Query 管「服务端状态」:接口数据、缓存、失效、预取、并发控制
  • 本地状态库管「客户端状态」:UI 控制、临时表单、拖拽位置、筛选条件等

这样一来:

  • 在 UI 代码里,你看见 Query 基本就知道「这是服务端数据」
  • Query 这一层可以集中做很多「全局优化」,而不用改每个组件

「成熟的工程,不是 state 写在哪,而是:

哪一类 state 有哪一类『物理规律』和『约束』。」


四、技术债 + 业务债的交织:手写请求体系如何拖垮团队

4.1 一次性 Demo 代码,被长期运营拖成泥潭

几乎所有系统都经历过这个阶段:

  • 早期用 Axios 写的「Demo 级」代码,直接被拿来扩展
  • 「先这样写着,后面有时间重构」这句话被说了很多遍
  • 最后大家都默认「那块代码不能动」,因为一动就一堆回归问题

在这个过程中,请求层是最被低估的复杂度来源之一

  • 一开始只是十几个接口,每个组件各写各的
  • 后面变成几百个接口,每个页面都有「自己那一套」请求逻辑
  • 再往后,业务开始反复改版,接口升级 / 废弃交织在一起,你根本不知道哪些 API 还在用

如果一开始就有一个「统一的数据获取层」(哪怕不是 TanStack Query),很多债可以少一半。

4.2 「约定优于配置」的失败:口头规范从来不靠谱

你可能见过这些团队规范:

  • 所有请求都要走 request.ts
  • 不要在组件里直接用 Axios
  • 所有接口都要写在 apis/ 目录下
  • loading 一律用 useLoading 这个 hook

听起来都很合理,但实际执行往往是:

  • 新人 copy 一段旧代码,里面直接 axios.get,没人注意
  • 一些老代码来不及改,request.tsaxios 混用
  • 某些特殊页面自己搞了一套 fetchXXX 工具,理由是「更灵活」

口头约定在 2 个人的项目里还行,在 20 个人的项目里几乎必然失效。

TanStack Query 这种库,本质上是在帮你做一件事:

把「团队约定」写进「可执行的约束」里,而不是写进文档。

比如:

  • 所有读操作必须通过 useQuery / useInfiniteQuery
  • 所有写操作必须通过 useMutation,并且显式写明要 invalidateQueries 哪些资源
  • 所有接口调用都必须声明 queryKey,否则根本没办法用

这就是「硬约束」带来的工程收益。

4.3 可观测性与调试:当问题变成「这条数据是从哪来的?」

大型前端系统里,一个非常典型的排查场景是:

「为什么这个页面上的这个数据不对?」

如果你用的是「到处手写 Axios」的模式,你需要:

  • 在代码里找「谁在请求这个接口」
  • 再找「谁在改这个 state」
  • 再找「是不是某个组件没刷 / 被缓存了」

如果你有 Query 层,你可以:

  • 打开 React Query Devtools / 自己封装的调试面板
  • 看看当前有哪些 Query,在什么时间被请求过
  • 哪些被 mark 为 stale,哪些被 invalidate 过

起码你有一个观察「服务端状态」的统一入口。

「没有可观测性,就谈不上工程化。

你只是在用经验和直觉,硬抗系统复杂度。」


五、为什么即使是顶尖前端团队,仍然会走向 TanStack Query

5.1 并不是「它更潮」,而是「它更可控」

顶尖的前端团队完全可以自己造一套「请求 + 缓存 + 失效 + 并发控制」框架。

很多团队也确实造过,但是:

  • 造出来的框架往往不可移植,换项目就重新造一次
  • 文档跟不上迭代,只剩下口口相传的「内部 best practice」
  • 很多细节(比如 queryKey 命名规范、失效策略组合)没有被沉淀出来

选择 TanStack Query,本质上是:

  • 认同它的「服务端状态 = 一等公民」这个工程观
  • 不想再在「请求层」上无限造轮子,而是把精力放到业务和体验上

5.2 物理规律不可对抗:延迟、抖动、失败率不会因为你用不用 Query 而改变

不管你用不用 TanStack Query,后端系统都会有:

  • 延迟抖动(p99 比 p50 大一个量级)
  • 短暂失败(网络、限流、瞬时错误)
  • 冷启动、缓存失效、突发流量

区别在于:

  • 用 Axios,每个页面都要自己应对这些「物理规律」
  • 用 TanStack Query,这些东西可以被抽象成统一的「策略」

例如:

ts 复制代码
// 全局配置:统一重试策略
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 2,
      retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 10000),
      staleTime: 60 * 1000,
    },
  },
});

你不用再一遍一遍地想:

  • 这个请求要不要重试?
  • 要不要加退避?
  • 要不要缓存?缓存多久?

大部分时候,全局默认值 + 个别 override 就够用了。

「真正拖垮系统的不是单次调用,而是『在真实物理世界中,成千上万次调用的叠加效应』。

TanStack Query 管的是后者。」

5.3 「顶尖」的含义:不是代码写得多,而是系统写得少、错得少

顶尖团队最终关心的是:

  • 团队整体的交付速度,而不是某个接口写得有多花哨
  • 系统整体的可预测性,而不是某个页面跑得有多快

在这个尺度下,TanStack Query 给的是:

  • 更可预测的「接口使用方式」
  • 更可观测的「数据状态」
  • 更容易被新同学理解和上手的「统一模式」

「成熟的工程,最重要的不是『我能做到什么』,

而是『我不需要再手动做到什么』。」


真实案例:从 Axios 到 TanStack Query 的演进之路

案例一:SaaS 中后台,从「到处 useEffect」到集中数据层

  • 原本方案
    React + Axios + 自封装 request.ts,几十个页面里充满了 useEffect + axios 调用,loading / error / empty 各写各的。
  • 遇到的瓶颈
    列表和详情页数据不同步,操作完需要手动刷新;
    重复请求严重,同一个用户详情可以在多个地方被请求 3--4 次;
    新人完全不知道「改哪一块会影响哪些页面」。
  • 迁移后的方案
    引入 TanStack Query,把所有「读接口」统一抽到 queries/ 目录,导出 useUserQuery / useUsersQuery / useProjectQuery 等;
    所有 mutation 都通过 useMutation,在 onSuccess 里统一 invalidateQueries
    收益
    • 接口相关 bug 数量 2 个月内下降约 40%
    • 列表与详情页数据不一致的问题几乎消失
    • 新人从「看不懂请求层」到「半天能上手改业务」

案例二:电商管理后台,从「乐观更新地狱」到统一失效策略

  • 原本方案
    用 Redux 管全局数据,接口调用通过 thunk/middleware 分发,手动写乐观更新逻辑。
  • 遇到的瓶颈
    乐观更新和实际返回不一致时,很难回滚;
    多人同时操作库存 / 价格时,前端状态与后端状态不同步;
    写一个复杂的「批量更新 + 撤销」功能需要在多个 reducer 之间来回跳。
  • 迁移后的方案
    把所有「服务端状态」从 Redux 迁到 React Query,Redux 仅保留 UI 控制和部分客户端配置;
    批量操作用 useMutation + onMutate / onError / onSettled 写出标准化的乐观更新模板。
    收益
    • 大部分乐观更新逻辑可以被复用
    • 整体代码量减少约 20%(大量重复 case 被删掉)
    • 回滚逻辑更集中,可测试性提高

案例三:内部运营工具,从「API 文档 != 真实使用方式」到「查询即文档」

  • 原本方案
    有 Swagger 文档,但每个前端项目(PC / H5 / 内嵌)各封一套 API,并各自拼装不同的查询参数。
  • 遇到的瓶颈
    文档更新和前端调用方式不同步;
    很多接口有奇怪的「隐形字段」只有一个团队知道;
    调试时经常需要「抓一遍请求」才能搞清楚到底传了啥。
  • 迁移后的方案
    以 TanStack Query 为中心,把每一个查询封成一个 useXXXQuery,这个 hook 本身就约束了参数、返回类型和使用场景
    给业务方的文档不再是「接口列表」,而是「可复用的查询能力」。
    收益
    • 部门间的协作变成「对齐查询定义」而不是「对齐接口字段」
    • 新增一个使用场景,只要复用已有的 Query 或在其基础上轻量扩展

结论很一致:Axios 很好用,但不适合扛大梁。
真正扛大梁的,是围绕它之上的那层「数据编排和工程约束」。


最合理的平衡:分层协作,而非替代

与其讨论「要不要抛弃 Axios」,不如把整个栈分层看:

阶段 / 层级 推荐技术 / 方案 设计目标
原型期 / 小项目 Axios + 轻量封装(request.ts + 少量 hooks) 快速试错,低心智负担
成长期中后台 Axios + TanStack Query(或 Vue Query 等) 把服务端状态抽象成「资源」
成熟期 / 多团队协作 Axios + TanStack Query + 约定式 queryKey / 目录规范 统一数据访问模式,降低协作成本
极致性能 / 特殊场景 Axios + TanStack Query + 自定义缓存层 / Streaming 方案 精细控制缓存、带宽与并发
渐进式改造遗留项目 保留原 Axios,新增功能全部走 TanStack Query 避免大手术式重构,渐进演进

再细一点到前端内部职责:

前端层级 推荐角色分工 关键词
请求传输层 Axios / fetch / 自研 HTTP 客户端 传输、重试、统一 header
数据获取编排层 TanStack Query / Vue Query 缓存、去重、失效、并发控制
业务服务层(hooks) useUserService / useOrderService 聚合多个 Query + 业务规则
UI 组件层 只消费 hooks 暴露的数据和状态 关注展示与交互,不关心请求细节

不是「Axios vs TanStack Query」,而是:
Axios 负责「怎么发」,TanStack Query 负责「怎么用」,业务 hooks 负责「用来做什么」。


总结:从 Axios 到 TanStack Query,从工具到工程

  • Axios 代表局部的自由与掌控,TanStack Query 代表全局的约束与可预测性
  • 问题不是 Axios 不行,而是当系统变大时,服务端状态的「物理规律」被无限放大
  • 顶尖团队真正关心的是「系统写得少、错得少」,而不是「这个请求写得多帅」
  • 成熟的前端架构,不再把「请求」当成一个工具调用,而是当成一个长期存在、被管理的资源
  • 最合理的方案从来不是「替代」,而是「分层协作」:请求传输层、数据编排层、业务服务层各司其职。

Axios 是起跑时的加速器,TanStack Query 是长跑时的配速器。
前者帮你快速上路,后者让你跑到终点。
选择技术,其实是在选择你如何对待复杂度:是把它摊在每个组件里,

还是收口到一层可以被观察、被约束、被演进的工程抽象里。


尾声:技术的尽头,是工程哲学

  • 稳定靠可预测性:同一类问题,在系统里应该有同一类解决方式。
  • 团队靠约束:越是大型系统,越需要把「约定」变成「可执行的规则」。
  • 成熟体现在写得少、错得少 :少写一次 useEffect + axios,多用一次 useXXXQuery,就是把复杂度搬离业务代码一步。

Axios 让你随时可以写一行请求代码,TanStack Query 让这行代码背后有一整套「物理规律」兜底。

成熟的团队,既懂得什么时候该追求自由,也懂得什么时候该把自由托付给框架。

相关推荐
ywf12151 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭2 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf8 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特8 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷8 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian9 小时前
前端node常用配置
前端
华洛9 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq9 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A10 小时前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常10 小时前
被EdgeToEdge适配折磨疯了,谁懂!
前端