我弃用了 TanStack Query,换成了 alova,代码量砍掉 70%

我弃用了 TanStack Query,换成了 alova,代码量砍掉 70%

一个前端老鸟的请求库迁移实录


故事的开始

半年前接手了一个中台项目,需求列表长得像购物小票:分页列表、多步骤表单、实时通知(SSE)、文件上传、搜索自动补全......

刚开始我理所当然地选择了 TanStack Query(以前叫 React Query),毕竟是 React 请求库的"顶流"。但项目越做越不对劲------为了应付不同的请求场景,我在 TanStack Query 外面套了一层又一层的封装:

  • 分页?自己写 useInfiniteQuery + 手动管理 page state + 拼数据
  • 表单?useMutation 只管提交,表单状态、草稿保存、重置全得自己来
  • SSE?TanStack Query 不支持,我去找了 @microsoft/fetch-event-source
  • 服务端重试?TanStack Query 只管客户端......

项目结束后我数了数,光是请求相关的代码就有 2000+ 行

直到有一天,一个同事丢过来一个链接:alova。

第一次震撼:分页从 50 行到 3 行

先来看最直观的对比。

Before: TanStack Query 实现分页

tsx 复制代码
import { useInfiniteQuery } from '@tanstack/react-query'

const PAGE_SIZE = 10

function TodoList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
    isError,
    error,
  } = useInfiniteQuery({
    queryKey: ['todos'],
    queryFn: async ({ pageParam = 1 }) => {
      const res = await fetch(`/api/todos?page=${pageParam}&pageSize=${PAGE_SIZE}`)
      return res.json()
    },
    getNextPageParam: (lastPage, allPages) => {
      return lastPage.hasMore ? allPages.length + 1 : undefined
    },
  })

  if (isLoading) return <Loading />
  if (isError) return <Error message={error.message} />

  const todos = data?.pages.flatMap(page => page.items) ?? []

  return (
    <div>
      {todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? '加载中...' : '加载更多'}
        </button>
      )}
    </div>
  )
}

这已经是最简写法了------20 行组件 + 服务端还得配合 hasMore 字段。如果再加上 loading、error 的状态枚举、空数据处理、刷新控制、页码展示......轻松突破 50 行。

After: alova usePagination

tsx 复制代码
import { usePagination } from 'alova/client'

function TodoList() {
  const {
    loading, data, page,
    pageSize, total,
    nextPage, prevPage,
  } = usePagination(
    (page) => alova.Get('/api/todos', { params: { page, pageSize: 10 } }),
    { total: res => res.total }
  )

  if (loading) return <Loading />

  return (
    <div>
      {data.map(todo => <TodoItem key={todo.id} todo={todo} />)}
      <Pagination
        page={page}
        total={total}
        onNext={nextPage}
        onPrev={prevPage}
      />
    </div>
  )
}

3 行核心代码pagepageSizetotal、翻页、加载状态全部内置。不需要 useInfiniteQuery,不需要手动拼 pages.flatMap,不需要手写 getNextPageParam 逻辑。

这可能是我写前端以来最爽的一次分页实现。

5 个场景,逐个对比

场景 1:表单提交 + 草稿持久化

TanStack Query 的做法:

tsx 复制代码
const mutation = useMutation({
  mutationFn: (data) => api.submitForm(data),
})

// 表单状态靠自己
const [formData, setFormData] = useState({})
const [draft, setDraft] = useState(() => {
  return JSON.parse(localStorage.getItem('formDraft') || '{}')
})

useEffect(() => {
  localStorage.setItem('formDraft', JSON.stringify(formData))
}, [formData])

// 提交
const handleSubmit = () => mutation.mutate(formData)

alova 的做法:

tsx 复制代码
const {
  form, loading, sendForm,
  updateForm, reset,
} = useForm((formData) => alova.Post('/api/submit', formData), {
  initialForm: {},
  store: true, // 自动草稿持久化
})

// 提交
const handleSubmit = () => sendForm()

store: true 一键开启草稿本地持久化,页面刷新表单数据还在。不用写 useEffect、不用手动操作 localStorage、不用管理一个额外的 mutation

场景 2:实时通知(SSE)

TanStack Query:

SSE 不在 TanStack Query 的职责范围内。我需要额外安装 @microsoft/fetch-event-source,然后自己管理连接状态、重连、消息解析:

tsx 复制代码
// 第三方库 + 手动管理
import { EventSource } from '@microsoft/fetch-event-source'

