跳到主要内容

2 篇博文 含有标签「react-query」

查看所有标签

react-query 管不到 BFF?alova/server 补上重试限流

· 阅读需 4 分钟
Alova Team

你用 react-query 搭了前端,它的缓存和重试让浏览器那边很顺。后来你起了个 Node BFF 调三个下游服务,这些调用的重试和限流都落在服务端,而 react-query 在那儿根本不跑。这篇划清 react-query 的工作止步于哪、alova/server 从哪接手,并给出两侧的代码。

那条线

react-query 活在浏览器里。它的 useQuery 重试、缓存、去重,全都发生在每个浏览器标签页里,绑着 React 的渲染周期。当你的 BFF 调下游时,这些逻辑在 BFF 内部一个都不执行。所以问题不是「react-query 还是 alova」,而是「服务端那层调用归谁管」。

重试:浏览器 vs BFF

react-query 在浏览器里重试一个失败的查询:

useQuery({
queryKey: ['resource', id],
queryFn: () => fetch(`/api/resource/${id}`).then(r => r.json()),
retry: 3,
retryDelay: attempt => Math.min(1000 * 2 ** attempt, 8000)
});

这个重试护的是用户的视图。它对 BFF 出站调下游毫无作用,因为那次调用压根不经过 react-query。

在 BFF 上,alova/server 对出站请求做退避重试:

const { createAlova } = require('alova');
const { axiosRequestAdapter } = require('@alova/adapter-axios');
const { retry } = require('alova/server');

const alovaInst = createAlova({
baseURL: 'https://downstream.internal',
requestAdapter: axiosRequestAdapter()
});

const getResource = (id) =>
retry(alovaInst.Get(`/resource/${id}`), {
retry: 5,
backoff: { delay: 1000, multiplier: 2 }
});

// 在路由 handler 里:
const data = await getResource(req.params.id);

同一个思路,不同层。react-query 管浏览器,retry 管服务端。顺带一句:alova 的客户端 hook 在 uni-app / Taro / 小程序里也能跑同一套 useRequest,所以「浏览器/服务端分层」在内客户端也成立。

限流:只在服务端有

react-query 完全没有「给 BFF 调下游做限流」这个概念,它的 API 里找不到「对服务 X 每秒最多 4 次」这种东西。那是服务端的事,而 alova/server 的 createRateLimiter 正是为它生的:

const { createRateLimiter } = require('alova/server');

const rateLimit = createRateLimiter({ duration: 4000, points: 4 });

app.get('/api/resource/:id', async (req, res) => {
try {
const data = await rateLimit(
retry(alovaInst.Get(`/resource/${req.params.id}`), {
retry: 5,
backoff: { delay: 1000, multiplier: 2 }
}),
{ key: `downstream:${req.params.id}` }
);
res.json(data);
} catch (err) {
res.status(429).json({ error: 'downstream busy, retry later' });
}
});

rateLimit 在请求真正出去之前先扣一个点,被下游打趴的时候它不被你自己的流量埋了。默认存储是 method 的 l2Cache;跨实例时你换成一个共享存储(@alova/psc 或 redis),额度才是真正集群级的。

缓存:先搞清楚它活在哪一侧

这是最容易被混的一点。react-query 的缓存是「每浏览器」的,它不让同一个用户在同一标签页里重复拉同一个资源,但对你 BFF 猛锤一个慢下游毫无作用,因为那次调用不在浏览器里。

alova 的缓存也是客户端策略特性:cacheFor 控制响应保留多久,模式只有 memoryrestore,非 GET 请求默认是 null(不缓存)。在服务端,alova/server 给你的是 retrycreateRateLimiter,它并不顺手塞给你一个下游响应的服务端缓存。如果你不想对重复的慢 GET 反复打下游,得自己加一层缓存(或倚仗下游自己带的),再把 retry/rateLimit 叠上去。

所以分工很干净:react-query 守浏览器缓存,alova/server 守 BFF 出站的重试和限流,互不相踩。

两个工具管的是不同层。请求止步于浏览器的地方用 react-query,BFF 那层交给 alova/server。要完整选型视角,见 react-query 与 alova 对比

react-query 还是 alova:看请求跑不跑出浏览器

· 阅读需 4 分钟
Alova Team

选数据请求库,通常开头都是「谁的缓存更好」。这个问题跳过了一个事后才咬人的点:你的请求到底跑在哪?

如果你的请求全在浏览器里、而且用 React,react-query 是成熟、踩坑少的选择。可一旦你需要同一套请求逻辑跑在「不是 React Native 的手机端」,或者跑在一个调下游服务的 BFF 里,地图就变了。alova 把这些情况收到了同一套 API 下。这篇是直来直去的对比,也包括 alova 更弱的地方。

react-query 明显领先的地方

先讲诚实的一面:

  • 和 React 贴合。 react-query 是围着 React 的渲染模型建的。useQuery 这类 hook 天然嵌进组件,它的 devtools 和查询面板是这个领域里最顺手的。
  • 缓存成熟度。 它的缓存、去重、后台重拉、stale-while-revalidate,在大量生产应用里被反复验证过。
  • 社区与答案。 这么多年的 Stack Overflow、博客、能直接抄的片段,意味着大部分问题网上已经有人解过了。

如果你的前端是 React、只在浏览器里、你也对这个生态满意,react-query 是更稳的默认。说反话是不诚实的。

alova 走得更远的地方

alova 也是请求策略层,但它不绑死某个框架或某个运行时:

  • 一套 API 跨运行时。 同一套 useRequestusePagination 调用,跑在 Web、移动端(React Native / Expo / uni-app / Taro / 小程序)、以及服务端(Node BFF)。请求逻辑你只写一遍。
  • 服务端 hook。 alova/server 给 BFF 的出站调用加了 retrycreateRateLimiter,这一层 react-query 管不到,因为它活在浏览器里。
  • 框架任选。 React、Vue、Svelte、Solid 都有一等公民级别的 hook,混合技术栈的团队不必被绑进 React。
  • 站在你现有的适配器上。 alova 自己不发请求,它跑在 fetch 或 axios 适配器之上,所以你已经在用 axios 的话,它继续干活。

逐项对照

关注点react-queryalova
主运行时浏览器(React)Web、移动端、服务端(BFF)
框架React 优先React / Vue / Svelte / Solid
客户端缓存极佳、成熟不错,策略 hook
服务端重试 / 限流不归它管alova/server hook
跨端 App仅 React NativeReact Native、Expo、uni-app、Taro、小程序
自己发 HTTP否(底层 fetch/axios)否(fetch/axios 适配器)
社区规模非常大较小

最后一行说明:两个库都不替代 axios。它们都坐在请求适配器之上,所以团队已经在用 axios 的话,发请求还是它的活。

什么时候 react-query 更对

  • 前端是 React、只在浏览器,你想要最深的 React 缓存生态。
  • 你不跑一个需要出站重试/限流的 BFF。
  • 团队已经熟 react-query,用它出活快。

什么时候 alova 更合适

  • 你要跨 Web、非 React Native 的 App、Node BFF 维护同一套请求逻辑。
  • 你的 BFF 需要对下游调用做退避重试加客户端限流(见 BFF 重试那篇)。
  • 你在用 Vue/Svelte/Solid,或混合栈,想要一个策略层。

什么情况下两个都不需要

如果你就发几个请求,手写 loading/data/error 三个状态也不重复,那 fetch 加一个小封装就够了。两个库只有在「同一类请求模式反复出现」时才值回票价。