Vue Query从入门到实战踩坑总结
Vue Query配置的最佳姿势,折腾半天才搞明白
刚开始用Vue Query的时候,我是直接按照官方文档来配的,结果项目一上线就发现各种问题。最大的坑就是缓存策略没搞对,用户刷新页面后数据还是老的,体验差得要命。
我的写法,亲测靠谱:
import { QueryClient, QueryClientProvider } from '@tanstack/vue-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5分钟变stale
cacheTime: 10 * 60 * 1000, // 10分钟后清理缓存
refetchOnWindowFocus: false, // 关闭窗口聚焦自动刷新
retry: 1,
retryDelay: 1000
}
}
})
// 在main.js里注入
app.use(queryClientProvider, { client: queryClient })
为什么要这么配置?staleTime设置成5分钟,意思是数据超过5分钟就认为过期,这时候如果有组件需要这个数据,就会触发重新请求。cacheTime设置比staleTime长一点,避免刚过期就被清除。
refetchOnWindowFocus这个参数特别坑,移动端切换到后台再回来,所有查询都会重新发起,网络请求爆增,我之前就这样被产品骂过。
这种写法更靠谱:自定义Query Key管理
刚开始我把query key直接写在组件里,比如['user', id]这样,结果项目一大,key到处都是,维护起来要疯。
我现在的做法是专门建一个文件管理所有的key:
// composables/queryKeys.js
export const queryKeys = {
user: (id) => ['user', id],
posts: (userId, page = 1) => ['posts', userId, page],
comments: (postId) => ['comments', postId],
notifications: ['notifications'],
profile: () => ['profile']
}
// 在组件中使用
import { useQuery } from '@tanstack/vue-query'
import { queryKeys } from '@/composables/queryKeys'
const { data, isLoading } = useQuery({
queryKey: queryKeys.user(userId),
queryFn: () => fetchUser(userId)
})
这样做的好处显而易见:集中管理,不容易写错,重构也方便。而且IDE还能帮你做自动补全,爽得很。
这几种错误写法,别再踩坑了
最常见的错误就是把复杂的对象直接作为query key的一部分:
// 错误示范 - 千万别这么写
const { data } = useQuery({
queryKey: ['search', searchParams], // searchParams是个复杂对象
queryFn: () => search(searchParams)
})
这样写的问题是,每次render时searchParams引用都变了,即使内容没变也会被认为是不同的key,导致重复请求。正确做法应该是只取关键字段或者转成JSON字符串:
// 正确写法
const { data } = useQuery({
queryKey: ['search', JSON.stringify(searchParams)],
queryFn: () => search(searchParams)
})
// 或者只取关键字段
const { data } = useQuery({
queryKey: ['search', searchParams.keyword, searchParams.category],
queryFn: () => search(searchParams)
})
另一个常见错误是在queryFn里直接引用组件内的响应式数据:
// 错误示范
export default {
setup() {
const filters = ref({ status: 'active', type: 'article' })
const { data } = useQuery({
queryKey: ['list', filters.value], // 这里有问题
queryFn: async () => {
return await api.getList(filters.value) // filters可能变化
}
})
}
}
这样写可能导致queryFn里的filters不是最新的值。正确的做法是把依赖项明确写在queryKey里:
// 正确写法
export default {
setup() {
const filters = ref({ status: 'active', type: 'article' })
const { data } = useQuery({
queryKey: ['list', filters.value.status, filters.value.type],
queryFn: async () => {
return await api.getList(filters.value)
}
})
}
}
实际项目中的坑:错误处理和loading状态
Vue Query的错误处理机制挺好的,但实际用的时候需要注意一些细节。特别是对于那种”用户不存在”的场景,不应该当作错误处理:
const { data, error, isLoading } = useQuery({
queryKey: ['user', id],
queryFn: async ({ signal }) => {
try {
const response = await fetch(https://jztheme.com/api/user/${id}, { signal })
if (!response.ok) {
if (response.status === 404) {
return null // 404不算是错误,返回null
}
throw new Error(HTTP ${response.status})
}
return response.json()
} catch (error) {
if (error.name !== 'AbortError') {
throw error
}
}
},
// 只有真正的错误才retry
retry: (failureCount, error) => {
return error.message.includes('HTTP') && !error.message.includes('404')
}
})
loading状态的处理也有讲究。对于分页列表,我不希望每次翻页都显示整个页面的loading,只希望当前区域loading:
<template>
<div>
<!-- 首次加载才显示整体loading -->
<div v-if="isLoading && !data">
<LoadingSpinner />
</div>
<!-- 数据存在时,翻页只显示局部loading -->
<div v-else>
<PostList :posts="data?.items || []" />
<button
@click="fetchNextPage"
:disabled="isFetchingNextPage"
>
{{ isFetchingNextPage ? '加载中...' : '加载更多' }}
</button>
</div>
</div>
</template>
<script setup>
import { useInfiniteQuery } from '@tanstack/vue-query'
const {
data,
isLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor
})
</script>
这里的关键是区分isLoading和isFetchingNextPage。isLoading只在首次获取数据时为true,而isFetchingNextPage在每次fetchMore时都为true,这样就能实现局部loading的效果。
缓存预填充和手动更新,提升用户体验
对于那些频繁更新的数据,我会结合WebSocket或者Server-Sent Events来手动更新缓存,避免频繁的轮询请求:
// 在WebSocket消息处理的地方
const { queryClient } = useQueryClient()
// 当收到新消息时更新缓存
socket.on('newMessage', (message) => {
queryClient.setQueryData(['messages'], (old) => {
if (!old) return [message]
return [message, ...old]
})
// 同时更新对应用户的未读消息数
queryClient.setQueryData(['unread-count', message.userId], (count) => count + 1)
})
// 或者直接无效化相关查询,让它下次自动重新获取
socket.on('postUpdated', (postId) => {
queryClient.invalidateQueries({ queryKey: ['post', postId] })
queryClient.invalidateQueries({ queryKey: ['feed'] })
})
这种做法能让UI瞬间响应,用户体验比等API返回要好太多。但要注意更新时机,避免更新过快导致界面闪烁。
内存泄漏和垃圾回收,不能忽视的问题
大型应用中,如果查询很多,缓存会占用大量内存。我一般会给全局配置加一些限制:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
cacheTime: 5 * 60 * 1000, // 5分钟后自动清理
}
},
// 控制最大缓存数量
queryCache: new QueryCache({
onFocus: () => console.log('focus'),
onOnline: () => console.log('online')
})
})
// 页面销毁时手动清理不需要的缓存
onUnmounted(() => {
queryClient.removeQueries({
queryKey: ['temporary-data'],
exact: true
})
})
另外,在路由切换时,有些页面级别的查询可以立即清理:
// 路由守卫中清理缓存
router.beforeEach((to, from) => {
// 清理上个页面的临时查询
queryClient.removeQueries({
queryKey: ['page-data', from.params.id]
})
})
以上是我使用Vue Query这么久总结的最佳实践,包括配置、错误处理、缓存管理等几个方面。有些方案不是最优的,但确实解决了我遇到的实际问题。有更好的实现方式欢迎评论区交流。

暂无评论