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):缓存保留时间,超过后会被垃圾回收 - fetchPolicy:
network-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条评论