前端请求方案选型:fetch、Axios 与服务端状态管理
用户列表页需要加载数据,搜索框需要在输入变化后重新请求,编辑资料后还要更新详情和列表。第一次实现时,每个功能都可以从一个 fetch() 开始;等这些功能放到同一个应用里,问题就逐渐变成了:两个组件会不会重复请求,页面回来时能不能先显示旧数据,保存成功后哪些地方需要更新。
这些问题经常被一起归进「请求封装」。于是,原生 fetch、Axios、React Query、SWR、各种 useRequest 都出现在同一份选型清单里。它们确实都和请求有关,但承担的职责不同。底层换成 fetch,可以改变请求客户端的实现;数据的共享、过期和修改后的同步,还需要另一组规则。
《为什么我在团队里禁止使用 axios?》提出了一个值得讨论的方向:现代 fetch 已经能够承担大量业务请求,项目可以重新评估对 Axios 的依赖。不过,是否使用 Axios、是否自己维护通用请求封装、是否引入服务端状态管理,是可以分别作出的决定。每项决定都对应不同的收益与维护责任。
能力与文档核对于 2026-10-21。示例围绕现代 Web 应用,TanStack Query 使用 v5 API;旧 WebView、小程序、React Native 和不同服务端运行时,需要另外核对对应适配器。代码片段用于解释职责和关键行为,不是一套可以覆盖全部接口协议的通用请求实现。
发出请求与管理数据,是两组职责
以读取用户资料为例,浏览器原生 API 的直接写法很短:
const response = await fetch('/api/users/42', {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const user = await response.json();
这里完成了网络请求、HTTP 状态检查和响应解析。fetch 在收到 404、500 等 HTTP 响应时不会自动 reject,请求函数需要决定哪些状态应当转成错误;网络失败、取消以及后续的解析失败,也可能使相应 Promise 拒绝。Fetch 的错误与响应处理规则解释了这些阶段的区别。
把它放进组件后,还需要决定请求何时执行,加载期间展示什么,失败后能否重试。再增加一个同时展示相同用户的组件,就会碰到数据共享和请求去重。编辑完成后,详情与不同筛选条件下的列表又可能同时过期。
下图展示一种常见的职责分层,箭头表示调用关系。简单页面可以直接调用请求函数;需要共享缓存的应用,再让数据管理层承接组件订阅。
请求客户端负责一次 HTTP 交互,包括 URL、请求头、请求体、响应解析、超时、取消,以及约定好的错误转换。原生 fetch、Axios、Ky、ofetch 都可以参与这一层;后三者会提供更多现成行为。
异步操作管理负责执行时机和执行状态,包括自动或手动调用、loading、error、防抖、节流、轮询等。React 的 ahooks useRequest、VueRequest 和 VueUse 的部分工具,适合从这类需求切入。
服务端状态管理负责远程数据在前端的副本,包括缓存身份、新鲜程度、跨组件订阅、后台刷新,以及写操作后的更新与失效。TanStack Query、SWR、Pinia Colada 和 RTK Query 都覆盖了这类问题。
这里的服务端状态指由远程系统持有、前端通过异步操作获取和修改的数据。用户资料、订单列表、库存都属于这一类。弹窗是否打开、搜索框尚未提交的输入、编辑中的表单,则通常属于客户端状态。
这些分类允许能力重叠。useRequest 也可能提供缓存,Query 库也会管理 loading 和轮询。比较时应当看项目需要哪组行为,而不能只凭函数名判断。
fetch 封装与成熟请求客户端
现代 fetch 提供了统一的请求与响应模型,能够配合 AbortController、AbortSignal、Headers、URL 和 Streams API 使用。对接口数量少、协议稳定的应用,一层范围明确的封装可以很合适。需要评估的是封装承诺的范围,以及范围扩大以后谁来维护边界。
一份简短封装隐含了哪些接口约定
下面这些行为看起来都很普通,却对应不同的契约:
| 封装中的默认行为 | 它隐含的约定 |
|---|---|
所有响应都调用 json() |
每个成功响应都有合法 JSON |
所有请求都设置 JSON Content-Type |
请求体都按 JSON 发送 |
自动返回 body.data |
所有接口共享同一种响应外层 |
| HTTP 200 就返回成功 | 业务失败不会放在成功状态码里 |
| 超时或错误后重发 | 请求允许被再次执行,请求体也能再次发送 |
| 自动读取浏览器存储中的 Token | 调用发生在浏览器,且认证确实采用这一来源 |
这些约定可以成为项目规范,但不宜静默扩展到文件下载、204 空响应、FormData、SSE 和服务端请求。比如,用 response.json() 包装所有请求,会在解析前等待整个 JSON 响应完成;要逐步消费流式内容,需要使用响应体的 reader 或对应的流解析器。
请求头也有真实的数据结构。RequestInit.headers 接受普通对象、键值对数组和 Headers 实例,直接用对象展开合并,并不能正确覆盖所有输入。可以先通过 new Headers(input) 归一化,再使用 set()、has() 等方法操作。上传 FormData 时,应让浏览器生成 multipart 的 Content-Type 和 boundary。FormData 的发送说明明确提醒了这一点。
错误通常也需要区分层次。网络失败表示没有获得可用响应;HTTP 错误表示响应状态不符合约定;业务错误可能藏在 HTTP 200 的 JSON 中;解析或校验失败则说明响应不能按预期消费。取消请求是否展示错误提示,又是界面的选择。请求层可以把这些差异转换成稳定错误对象,界面根据错误类别决定交互。
TypeScript 泛型只影响开发阶段的类型判断。request<User>() 或 response.json() as Promise<User> 都不会在运行时检查服务端是否真的返回了 User。如果外部响应的可信度影响后续逻辑,可以在接口函数边界加入 schema 校验,而不是把泛型当作校验结果。
超时、取消和旧结果覆盖
下面这行代码很容易出现在自建封装里:
const signal = config.signal ?? AbortSignal.timeout(10_000);
它表达的是二选一。调用方一旦传入 signal,默认超时就不再参与。现代运行时可以使用 AbortSignal.any()组合取消来源:
const timeoutSignal = AbortSignal.timeout(10_000);
const signal = config.signal
? AbortSignal.any([config.signal, timeoutSignal])
: timeoutSignal;
const response = await fetch(url, { ...config, signal });
这里的 config 指标准 RequestInit,只展示信号组合。需要兼容旧运行时时,应核对 timeout()、any() 的支持范围;使用定时器兜底的实现,还要处理定时器和事件监听的清理。AbortSignal.timeout()采用 active time 语义,浏览器暂停页面等情况也会影响计时,不能把它理解成服务端业务执行时限。
搜索框还会遇到另一个问题。搜索词 A 的请求先发出,搜索词 B 后发出;如果 A 最后返回,旧结果可能覆盖新结果。取消 A 可以减少无用工作,界面仍然需要明确结果属于哪个搜索条件。查询 key、执行序号或其他身份判断,负责保护结果归属。
调用某个工具返回的 cancel(),也不能仅凭名字就认定网络已经中断。它可能取消尚未执行的防抖任务,也可能让当前结果不再生效;真正终止 fetch,需要把对应 signal 传到网络调用。TanStack Query 的取消文档同样区分了默认缓存行为和消费取消信号后的行为。前端中断网络等待,也不代表服务端已经撤销写操作。
Token 刷新仍然需要系统自己的规则
几个请求同时返回 401 时,复用一个正在进行的 refresh Promise,可以减少重复刷新。这个机制适用于 fetch,也适用于 Axios 拦截器。
不过,「当前有刷新任务就等待它」只处理了重叠发生的刷新。假设请求 A 已经完成刷新,刷新锁也释放了,请求 B 携带旧 Token 的 401 此时才到达。B 如果只看锁是否为空,就可能再次刷新。请求层还需要比较发出请求时的认证版本与当前认证版本,判断是否已经有更新的凭据可用于一次重发。
认证链路也应明确刷新失败怎样结束会话、原请求最多重发几次、取消与总耗时怎样计算,以及请求体是否可重放。JSON 字符串可以重新发送,已经消费的流式请求体通常不能直接复用;Fetch 对请求体的说明给出了只能消费一次的例子。
服务端渲染时,认证来源和刷新协调要绑定当前请求或会话。浏览器里的 localStorage、模块级的单个 refresh Promise,都不能直接照搬成服务所有访问者的认证实现。选择哪个 HTTP 客户端,并不会替项目完成这些系统约束。
Axios 也可以使用 fetch
Axios 的 fetch adapter 从 v1.7.0 就已经加入,可以明确选择:
import axios from 'axios';
const client = axios.create({ adapter: 'fetch' });
const response = await client.get('/api/events', {
responseType: 'stream',
});
这个片段只说明适配器与响应类型,流的消费和 SSE 事件解析需要继续实现。stream 是否可用,仍取决于对应环境。把 Axios 一概描述成只支持浏览器 XHR 和 Node HTTP,会漏掉当前能力;它也已经支持通过 signal 接入 AbortController。
拦截器是否容易调试,则取决于职责组织。鉴权、日志、灰度和错误转换放进多个隐式拦截器里可能混乱,全部写进一个很长的 fetch 函数也可能混乱。明确执行顺序、保留错误原因、限制响应变形,比简单统计拦截器数量更有帮助。
Ky、ofetch 和自有封装的取舍
基于 fetch 的成熟客户端提供了一个折中方案。项目继续采用 Web 标准的请求模型,同时复用通用能力。
- Ky 提供 JSON 请求选项、HTTP 错误处理、超时、重试、hooks 和可配置实例。它适合希望使用 fetch 语义、又不想反复维护通用行为的现代 Web 项目。采用时要按安装版本阅读文档,尤其注意 hooks 和 URL 选项的版本变化。
- ofetch 提供自动响应解析、
baseURL、query、超时和请求生命周期拦截器。它与 Nuxt 的技术生态接近,也可以独立使用。偏好直接返回解析结果、集中配置请求默认值的项目,可以优先评估。 - Axios 适合已经有统一规范、插件依赖或适配需求的团队。浏览器上传进度需要按适配器与目标环境验证;如果明确需要 XHR 的上传进度事件,可以保留相应请求通道。
- 原生 fetch 加自有封装 适合约定很窄、依赖预算明确、维护责任清楚的项目。接口类型增加时,重新评估封装范围,不必持续把所有能力都扩充进同一个函数。
这些客户端默认行为并不一致。例如 ofetch 默认会对符合条件的失败请求重试,但默认不重试 POST、PUT、PATCH、DELETE;Ky 也有自己的重试方法与状态码集合。选型时应阅读对应版本的默认值,而不是假设引入库后行为仍等同于一次原生 fetch。
项目再包一层时,可以只保留自己的 API 地址、认证接入、业务错误和观测约定。缓存订阅、页面 loading、弹窗提示等职责各有自己的使用场景,不宜继续集中到请求客户端中。
服务端状态管理增加了什么
一个请求函数可以被任意组件调用,但组件各自调用时,并不会自然共享结果。请求完成后把对象存在某个全局变量里,也还没有定义新鲜度、订阅、刷新和释放规则。服务端状态库把这些行为组织成可配置的缓存模型。
查询 key 定义数据身份
以 TanStack Query 为例,query key 是一份查询结果的身份。相同 QueryClient 中,使用相同 key 的查询可以订阅同一份缓存;影响结果的条件要进入 key。查询 key 文档要求把查询函数依赖的变量包含在其中。
const detailKey = ['users', 'detail', '42'];
const activeListKey = ['users', 'list', { status: 'active', page: 1 }];
const disabledListKey = ['users', 'list', { status: 'disabled', page: 1 }];
这三份数据有关联,却有不同身份。只用 ['users'] 缓存所有请求,会把详情、分页和不同筛选条件混在一起。语言、租户或当前账号会改变结果时,也应有明确的缓存隔离策略;认证凭据本身不适合作为缓存 key,账号切换时则需要处理旧的受保护数据。
相同 key 能复用正在进行的查询,并不表示任意时间再次订阅都不会产生新请求。是否重新获取,还取决于数据新鲜度、挂载与刷新策略等条件。
新鲜度与缓存回收是两个时间
缓存存在时,界面可以先使用已有数据,再决定是否需要重新验证。下图展示常见的过期后重新验证过程;轮询、显式刷新和特殊配置需要另外考虑。
staleTime 定义数据的新鲜期。在通常的配置下,新鲜期内不需要因为普通的挂载或窗口聚焦再次验证;过期以后,缓存可以继续展示,只是在符合触发条件时允许重新获取。时间走完本身不会创建一个周期请求,定时刷新由 refetchInterval 之类的配置负责。
gcTime 定义无订阅者的缓存保留时间。当某份查询没有活跃订阅者后,它才进入这类回收计时。它控制后续还能否复用缓存,不负责判断数据是否新鲜。
例如,设置 staleTime: 30_000、gcTime: 5 * 60_000,含义是数据通常在 30 秒内视为新鲜,没有订阅者后缓存可以保留 5 分钟。离开页面 40 秒后返回时,旧缓存可能还在,界面可以先展示它,再按策略后台刷新。这不等于每 30 秒自动请求,也不等于 30 秒后删除数据。
TanStack Query v5 的默认设置比较积极,查询数据默认视为 stale,无活跃订阅者的查询默认在 5 分钟后回收;重新挂载、窗口重新聚焦和网络恢复,可能触发过期查询的后台刷新。项目应根据数据变化频率和请求成本配置这些行为。默认值文档还区分了轮询与新鲜度的关系。
已有数据时的后台刷新,也应与第一次加载分开。第一次没有可展示数据,可以使用骨架或空白加载态;已有数据时,通常保留内容并提示正在刷新。后台刷新失败是否继续展示旧数据,要结合业务有效性决定,不能用一个 loading 布尔值把内容反复清空。
Query 缓存与 HTTP 缓存各有作用
浏览器可能已经按 Cache-Control、ETag 等机制缓存 HTTP 响应,Query 库仍然有用,因为两层缓存管理的对象不同。
| 机制 | 管理的对象 | 典型行为 |
|---|---|---|
| HTTP 缓存 | HTTP 响应及验证信息 | 强缓存、条件请求、304 |
| Query 缓存 | 应用查询结果与订阅状态 | 共享数据、标记过期、后台刷新 |
| 客户端状态 | 应用本地交互数据 | 表单草稿、弹窗、未提交筛选 |
Query 层判断需要重新获取后,底层请求仍可能命中浏览器 HTTP 缓存。反过来,浏览器有缓存也不会自动告诉应用:保存用户资料以后,哪些列表应标记过期。HTTP 缓存的命中与重新验证规则,可以对照 HTTP caching。
保存成功后,哪些缓存需要更新
编辑用户资料时,服务端写入成功只是一个阶段。详情、列表、搜索结果和统计数据,可能都受到影响;应用需要表达这些关联。Query 库提供执行机制,具体关联仍由业务定义。
下图是一种常见策略,前提是更新接口返回完整详情。详情可以直接写入返回值,列表则重新验证,因为修改可能改变其成员、排序或分页。
下面的 Vue 片段只展示写成功后的处理,updateUser 是项目提供的异步接口函数,返回包含 id 的完整用户详情。代码位于已经安装 VueQueryPlugin 的组件 setup 或 composable 中:
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import { updateUser } from './user-api';
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: async (user) => {
queryClient.setQueryData(['users', 'detail', user.id], user);
await queryClient.invalidateQueries({
queryKey: ['users', 'list'],
});
},
});
setQueryData 更新一份确定的查询结果;invalidateQueries 可以按 key 前缀匹配多份查询,将它们标记过期,并按默认规则重新获取活跃查询。没有订阅者的列表通常留到之后使用时再获取,不能把「失效」理解成马上重取应用里所有列表。失效规则与使用 mutation 返回值更新缓存分别说明了这两种操作。
如果更新接口只返回少数字段,就不能直接用它替换完整详情。可以按明确契约合并,也可以把详情标记过期。合并时要创建新对象,避免就地修改缓存里被多个订阅者使用的对象。表单需要编辑时,也可以建立独立草稿,保存后再通过明确入口更新查询数据。
列表尤其不能只按 ID 修改一行就认定完成同步:用户从 active 改为 disabled 后,可能要移出当前列表,另一份列表还要增加条目,总数与分页也可能变化。
乐观更新是在服务端确认前先更新界面,使操作看起来更快。它同时引入了失败回滚、并发写入、旧请求覆盖和最终重新验证的问题。不能只加一段本地赋值,就省略失败路径。
TanStack Query 主要按查询保存结果,不会因为几个返回对象恰好有同一个 ID,就自动维护所有副本。若应用需要实体级归一化缓存,可以比较 Apollo Client 等 GraphQL 客户端提供的数据模型。
在 React 项目中,管理一次异步操作的执行状态,与在组件间共享远程数据,是两类不同的需求。已有 Redux 的项目还应计算新增另一套缓存与数据层的成本。完整内容见React 的服务端状态工具弹窗。
Vue 的响应式请求工具和查询缓存不是同一种能力。Nuxt 页面取数先核对框架入口;需要进一步共享缓存时,再确定数据由哪套机制维护,并检查响应式 key 与 SSR 隔离。完整内容见Vue 与 Nuxt 的数据入口弹窗。
协议、类型与跨端需求也会改变候选
如果请求层还要覆盖多端适配、API 代码生成或实体级缓存,仅比较 fetch 与 Axios 的调用方式就不够了。这些需求会改变接口函数的来源和数据管理方式。
alova覆盖请求适配和更多请求策略,包括不同框架接入、分页、认证、重试与 SSE 等。希望采用较完整请求方案的团队,可以评估它;已有客户端与缓存规范的项目,则应计算替换和重叠成本,不必为了功能清单更长就重新组织所有接口。
后端提供可靠 OpenAPI 时,openapi-fetch可以配合生成的类型获得路径、参数与响应的类型提示;Orval还能生成不同客户端形式及 React Query、Vue Query 等集成代码。这里首先要确认接口文档是否准确、业务错误怎样表达、生成代码怎样更新,以及生成层与自定义认证层在哪里衔接。
代码生成改善的是契约同步与重复劳动,运行时仍然可能收到不符合文档的数据。需要验证的边界,继续使用运行时校验,而不能因为接口代码由工具生成就默认全部可信。
GraphQL 应用还可以比较 Apollo Client、urql等专用客户端。它们提供的协议、订阅和缓存模型,可能比给普通 HTTP 请求增加一层统一包装更贴合需求。
小程序、React Native 和 App WebView 则需要分别确认网络入口、上传下载、Cookie、证书和取消能力。浏览器 API 标准化降低了迁移成本,却不能保证每一种宿主环境都具备相同的运行条件。
新项目的默认选择与验证方式
选型应当在候选职责已经清楚之后,回到应用的数据关系。独立请求、共享查询、复杂写后同步和框架 SSR,会把项目引向不同组合。
| 项目条件 | 优先评估的方案 |
|---|---|
| 少量独立请求,主要管理局部状态 | 原生 fetch 或成熟客户端,配局部异步工具 |
| Vue SPA,共享查询和写后同步较多 | TanStack Vue Query + ofetch、Ky 或 Axios |
| Vue / Pinia 团队,重视 Vue 与路由集成 | Pinia Colada + 请求客户端 |
| React SPA,查询与 mutation 较复杂 | TanStack React Query + 请求客户端 |
| React,偏好简洁的缓存与重新验证入口 | SWR + fetcher |
| 已采用 Redux Toolkit | RTK Query |
| Nuxt 普通页面取数与首屏渲染 | 框架的 useFetch / useAsyncData / $fetch |
| 防抖、轮询、手动执行是主要需求 | ahooks useRequest 或 VueRequest |
对一个新的 Vue 业务系统,我会优先比较 TanStack Vue Query 与 Pinia Colada,再确定底层使用 ofetch、Ky 还是 Axios。对接口很少的页面,可以从 VueUse 或简单异步函数开始。维护良好的 Axios 项目也可以直接增加合适的数据管理层,底层替换不必成为前置条件。
重试、错误和数据 owner 要明确
成熟 HTTP 客户端与 Query 库都可能有重试。如果外层最多执行 3 次,每一次底层又最多尝试 2 次,一轮查询就可能产生 6 次网络调用。重试应当有明确的负责层,区分网络错误、特定 HTTP 状态、业务失败与用户取消;写操作还要结合幂等契约决定是否允许重试。
错误提示也需要一个出口。请求客户端可以保留 status、业务码、原因与必要上下文,页面再决定使用表单错误、局部重试还是全局提示。每一层都弹 Toast,会让一次最终失败出现多条相同通知。
数据已经由 Query 缓存负责时,通常不再把同一份查询结果同步复制到 Pinia 或 Redux 的另一份状态中。表单编辑草稿可以单独存在,但它与查询结果的关系、保存后如何提交和重新同步,应当是明确的业务流程。
用一组会改变决定的场景试选型
在项目内接入候选后,可以用同一组场景检查行为:
- 两个组件同时订阅相同用户,检查正在进行的查询能否复用,二者是否共享结果。
- 离开页面再回来,分别在新鲜期内、过期但未回收、缓存已回收时检查展示与请求行为。
- 修改资料后,确认详情立即更新,相关活跃列表按规则重新验证,筛选条件与总数仍然正确。
- 快速切换搜索条件,确认旧结果不会覆盖新条件,并检查取消是否实际传到网络请求。
- 切换账号或租户,确认旧缓存、正在进行的请求和后到达的响应不会污染新会话。
- SSR 项目额外确认不同访问者的实例隔离,以及客户端是否复用了服务端传递的数据。
这些场景比单独统计包体积或比较一行 API 更接近维护成本。包体积仍然值得测量,但应基于真实构建和目标入口,不能用 node_modules 的目录大小推导首屏收益。
旧项目迁移时,可以先保留现有接口函数,把需要共享缓存的资源接入 Query 层,再逐步处理取消、错误与失效规则。确实决定更换客户端后,单独验证 JSON、空响应、上传、下载、认证、取消和重试等契约,避免同时改变传输行为与缓存行为后难以定位回归。
总结
fetch 已经足以成为现代 Web 请求层的基础。是否继续采用 Axios,主要取决于现有约定、适配器、插件和维护成本;是否采用 Query 库,则取决于远程数据怎样被共享、重新验证和修改。把数据身份、生命周期与写后同步规则确定下来,请求客户端才有一个清楚而稳定的职责范围。
两层职责可以分别调整:维护良好的 Axios 项目可以先接入查询缓存,不必先替换客户端;只有局部异步需求的页面,也不必为了统一而引入完整的数据层。组合之后,要避免两层重试相乘、一次失败重复提示,以及查询结果又被复制到另一份 store。认证隔离、旧结果归属和跨端适配仍有各自的契约,换一个客户端不会替项目完成这些设计。
React 的服务端状态工具
React 中的常见选择
React 项目既可以围绕共享的远程数据组织查询,也可以围绕一次异步操作管理状态。两种方式都能调用同一个接口函数,区别在于上层想维护什么。
TanStack React Query
React Query 现在属于 TanStack Query,React 包名是 @tanstack/react-query。它适合有较多共享查询、分页、失效、mutation 和乐观更新需求的应用。React 版的概览把主要职责定义为获取、缓存、同步与更新服务端状态。
查询函数可以调用原生 fetch、Axios、Ky、生成客户端,甚至不经过 HTTP 的异步数据源。接入时要维持稳定契约:成功返回数据,失败抛出错误或返回拒绝的 Promise。底层吞掉错误后返回一个普通对象,Query 层就可能把它当成成功数据。
对于数据交互较多的 React SPA,我通常会优先评估这一方案。成本是需要学习 query key、缓存生命周期和失效规则;如果项目只有几个互不关联的请求,这套模型的收益就会变小。
SWR
SWR围绕缓存和重新验证组织数据读取,调用入口是 useSWR(key, fetcher)。相同 key 可以共享缓存,支持重新聚焦和网络恢复后的验证,适合希望使用较简洁查询接口的 React 项目。
SWR 也支持写操作、乐观更新和回滚,可以使用 mutate 或 useSWRMutation。因此,「有写操作就必须放弃 SWR」并不成立;应该比较项目里的写后同步模式,以及团队更熟悉哪套 API。mutation 文档给出了这些能力的具体用法。
ahooks useRequest
ahooks 的 useRequest以异步 service 为入口,自动管理 data、loading、error,并提供手动执行、轮询、防抖、节流、重试和缓存等插件能力。它适合搜索、按钮操作、任务状态轮询等以执行控制为中心的需求。
例如,在 React 组件中,搜索 service 可以交给它做手动执行和防抖;这里省略输入框绑定,输入变化时调用 run(keyword):
import { useRequest } from 'ahooks';
import { searchUsers } from './user-api';
const { data, loading, error, run } = useRequest(searchUsers, {
manual: true,
debounceWait: 300,
});
防抖回答的是输入频繁变化时何时执行,查询缓存回答的是某份结果怎样复用与更新,二者可以同时存在。ahooks 也有 Cache & SWR 能力,不能把它简单归为没有缓存的 loading 工具;复杂项目应具体比较缓存身份、跨组件同步和失效入口。
RTK Query
RTK Query是 Redux Toolkit 提供的数据获取与缓存方案,可以集中定义 endpoint、使用 tag 表达失效关系,并生成 React hooks。其缓存与请求逻辑可以用于不同 UI,React hooks 是常见使用入口。
已经采用 Redux Toolkit 的项目,可以先评估 RTK Query,减少两套数据层并存的成本。尚未采用 Redux 的小项目,则需要把新增 Redux 体系的成本一起计算,不能只比较某个请求 hook 的行数。
Vue 与 Nuxt 的数据入口
Vue 中的常见选择
Vue 生态既有响应式请求工具,也有完整的查询缓存方案。函数名里的 use 只表示组合式使用方式,不能代表它们共享同一套缓存模型。Vue 项目的选型还要检查响应式参数如何进入 key,以及 SSR 下实例如何隔离。
VueUse useFetch 与 useAsyncState
VueUse 的 useFetch为 fetch 增加响应式状态、取消、超时、生命周期 hooks 和按 URL 变化重新执行的能力;createFetch 可以建立带默认配置的项目入口。
import { useFetch } from '@vueuse/core';
const { data, error, isFetching, execute } = useFetch('/api/users/42', {
immediate: false,
}).json();
如果已有一个返回 Promise 的业务函数,可以使用 useAsyncState管理执行结果和状态;Axios 用户也可以评估 VueUse integrations 中的 useAxios。
这些工具适合局部异步需求。只有 loading、error、取消和重新执行时,使用它们很直接;如果已经在自己补全局 key 缓存、订阅、去重、失效与回收,就应重新评估 Query 方案。VueUse 当前这些入口也需要与独立的 VueRequest useRequest 区分。
TanStack Vue Query
@tanstack/vue-query 是 TanStack Query 的官方 Vue 适配,采用同一套 query key、QueryClient 和查询生命周期模型。它适合共享数据较多、需要明确写后同步规则的 Vue SPA,也适合希望在 React 与 Vue 之间迁移相似数据层经验的团队。
下面的片段位于组件 setup 中,假定应用已安装 VueQueryPlugin,getUser 是接收 ID 与取消信号的接口函数:
import { ref } from 'vue';
import { useQuery } from '@tanstack/vue-query';
import { getUser } from './user-api';
const userId = ref('42');
const { data, error, isPending, isFetching } = useQuery({
queryKey: ['users', 'detail', userId],
queryFn: ({ signal }) => getUser(userId.value, signal),
staleTime: 30_000,
gcTime: 5 * 60_000,
retry: 1,
});
userId 作为 ref 放在 key 中,Vue Query 才能追踪它的变化。只在创建时取出 userId.value 并传给一个接收普通字符串的 composable,会丢掉后续变化的联系。组件 props 也有类似情况,可以传 ref、computed 或 getter。响应式文档专门说明了这些输入形态。
isPending 表示查询处于尚未获得成功数据的 pending 状态,isFetching 表示查询函数正在执行。被禁用、尚未开始请求的查询也可能处于 pending,第一次实际获取数据的 loading 状态可使用 isLoading;已有数据的后台刷新则结合 data 与 isFetching 判断。延迟查询与 loading 的说明给出了这些标记之间的关系。
这里的 30 秒新鲜期和一次重试只是示例策略,实际值要根据接口调整。signal 也要由 getUser 继续传到 fetch 或 Axios,取消能力才会落实到请求。
Pinia Colada
Pinia Colada为 Vue 提供查询缓存、请求去重、mutation、乐观更新、SSR 和与 Vue Router data loaders 的集成。它依赖 Pinia,但 Query 缓存已经由库负责,不需要再把同一份数据手工复制到另一份业务 store。
它的 API 使用 key 和 query,动态参数可以通过 getter 追踪。例如 key: () => ['users', 'detail', userId.value] 配合读取当前 ID 的 query。这与 Vue Query 的参数形状不同,迁移时应按实际查询 API调整,而不能仅替换 import。
以 Vue 为主、已经采用 Pinia 的团队,可以把它纳入首轮候选。比较重点包括查询与 mutation 的组织方式、路由集成、插件和调试工具是否适合当前项目;生态与维护情况则按准备安装的版本重新检查。
VueRequest 与 SWRV
VueRequest提供 useRequest(service) 形式的接口,覆盖手动执行、轮询、错误重试、缓存、防抖节流和分页等需求。习惯 ahooks useRequest 的开发者可以从它开始比较,尤其关注复杂场景下的共享与失效规则。
SWRV则采用 Vue Composition API 提供 SWR 风格的数据获取。它也是候选之一,不过当前 README 明确标注从 0.10.0 起移除了 SSR 支持,SSR 项目需要把这一限制纳入判断。选型不能只看工具名称与熟悉程度。
Nuxt 的数据获取应先考虑框架入口
Nuxt 自带 useFetch、useAsyncData 和 $fetch。$fetch 执行请求,useFetch 与 useAsyncData 还负责 SSR 数据及其向客户端传递,帮助客户端 hydration 时复用服务端结果。Nuxt 的数据获取文档解释了它们各自的用途。
在首屏 setup 中直接调用 $fetch,与使用带数据传递能力的入口,行为并不相同;用户点击后执行的操作,又可以直接调用相应异步函数。Nuxt 的 useFetch 与 VueUse 同名函数也有不同职责,自动导入与显式 import 应保持清楚。
普通页面取数可以先使用框架入口。出现较复杂的客户端缓存、mutation、失效和共享查询需求后,再评估 TanStack Query 或 Pinia Colada,并确定同一份数据由哪一套机制负责。引入两套缓存以后,互相更新和 SSR hydration 的成本需要一起计算。