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

405 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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'] })
```