在中后台管理系统场景下,最近在探索 Vue 3 项目前端架构演进时,在考虑一个问题:要不要引入 TanStack Query(以下简称 TQ)?
网上铺天盖地的推荐、官网炫目的例子、社区的“现代数据获取标配”标签,让人感觉不上 TQ 就落伍了。但经过一轮又一轮的推敲,我们得出了一个和主流声音不太一样的结论:在典型的中后台业务里,你可能真的不需要它。
甚至,把它的 Cache 能力直接引入,反而会破坏架构的纯粹性。
一、 TQ 确实有闪光点
它的 loading / isPending / isFetching 等声明式状态机制非常优雅,几乎消灭了手动管理 loading 变量的模板代码。自动去重(Deduplication)、防抖、后台静默刷新等特性,在高交互、频繁切换的消费级应用(如 Twitter、在线协作文档)里确实很香。
但这些优点主要服务于“重交互、实时性强”的场景。而中后台 90% 的页面是 CRUD + 列表/表单,核心流程是:打开列表 → 编辑 → 返回并刷新。在这种“拉取-显示-操作”的线性生命周期里,TQ 解决的问题往往不是我们的痛点,反而引入了新的麻烦。
二、 核心矛盾:缓存究竟该归谁管?
这是我们拒绝 TQ 最根本的架构理由。我们需要明确两个原则:
1. 缓存层应该是尽可能少的
在复杂的工程中,数据流经的层级越多,维护成本呈几何倍数增长。一旦你在 Service/Api 层已经有了状态缓存,再引入 TQ 缓存就会形成双缓存地狱:两个系统同时接管同一份数据。
当数据不一致或更新失效时,你很难分清是 Service 层的处理滞后,还是 TQ 的 staleTime 没设对。这种排查过程极度痛苦,本质上是架构层面的职责重叠。
2. 缓存属于“底层基础设施”,而非“UI 业务逻辑”
缓存其实是后端数据的前端镜像,它天生属于数据层(Service/Api)。
TQ 的设计哲学是让上层(业务组件)通过 queryKey 来感知并操作缓存。这在小项目里很灵活,但在中大型团队里却是一种严重的抽象泄露(Leaky Abstraction) :
- QueryKey 维护灾难: 开发者需要在组件里自由定义 Key。不同页面拼出的 Key 稍有差异,就会导致缓存无法共享或
invalidate失败。 - 工程不可控: 你无法限制业务开发者如何定义 Key。在大规模使用后,往往只能靠人工 Review 这种低效手段来兜底。
TQ 的核心维护者也在博客中提及,在大型项目中建议集中维护 QueryKey:tkdodo.eu/blog/effect…
结论: 业务组件的唯一职责是“消费数据”,它不该关心数据是从内存里拿的还是网络里拉的,更不该亲手去管理那个复杂的 queryKey。一旦我们同意缓存不该由 UI 层来调度,那么缓存机制就必须向下沉淀。
如果用集中维护 QueryKey 的方式去做 QT 的缓存,本质和在数据层做缓存没有区别,但还在 UI 层配置,整体结构上也显得奇怪。
三、 基础设施的终极形态:透明化
既然缓存被按回了底层,它应该以什么样的姿态存在?在这一点上,经典的计算机系统设计早就给过我们启发:最完美的缓存,是完全透明的。
就像操作系统的 Page Cache 或 CPU 的 L1/L2 Cache:上层应用程序只管发起单纯的 read() 或 write() 指令,内核在底层默默地把缓存调度干好。你永远不需要在业务代码里手动声明:“请把这段逻辑存进二级缓存,Key 叫做 my-data-key”。
中后台的 Api 层缓存也理应如此。它应该像水和空气一样存在于底层的请求管线中,而不是作为一个“高调的工具库”跳到业务组件面前指手画脚。
四、 设想的方案:在 Service 层做声明式配置
对于后端的一个数据是否过期,常见的其实就两种场景:
- 明确知晓某个操作导致了这个数据的过期(本质是前端触发了某个写操作)
- 不知道什么操作导致过期,定义个自身可接受的缓存时间
// 接口定义层(Service 层)
const userApi = {
getList: {
url: '/api/users',
cache: {
ttl: 30_000, // 30秒透明缓存
// 关键:声明哪些接口操作会触发该接口失效
invalidatedBy: ['/api/user/save', '/api/user/delete']
}
},
save: {
url: '/api/user/save',
}
}
框架内部实现极简:
- 请求时: 自动检查该接口是否有未过期的缓存。
- 写操作成功时: 根据
invalidatedBy自动批量失效相关缓存。
对业务层而言:
// 业务组件层
// 开发者完全感知不到缓存的存在,不需要管 Key,不需要手动刷新
const { userList, queryUserListloading } = useAsyncData('userList' userApi.getList);
-
通过
vue-asyncx的useAsyncData单独使用loading、聚焦自动刷新、防抖节流等能力 -
因为所有失效场景(时间过期 + 相关写操作触发)都已经在 Service/Api 层自动处理好了,业务层几乎不再需要
refresh函数,也不需要手动invalidateQueries。-
在大型项目(数百个页面),长期维护(5年甚至更长时间),多人协作(一份代码历经多人接收)的限制下,业务侧的代码应该尽可能简单,业务侧需要理解的概念应该尽可能的少。
-
在业务侧代码可能由 AI 接手的背景下,拿走一种变化的维度,长期看系统应该是更优的。
-
五、 什么时候才可能需要 TQ?
只有在以下极少数例外场景,TQ 的收益才会超过它的架构副作用:
- 极致的实时交互: 如协同编辑器、大型画布、需要频繁乐观更新(Optimistic Updates)的场景。
总结
TanStack Query 是一个伟大的产品,但它解决的是“状态同步”的难题。而中后台列表/表单业务的核心痛点是**“工程的可预测性”和“开发效率”**。
把复杂性留在框架底层,保持业务层的纯净,将缓存彻底做成透明的实现细节——这才是对大型团队长期维护更负责的设计。
你可能真的不需要 TanStack Query。 至少,在大多数后台管理系统中,它的缓存功能不该出现在业务代码的第一线。