前端开发实战笔记:为什么从 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 让这行代码背后有一整套「物理规律」兜底。

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

相关推荐
面向div编程6 分钟前
Vite的知识点
前端
疯狂踩坑人9 分钟前
【前端工程化】一文看懂现代Monorepo(npm)工程
前端·npm·前端工程化
JarvanMo12 分钟前
Flutter:如何更改默认字体
前端
默海笑13 分钟前
VUE后台管理系统:定制化、高可用前台样式处理方案
前端·javascript·vue.js
YaeZed20 分钟前
Vue3-toRef、toRefs、toRaw
前端·vue.js
用户66006766853920 分钟前
CSS定位全解析:从static到sticky,彻底搞懂布局核心
前端·css
听风说图21 分钟前
Figma Vector Networks: 形状、填充及描边
前端
hanliu200325 分钟前
实训11 ,百度评分
前端
Y***K43430 分钟前
TypeScript模块解析
前端·javascript·typescript