主题
概述
TanStack Query (原 Vue Query) 是一个强大的异步状态管理库,专门用于处理服务端状态。它解决了传统数据请求的诸多痛点:缓存管理、重复请求去重、后台数据同步、分页等。
为什么使用 TanStack Query?
- 自动缓存:相同 queryKey 自动复用缓存数据
- 后台刷新:窗口聚焦时自动重新获取最新数据
- 请求去重:多个组件同时请求自动合并
- 乐观更新:mutation 时可先更新 UI,失败再回滚
- 分页与无限滚动:内置支持,无需自行实现
技术特性
| 特性 | 说明 |
|---|---|
| 缓存策略 | stale-while-revalidate |
| 重试机制 | 自动重试失败请求 |
| 请求去重 | 相同 queryKey 自动去重 |
| 后台刷新 | 窗口聚焦/网络恢复时刷新 |
| 开发工具 | @tanstack/vue-query-devtools |
| TypeScript | 完整类型支持 |
核心概念
Query vs Mutation
| 概念 | 用途 | HTTP 方法 |
|---|---|---|
| Query | 获取数据(读) | GET |
| Mutation | 修改数据(写) | POST, PUT, PATCH, DELETE |
基础用法
useQuery - 数据查询
composables/useUsers.ts
typescript
import { useQuery } from '@tanstack/vue-query'
import { userApi } from '@/api/user'
// 无参数查询
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: userApi.getAll,
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
})
}
// 单个资源查询
export function useUser(id: Ref<string>) {
return useQuery({
queryKey: ['user', id],
queryFn: () => userApi.getById(id.value),
enabled: computed(() => !!id.value), // id 有值时才请求
})
}在组件中使用
UserList.vue
vue
<script setup lang="ts">
import { useUsers } from '@/composables/useUsers'
const { data: users, isLoading, error, refetch } = useUsers()
</script>
<template>
<div v-if="isLoading">加载中...</div>
<div v-else-if="error">加载失败: {{ error.message }}</div>
<div v-else>
<div v-for="user in users" :key="user.id">
{{ user.name }}
</div>
<Button @click="refetch">刷新</Button>
</div>
</template>响应式参数(重要!)
最常见的错误
当 composable 需要接收参数时,参数类型必须是 Ref<T>,否则参数变化不会触发重新请求!
正确写法
composables/useCourses.ts
typescript
import { useQuery } from '@tanstack/vue-query'
import type { Ref } from 'vue'
import { courseApi } from '@/api/course'
// ✅ 正确:参数是 Ref 类型
interface CourseListParams {
page: Ref<number>
pageSize: Ref<number>
keyword: Ref<string>
categoryId: Ref<string>
}
export function useCourses(params: CourseListParams) {
return useQuery({
// queryKey 中直接放 Ref,TanStack Query 会自动追踪变化
queryKey: ['courses', params.page, params.pageSize, params.keyword, params.categoryId],
// queryFn 中使用 .value 获取实际值
queryFn: () => courseApi.getList({
page: params.page.value,
page_size: params.pageSize.value,
keyword: params.keyword.value,
category_id: params.categoryId.value,
}),
staleTime: 5 * 60 * 1000,
})
}在页面中使用
CourseList.vue
vue
<script setup lang="ts">
import { useCourses } from '@/composables/useCourses'
// 创建响应式变量
const page = ref(1)
const pageSize = ref(10)
const keyword = ref('')
const categoryId = ref('')
// 传入 Ref,任何一个值变化都会自动重新请求
const { data, isLoading } = useCourses({
page,
pageSize,
keyword,
categoryId,
})
</script>
<template>
<div>
<Input v-model="keyword" placeholder="搜索..." />
<Select v-model="categoryId">
<SelectItem value="">全部</SelectItem>
<SelectItem value="1">前端</SelectItem>
<SelectItem value="2">后端</SelectItem>
</Select>
<div v-if="isLoading">加载中...</div>
<CourseCard v-for="course in data?.items" :key="course.id" :course="course" />
<Pagination v-model:page="page" :total="data?.total" />
</div>
</template>错误写法
错误示例
typescript
// ❌ 错误:参数是普通类型,不会响应式更新
interface CourseListParams {
page: number // 错误!应该是 Ref<number>
pageSize: number // 错误!应该是 Ref<number>
}
export function useCourses(params: CourseListParams) {
return useQuery({
queryKey: ['courses', params.page, params.pageSize], // 不会追踪变化
queryFn: () => courseApi.getList(params),
})
}useMutation - 数据变更
composables/useUsers.ts
typescript
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { userApi } from '@/api/user'
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: userApi.create,
onSuccess: () => {
// 创建成功后,使 users 列表缓存失效,触发重新请求
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
}
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<User> }) =>
userApi.update(id, data),
onSuccess: (_, variables) => {
// 更新成功后,使相关缓存失效
queryClient.invalidateQueries({ queryKey: ['users'] })
queryClient.invalidateQueries({ queryKey: ['user', variables.id] })
},
})
}
export function useDeleteUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: userApi.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
}使用 Mutation
CreateUserForm.vue
vue
<script setup lang="ts">
import { useCreateUser } from '@/composables/useUsers'
const { mutateAsync: createUser, isPending } = useCreateUser()
const handleSubmit = async (formData: CreateUserDto) => {
await createUser(formData)
// 成功后的处理(Toast 由 httpClient 统一处理)
}
</script>
<template>
<form @submit.prevent="handleSubmit(formData)">
<!-- 表单内容 -->
<Button type="submit" :disabled="isPending">
{{ isPending ? '创建中...' : '创建用户' }}
</Button>
</form>
</template>条件查询
使用 enabled 控制何时执行查询:
条件查询
typescript
export function useUserOrders(userId: Ref<string | null>) {
return useQuery({
queryKey: ['orders', userId],
queryFn: () => orderApi.getByUserId(userId.value!),
// ⚠️ enabled 必须使用 computed 包裹!
enabled: computed(() => !!userId.value),
})
}enabled 必须是 computed
如果 enabled 依赖响应式数据,必须用 computed() 包裹,否则条件变化不会生效。
手动控制
手动刷新
typescript
const { data, refetch, isFetching } = useUsers()
// 手动刷新
const handleRefresh = () => {
refetch()
}缓存失效
缓存失效
typescript
import { useQueryClient } from '@tanstack/vue-query'
const queryClient = useQueryClient()
// 使特定 queryKey 的缓存失效
queryClient.invalidateQueries({ queryKey: ['users'] })
// 使匹配前缀的所有缓存失效
queryClient.invalidateQueries({ queryKey: ['users'] }) // 会匹配 ['users'], ['users', 1], ['users', { page: 1 }] 等
// 直接设置缓存数据
queryClient.setQueryData(['user', id], updatedUser)
// 获取缓存数据
const cachedUser = queryClient.getQueryData(['user', id])最佳实践
开发建议
- queryKey 设计:使用数组,按层级组织,如
['users', userId, 'orders'] - staleTime 设置:根据数据更新频率设置,避免过度请求
- 错误处理:httpClient 已统一处理,无需在 onError 中重复 toast
- 缓存失效:mutation 成功后记得 invalidateQueries
- 类型安全:充分利用 TypeScript 类型推导
完整示例
composables/useProducts.ts
typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
import type { Ref } from 'vue'
import { productApi } from '@/api/product'
import type { Product, CreateProductDto, UpdateProductDto } from '@/types/product'
interface ProductListParams {
page: Ref<number>
pageSize: Ref<number>
keyword: Ref<string>
status: Ref<string>
}
// 列表查询
export function useProducts(params: ProductListParams) {
return useQuery({
queryKey: ['products', params.page, params.pageSize, params.keyword, params.status],
queryFn: () => productApi.getList({
page: params.page.value,
page_size: params.pageSize.value,
keyword: params.keyword.value,
status: params.status.value,
}),
staleTime: 5 * 60 * 1000,
})
}
// 详情查询
export function useProduct(id: Ref<string>) {
return useQuery({
queryKey: ['product', id],
queryFn: () => productApi.getById(id.value),
enabled: computed(() => !!id.value),
})
}
// 创建
export function useCreateProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: CreateProductDto) => productApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] })
},
})
}
// 更新
export function useUpdateProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateProductDto }) =>
productApi.update(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['products'] })
queryClient.invalidateQueries({ queryKey: ['product', variables.id] })
},
})
}
// 删除
export function useDeleteProduct() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: productApi.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] })
},
})
}