xlx_teacher_app/.cursor/rules/tanstack-query-guide.md

405 lines
9.3 KiB
Markdown
Raw Permalink Normal View History

2025-09-21 12:30:25 +08:00
# TanStack Query 使用指南
## 📋 概述
TanStack Query (原名 React Query) 是一个强大的数据获取和状态管理库,为 Vue 3 项目提供:
- 🚀 **智能缓存**: 自动缓存和去重请求
- 🔄 **后台更新**: 自动在后台重新获取数据
-**实时同步**: 窗口聚焦时自动刷新
- 📱 **离线支持**: 网络恢复时自动重试
- 🎯 **响应式**: 与 Vue 3 Composition API 完美集成
## 🚀 快速开始
### 1. 基础配置
项目已配置好 TanStack Query`main.ts` 中:
```typescript
import { VueQueryPlugin } from '@tanstack/vue-query'
app.use(VueQueryPlugin)
```
### 2. 基础用法
```vue
<template>
<div v-if="isLoading">加载中...</div>
<div v-else-if="error">发生错误: {{ error.message }}</div>
<div v-else>
<div v-for="item in data" :key="item.id">
{{ item.name }}
</div>
</div>
</template>
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { apiService } from '@/api'
const { isLoading, error, data } = useQuery({
queryKey: ['users'],
queryFn: () => apiService.getUserList()
})
</script>
```
## 📚 核心 API
### useQuery - 数据获取
用于获取数据的基础 hook
```typescript
const {
data, // 查询结果
isLoading, // 首次加载状态
isFetching, // 获取状态(包括后台刷新)
error, // 错误信息
refetch, // 手动重新获取
remove // 移除查询缓存
} = useQuery({
queryKey: ['queryKey'],
queryFn: fetchFunction,
enabled: true, // 是否启用查询
staleTime: 30000, // 数据过期时间30秒
gcTime: 300000 // 缓存垃圾回收时间5分钟
})
```
### useMutation - 数据修改
用于创建、更新、删除操作:
```typescript
const {
mutate, // 执行变更
mutateAsync, // 异步执行变更
isLoading, // 变更加载状态
error, // 变更错误
isSuccess // 变更成功状态
} = useMutation({
mutationFn: (data) => apiService.createUser(data),
onSuccess: (data) => {
// 成功回调
queryClient.invalidateQueries(['users'])
},
onError: (error) => {
// 重要: ElMessage 已经在httpClient 封装过了,如果不需要对异常进行特殊处理,你完全不需要 onError
}
})
```
### useQueryClient - 查询客户端
用于手动管理缓存:
```typescript
import { useQueryClient } from '@tanstack/vue-query'
const queryClient = useQueryClient()
// 手动设置查询数据
queryClient.setQueryData(['user', userId], userData)
// 使特定查询失效
queryClient.invalidateQueries({ queryKey: ['users'] })
// 移除查询
queryClient.removeQueries(['user', userId])
// 获取查询数据
const userData = queryClient.getQueryData(['user', userId])
```
## 🎯 实际应用场景
### 1. 列表页面数据获取
列表不需要使用这个,使用封装好的 useTable 即可
### 2. 详情页面数据获取
```vue
<template>
<div v-if="isLoading">
<ElSkeleton />
</div>
<div v-else-if="error">
<ElResult icon="error" title="加载失败" :sub-title="error.message">
<template #extra>
<ElButton @click="refetch">重试</ElButton>
</template>
</ElResult>
</div>
<div v-else>
<h1>{{ data?.name }}</h1>
<p>{{ data?.description }}</p>
</div>
</template>
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { userApi } from '@/api'
interface Props {
userId: number
}
const props = defineProps<Props>()
const { isLoading, error, data, refetch } = useQuery({
queryKey: computed(() => ['user', props.userId]),
queryFn: () => userApi.getUserDetail(props.userId),
enabled: computed(() => !!props.userId)
})
</script>
```
### 3. 表单提交和数据更新
```vue
<template>
<ElForm @submit="handleSubmit">
<ElFormItem label="姓名">
<ElInput v-model="form.name" />
</ElFormItem>
<ElFormItem>
<ElButton
type="primary"
@click="handleSubmit"
:loading="isLoading"
>
{{ isLoading ? '保存中...' : '保存' }}
</ElButton>
</ElFormItem>
</ElForm>
</template>
<script setup lang="ts">
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { userApi } from '@/api'
const queryClient = useQueryClient()
const form = reactive({
name: '',
email: ''
})
const { mutate: createUser, isLoading } = useMutation({
mutationFn: (userData) => userApi.createUser(userData),
onSuccess: (newUser) => {
ElMessage.success('创建成功')
// 方法1: 使列表查询失效,重新获取
queryClient.invalidateQueries(['users'])
// 方法2: 手动更新缓存(性能更好)
queryClient.setQueryData(['users'], (oldData) => {
return {
...oldData,
list: [newUser, ...oldData.list]
}
})
// 清空表单
Object.assign(form, { name: '', email: '' })
},
// 不用 onError因为 ElMessage 已经在httpClient 封装过了
})
const handleSubmit = () => {
createUser(form)
}
</script>
```
### 4. 依赖查询
```vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
const props = defineProps<{
userId: number
}>()
const otherRefVariable = ref(null)
// 用户列表查询
const { data: users } = useQuery({
queryKey: ['users'],
queryFn: () => userApi.getUserList()
})
// 用户详情查询依赖于选中的用户ID
// 如果是 props 的变量,需要 computed 包裹
const { data: userDetail, isLoading: isLoadingDetail } = useQuery({
queryKey: computed(() => ['user', props.userId]),
queryFn: () => userApi.getUserDetail(props.userId),
enabled: computed(() => !!props.userId)
})
// 用户权限查询(依赖于 otherRefVariable
// 如果依赖的变量是 ref 且不需要进行计算,则不需要 computed 包裹
const { data: userPermissions } = useQuery({
queryKey: ['userPermissions', otherRefVariable],
queryFn: () => userApi.getUserPermissions(otherRefVariable.value),
enabled: computed(() => !!userDetail.value)
})
</script>
```
### 5. 无限滚动加载
```vue
<template>
<div class="infinite-list">
<div v-for="item in flatData" :key="item.id">
{{ item.name }}
</div>
<div v-if="isFetchingNextPage" class="loading">
加载更多...
</div>
<ElButton
v-if="hasNextPage"
@click="fetchNextPage"
:loading="isFetchingNextPage"
>
加载更多
</ElButton>
</div>
</template>
<script setup lang="ts">
import { useInfiniteQuery } from '@tanstack/vue-query'
import { userApi } from '@/api'
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['users', 'infinite'],
queryFn: ({ pageParam = 1 }) => userApi.getUserList({
current: pageParam,
size: 20
}),
getNextPageParam: (lastPage, pages) => {
// 判断是否还有下一页
if (lastPage.current < lastPage.pages) {
return lastPage.current + 1
}
return undefined
}
})
// 展平数据
const flatData = computed(() => {
return data.value?.pages.flatMap(page => page.list) || []
})
</script>
```
## 🔧 高级功能
### 3. 条件查询
```typescript
const searchQuery = ref('')
const shouldSearch = computed(() => searchQuery.value.length >= 2)
const { data: searchResults } = useQuery({
queryKey: computed(() => ['search', searchQuery.value]),
queryFn: () => searchApi.search(searchQuery.value),
enabled: shouldSearch,
staleTime: 30000 // 搜索结果缓存30秒
})
```
## ⚙️ 配置选项
### 常用配置项
```typescript
useQuery({
queryKey: ['key'],
queryFn: fetchFunction,
// 缓存配置
staleTime: 30000, // 数据新鲜时间
gcTime: 300000, // 垃圾回收时间
// 重试配置
retry: 3, // 重试次数
retryDelay: 1000, // 重试延迟
// 刷新配置
refetchOnMount: true, // 组件挂载时刷新
refetchOnWindowFocus: false, // 窗口聚焦时刷新
refetchOnReconnect: true, // 网络重连时刷新
// 条件配置
enabled: true, // 是否启用查询
// 占位数据
placeholderData: [], // 占位数据
keepPreviousData: true // 保留之前的数据
})
```
## 📋 最佳实践
### 3. 性能优化
```typescript
// 使用 keepPreviousData 避免加载闪烁
const { data } = useQuery({
queryKey: computed(() => ['users', pagination.value]),
queryFn: () => userApi.getUserList(pagination.value),
keepPreviousData: true
})
```
## 🐛 常见问题
### Q: 数据没有自动更新?
A: 检查查询键是否正确设置为响应式:
```typescript
// ❌ 错误:查询键不是响应式的
useQuery({
queryKey: ['users', searchQuery.value], // 不会响应变化
queryFn: () => api.search(searchQuery.value)
})
// ✅ 正确:使用 computed 让查询键响应式
useQuery({
queryKey: computed(() => ['users', searchQuery.value]),
queryFn: () => api.search(searchQuery.value)
})
```
### Q: 如何清除特定查询的缓存?
A: 使用 queryClient 的相关方法:
```typescript
const queryClient = useQueryClient()
// 使查询失效(会重新获取)
queryClient.invalidateQueries({ queryKey: ['users'] })
// 移除查询(从缓存中删除)
queryClient.removeQueries({ queryKey: ['users'] })
// 重置查询(重置为初始状态)
queryClient.resetQueries({ queryKey: ['users'] })
```