useEffect(() => {
  const es = new EventSource('/api/notifications')
  es.onmessage = (event) => {
    setNotifications(prev => [...prev, JSON.parse(event.data)])
  }
  es.onerror = () => { /* 手动重连 */ }
  return () => es.close()
}, [])

alova 的做法:

tsx 复制代码
const { data, readyState } = useSSE(
  alova.Get('/api/notifications', {
    // SSE 配置
  })
)

readyState 自动反映连接状态(CONNECTING / OPEN / CLOSED),data 是一个响应式流,拿来就用。

场景 3:自动轮询 + 焦点刷新 + 节流

TanStack Query:

tsx 复制代码
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  refetchInterval: 3000,   // 每 3 秒轮询
  refetchOnWindowFocus: true,
})

alova 的做法:

tsx 复制代码
useAutoRequest(alova.Get('/api/todos'), {
  throttle: 3000,           // 节流 3 秒
  enablePoll: true,         // 启用轮询
  enableFocus: true,        // 焦点刷新
})

本身差不多,但 alova 的 throttle 参数可以同时控制轮询和焦点刷新的最小间隔,不会出现焦点来回切换疯狂刷新的问题。

场景 4:请求失败自动重试(服务端)

TanStack Query:

tsx 复制代码
// 只在客户端有效
useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  retry: 3,
  retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
})

alova 服务端重试:

tsx 复制代码
import { retry } from 'alova/server'

// 在 Node.js / Bun / Deno 中都可以用
const result = await retry(
  alovaInstance.Post('/api/order', orderData),
  { retry: 3, backoff: { start: 1000, multiplier: 2 } }
).send()

而且 alova 还支持分布式速率限制

tsx 复制代码
import { RateLimiter } from 'alova/server'

const limiter = new RateLimiter({
  points: 10,     // 10 个请求
  duration: 1,    // 每秒
})

// 配合 Redis 跨进程共享限制状态
const result = await limiter.limit(
  alovaInstance.Get('/api/external')
).send()

这两个能力 TanStack Query 根本覆盖不了------它是纯客户端库,服务端场景力不从心。

场景 5:跨框架复用

TanStack Query 在 React 项目里很香,但你换个 Vue 项目呢?再学一套 @tanstack/vue-query?然后小程序呢?Node.js 后端呢?

alova 一套 API 走天下:

tsx 复制代码
// React 项目
const { data } = useRequest(alova.Get('/api/todos'))

// Vue 项目 --- 只需换 statesHook
const { data } = useRequest(alova.Get('/api/todos'))

// Svelte 项目 --- 一样
const { data } = useRequest(alova.Get('/api/todos'))

// 小程序 (UniApp / Taro) --- 一样
const { data } = useRequest(alova.Get('/api/todos'))

// Node.js 后端 --- 直接 await
const data = await alova.Get('/api/todos')

import 不同,API 相同。 一套心智模型覆盖所有场景。


数据说话:代码量对比

场景 TanStack Query alova 减少
分页列表 ~50 行 3 行 94%
表单提交 + 草稿 ~35 行 3 行 91%
SSE 实时通知 ~30 行(第三方库) 1 行 97%
自动轮询 ~10 行 2 行 80%
失败重试 ~8 行 1 行 87%
总计(5 个场景) ~133 行 ~10 行 92.5%

当然这不是说 TanStack Query 不好------它是一个优秀的库,在「缓存 + 状态管理」这个点上做得非常好。但它定位的是 "tool",而不是 "strategy"。每一个业务场景你都需要自己去组合它的基础能力。

alova 的答案是:把这些常见场景直接封装成策略,开箱即用。

就像你用 React 不需要自己实现虚拟 DOM diff,用 alova 你也不需要自己实现分页逻辑。

什么时候选谁?

  • 简单的 CRUD + 缓存场景:TanStack Query 完全够用,没有任何问题
  • 复杂的中后台 / BFF 项目:涉及的请求模式多,alova 的策略思维帮你节约大量时间
  • 跨框架 / 跨端项目:alova 的框架无关性是硬优势
  • Node.js 服务端需要请求控制:重试、限流、分布式锁,alova 是唯一选择

最后

说实话,作为一个开源作者,我深知做一个好库有多难,而让好库被人知道更难。alova 的 v3 版本已经非常成熟了,20+ 请求策略、客户端+服务端全覆盖、框架无关,这些能力不应该只被 4k 人看到。

如果你看完了觉得有点意思:

GitHub: alovajs/alova

🌐 官网: alova.js.org

也欢迎留言讨论------你觉得请求库的下一个进化方向是什么?