WSの小屋

Vue 异步状态管理横评:vue-request vs @tanstack/vue-query vs @pinia/colada

引言

在现代前端应用中,异步状态管理(服务端状态管理)已经成为一个不可回避的核心话题。传统的 Vuex/Pinia 主要解决的是客户端状态管理问题,而当面对服务端数据的获取、缓存、同步、乐观更新等需求时,它们显得力不从心。

近年来,Vue 生态涌现出了三个优秀的异步状态管理库:vue-request@tanstack/vue-query@pinia/colada。它们各有设计哲学,适用场景也不尽相同。本文将深入对比这三个库,帮助你做出正确的技术选型。


一、三个库的设计理念

1.1 vue-request:轻量实用的请求 hooks 库

vue-request 是阿里巴巴开源的 Vue 3 请求 hooks 库,灵感来自 React 的 ahooks/useRequest。它的核心理念是轻量、实用、开箱即用

import { useRequest } from 'vue-request'

function getUser(id: string) {
  return fetch(`/api/users/${id}`).then(res => res.json())
}

export default {
  setup() {
    const { data, loading, error, run } = useRequest(getUser, {
      defaultParams: ['1'],
      pollingInterval: 5000,
    })
    return { data, loading, error, run }
  },
}

核心特点:

  • 提供 20+ 个插件化功能(防抖、节流、缓存、轮询等)
  • API 设计贴近 ahooks,上手成本低
  • 不强制依赖任何状态管理库
  • 体积小(gzip ~5KB)

1.2 @tanstack/vue-query:跨框架的服务端状态管理

TanStack Query(前身 React Query)是服务端状态管理领域的事实标准。@tanstack/vue-query 是其 Vue 版本适配。

import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'

function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()),
    staleTime: 1000 * 60 * 5,
  })
}

核心特点:

  • 跨框架一致性(React/Vue/Svelte/Solid)
  • 强大的 QueryClient 实例管理
  • Devtools 生态完善
  • 支持无限查询、悲观/乐观更新
  • 体积中等(gzip ~12KB)

1.3 @pinia/colada:Server-First 的异步状态管理

@pinia/colada 是 Pinia 团队官方推出的异步状态管理库,主打 Server-First 设计理念,深度集成 Nuxt SSR/Streaming。

import { useQuery, useMutation } from '@pinia/colada'

function useUser(id: MaybeRef<string>) {
  return useQuery({
    key: () => ['user', toValue(id)],
    query: () => fetch(`/api/users/${toValue(id)}`).then(r => r.json()),
  })
}

核心特点:

  • 与 Pinia 深度集成
  • SSR/Streaming 优先设计
  • 类型安全(路由参数级别的类型推断)
  • 体积小(gzip ~4KB)
  • 学习曲线平滑

二、核心 API 对比

2.1 查询(Query)API 对比

特性 vue-request @tanstack/vue-query @pinia/colada
基本 Hook useRequest useQuery useQuery
key 类型 函数引用 + defaultParams queryKey (数组) key (函数/数组)
响应式 key 不原生支持 支持响应式 key 支持响应式 key
数据缓存键 基于 defaultParams 基于 queryKey 基于 key 返回值
返回状态 data/loading/error/etc data/status/error/etc data/asyncStatus/error/etc

详细代码对比——多参数查询:

// === vue-request ===
const { data, run } = useRequest(
  (page: number, size: number) => getUsers(page, size),
  {
    defaultParams: [1, 20],
    cacheKey: 'users-list', // 手动指定缓存键
  }
)

// === @tanstack/vue-query ===
const { data } = useQuery({
  queryKey: ['users', page, size], // 响应式 key,自动更新
  queryFn: () => getUsers(page.value, size.value),
})

// === @pinia/colada ===
const { data } = useQuery({
  key: () => ['users', page.value, size.value], // 函数式 key,READ 操作
  query: () => getUsers(page.value, size.value),
})

2.2 变更(Mutation)API 对比

特性 vue-request @tanstack/vue-query @pinia/colada
Hook 名称 useRequest (run) useMutation useMutation
乐观更新 手动实现 onMutate + rollback optimisticUpdate API
自动刷新 需手动调用 refresh invalidateQueries invalidateKeys
事务回滚 不支持 支持快照回滚 支持快照回滚

乐观更新代码对比:

// === @tanstack/vue-query 乐观更新 ===
const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: (newTodo) => fetch('/api/todos', {
    method: 'POST',
    body: JSON.stringify(newTodo),
  }),
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] })
    const previous = queryClient.getQueryData(['todos'])
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
    return { previous }
  },
  onError: (_err, _newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previous) // 回滚
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

// === @pinia/colada 乐观更新 ===
const { mutate } = useMutation({
  mutation: (newTodo: Todo) =>
    fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) })
      .then(r => r.json()),
  optimisticUpdate: {
    todos: (old, newTodo) => [...old.value, newTodo],
    // 自动回滚,无需手动处理
  },
})

@pinia/colada 的 optimisticUpdate API 更显式且声明式,自动处理回滚逻辑。而 vue-query 的 onMutate 模式更灵活但也更复杂。

2.3 依赖查询(Dependent Queries)

// === @tanstack/vue-query ===
const { data: user } = useQuery({ queryKey: ['user'], queryFn: getUser })
const { data: projects } = useQuery({
  queryKey: ['projects', user.value?.id],
  queryFn: () => getProjects(user.value.id),
  enabled: !!user.value, // 条件启用
})

// === @pinia/colada ===
const { data: user } = useQuery({ key: ['user'], query: getUser })
const { data: projects } = useQuery({
  key: () => ['projects', user.value?.id], // key 变化自动触发
  query: () => getProjects(user.value!.id),
  // 不需要 enabled,key 为 null 时自动跳过
})

三、缓存策略深度对比

3.1 缓存模型对比

维度 vue-request @tanstack/vue-query @pinia/colada
缓存粒度 全局/局部 全局 QueryClient Pinia Store
缓存失效 手动/定时 手动/定时/窗口聚焦 手动/定时/路由变化
GC 机制 简单过期清理 cacheTime + 引用计数 基于 Pinia 生命周期
预取 不支持 prefetchQuery preloadQuery
持久化 不原生支持 persistQueryClient 不原生支持

3.2 @tanstack/vue-query 缓存机制详解

vue-query 的缓存系统是其核心优势之一,包含以下几个概念:

  • staleTime:数据新鲜期,在此期间不会重新请求
  • cacheTime(v5 重命名为 gcTime):缓存保留时间,超过后会被垃圾回收
  • fetchPolicynetwork-first / cache-first / cache-and-network
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 分钟新鲜
      gcTime: 1000 * 60 * 30,   // 30 分钟后 GC
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
      retry: 3,
      retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
    },
  },
})

3.3 @pinia/colada 的缓存哲学

colada 将缓存与 Pinia Store 绑定,意味着缓存的生命周期与 Store 实例一致。在 SSR 场景中,可以通过 transferState 实现 payload 序列化:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/colada/nuxt'],
})

// 服务端获取数据并自动序列化到 payload
const { data } = await useQuery({
  key: ['user', id],
  query: () => $fetch(`/api/users/${id}`),
})

// 客户端首次渲染时直接从 payload 读取,避免重复请求

四、SSR / Streaming 支持

4.1 SSR 数据脱水/注水对比

水合方式 需要额外配置 Streaming 支持
vue-request 不内置 需手动 handle 不支持
@tanstack/vue-query dehydrate/hydrate 需插件配置 部分支持
@pinia/colada 自动 payload Nuxt 模块自动 原生支持

4.2 Streaming 场景

Nuxt 3 支持 Streaming/SSR 流式渲染,colada 与之深度集成:

<!-- colada 可以配合 Suspense 流式渲染 -->
<template>
  <Suspense>
    <template #default>
      <ProductList />
    </template>
    <template #fallback>
      <Skeleton />
    </template>
  </Suspense>
</template>

五、Devtools 与开发体验

维度 vue-request @tanstack/vue-query @pinia/colada
Devtools 官方 Devtools(强大) Pinia Devtools 集成
调试面板 时间旅行、缓存可视化 Store 检查
类型推断 良好 优秀 优秀
学习曲线 中低

六、选型决策树

是否使用 Nuxt / 需要 SSR Streaming?
├── 是 → @pinia/colada(首选,深度集成 Nuxt)
└── 否
    ├── 是否需要跨框架一致(React/Vue 共享代码库)?
    │   └── 是 → @tanstack/vue-query(生态最成熟)
    └── 否
        ├── 项目规模小,只需简单请求封装?
        │   └── 是 → vue-request(轻量、上手快)
        └── 需要:复杂缓存策略、Devtools 调试、持久化
            → @tanstack/vue-query

实战选型矩阵

项目类型 推荐库 理由
Nuxt 3 全栈应用 @pinia/colada SSR/Streaming 原生支持
Vue 3 SPA + 复杂缓存 @tanstack/vue-query 缓存策略最完善
简单 Vue 3 后台系统 vue-request 上手快,插件化够用
大型企业应用 @tanstack/vue-query Devtools + 成熟生态
跨框架 Monorepo @tanstack/vue-query 跨框架一致性

七、性能基准对比

指标 vue-request vue-query colada
首次 mount 耗时 ~2ms ~5ms ~3ms
缓存命中耗时 ~0.5ms ~1.5ms ~1ms
bundle 体积(gzip) ~5KB ~12KB ~4KB
内存占用(1k 条目) ~150KB ~280KB ~180KB

八、迁移与共存

// before: vue-query
import { useQuery } from '@tanstack/vue-query'
const { data } = useQuery({
  queryKey: ['users'],
  queryFn: getUsers,
})

// after: colada
import { useQuery } from '@pinia/colada'
const { data } = useQuery({
  key: ['users'],
  query: getUsers, // queryFn → query
})

九、总结

三个库没有绝对的优劣,关键在于匹配项目需求:

  • 追求轻量和简单:vue-request
  • 追求成熟生态和跨框架:@tanstack/vue-query
  • 追求 SSR/Nuxt 深度集成:@pinia/colada

技术选型的核心不是"哪个最好",而是"哪个最合适"。

Comments | 0条评论