Appearance
Vue Query
简介
Vue Query(也称为 TanStack Query) 是一个强大的数据获取和缓存库,专为 Vue 应用设计。它简化了服务器状态管理,提供了声明式的数据获取方式,自动处理加载状态、缓存、数据同步和错误处理等复杂问题。
安装
bash
npm install @tanstack/vue-query
# 或
yarn add @tanstack/vue-query
# 或
pnpm add @tanstack/vue-query基本使用
配置 Vue Query
在 Vue 应用中设置 Vue Query:
typescript
// main.ts
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
const app = createApp(App)
// 注册 Vue Query 插件
app.use(VueQueryPlugin, {
// 可选配置
queryClientConfig: {
defaultOptions: {
queries: {
// 全局查询配置
staleTime: 1000 * 60 * 5, // 数据 5 分钟内视为新鲜
cacheTime: 1000 * 60 * 30, // 未使用的数据缓存 30 分钟
retry: 3, // 失败时重试 3 次
refetchOnWindowFocus: true, // 窗口获得焦点时重新获取数据
},
},
},
})
app.mount('#app')基本查询
使用 useQuery 钩子获取数据:
vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
// 定义获取数据的函数
const fetchUsers = async () => {
const response = await fetch('https://api.example.com/users')
if (!response.ok) {
throw new Error('网络请求失败')
}
return response.json()
}
// 使用 useQuery 钩子
const {
data: users,
isLoading,
isError,
error,
refetch,
} = useQuery({
queryKey: ['users'], // 查询的唯一标识
queryFn: fetchUsers, // 获取数据的函数
})
</script>
<template>
<div>
<h1>用户列表</h1>
<button @click="refetch">刷新数据</button>
<div v-if="isLoading">加载中...</div>
<div v-else-if="isError">加载失败: {{ error?.message }}</div>
<div v-else>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</div>
</template>带参数的查询
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useQuery } from '@tanstack/vue-query'
const userId = ref(1)
const fetchUserById = async (id) => {
const response = await fetch(`https://api.example.com/users/${id}`)
if (!response.ok) {
throw new Error('网络请求失败')
}
return response.json()
}
// 使用带参数的查询
const {
data: user,
isLoading,
isError,
} = useQuery({
queryKey: ['user', userId], // 包含参数的查询键
queryFn: () => fetchUserById(userId.value),
enabled: !!userId.value, // 只有当 userId 有值时才执行查询
})
const changeUser = (id) => {
userId.value = id
}
</script>数据变更
使用 useMutation 进行数据修改
vue
<script setup lang="ts">
import { useMutation, useQueryClient } from '@tanstack/vue-query'
import { ref } from 'vue'
// 表单数据
const newUser = ref({ name: '', email: '' })
// 获取查询客户端实例
const queryClient = useQueryClient()
// 创建用户的 API 调用
const createUser = async (userData) => {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
})
if (!response.ok) {
throw new Error('创建用户失败')
}
return response.json()
}
// 使用 useMutation 钩子
const { mutate, isLoading, isError, error, isSuccess } = useMutation({
mutationFn: createUser,
onSuccess: () => {
// 成功后重新获取用户列表
queryClient.invalidateQueries({ queryKey: ['users'] })
// 重置表单
newUser.value = { name: '', email: '' }
},
})
const handleSubmit = () => {
mutate(newUser.value)
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div>
<label for="name">姓名:</label>
<input id="name" v-model="newUser.name" required />
</div>
<div>
<label for="email">邮箱:</label>
<input id="email" v-model="newUser.email" type="email" required />
</div>
<button type="submit" :disabled="isLoading">{{ isLoading ? '提交中...' : '创建用户' }}</button>
<div v-if="isError" class="error">错误: {{ error?.message }}</div>
<div v-if="isSuccess" class="success">用户创建成功!</div>
</form>
</template>高级功能
查询无效化与重新获取
typescript
// 使查询无效并重新获取
queryClient.invalidateQueries({ queryKey: ['users'] })
// 使特定用户的查询无效
queryClient.invalidateQueries({ queryKey: ['user', userId] })
// 使所有查询无效
queryClient.invalidateQueries()预取数据
typescript
// 预取用户数据
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
// 预取特定用户数据
await queryClient.prefetchQuery({
queryKey: ['user', 1],
queryFn: () => fetchUserById(1),
})手动更新缓存数据
typescript
// 更新缓存中的用户列表
queryClient.setQueryData(['users'], (oldData) => {
// 添加新用户到列表
return [...oldData, newUser]
})
// 直接设置缓存数据
queryClient.setQueryData(['user', 1], {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
})查询取消
typescript
import { useQuery } from '@tanstack/vue-query'
import axios from 'axios'
// 创建可取消的请求
const fetchWithCancel = async ({ signal }) => {
return axios
.get('https://api.example.com/users', {
signal, // 传递 AbortSignal
})
.then((res) => res.data)
}
// 使用可取消的查询
const { data, isLoading } = useQuery({
queryKey: ['users'],
queryFn: fetchWithCancel,
})
// 取消查询
data.value?.cancel()无限查询
vue
<script setup lang="ts">
import { useInfiniteQuery } from '@tanstack/vue-query'
const fetchProjects = async ({ pageParam = 0 }) => {
const response = await fetch(`https://api.example.com/projects?page=${pageParam}&limit=10`)
return response.json()
}
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
getNextPageParam: (lastPage, allPages) => {
// 如果没有更多数据,返回 undefined
return lastPage.hasMore ? allPages.length : undefined
},
})
</script>
<template>
<div>
<h1>项目列表</h1>
<div v-if="isLoading">加载中...</div>
<div v-else>
<div v-for="(group, i) in data?.pages" :key="i">
<div v-for="project in group.projects" :key="project.id">
{{ project.name }}
</div>
</div>
<button @click="fetchNextPage" :disabled="!hasNextPage || isFetchingNextPage">
{{ isFetchingNextPage ? '加载更多...' : hasNextPage ? '加载更多' : '没有更多数据' }}
</button>
</div>
</div>
</template>与 Vue 组合式 API 集成
创建自定义查询钩子
typescript
// useUsers.ts
import { useQuery } from '@tanstack/vue-query'
export function useUsers() {
const fetchUsers = async () => {
const response = await fetch('https://api.example.com/users')
if (!response.ok) {
throw new Error('获取用户失败')
}
return response.json()
}
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
}
// useUser.ts
import { useQuery } from '@tanstack/vue-query'
export function useUser(userId) {
const fetchUser = async () => {
const response = await fetch(`https://api.example.com/users/${userId.value}`)
if (!response.ok) {
throw new Error('获取用户失败')
}
return response.json()
}
return useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
enabled: !!userId.value,
})
}在组件中使用自定义钩子
vue
<script setup lang="ts">
import { useUsers } from '../composables/useUsers'
import { useUser } from '../composables/useUser'
import { ref } from 'vue'
// 使用自定义查询钩子
const { data: users, isLoading: isLoadingUsers } = useUsers()
// 选中的用户 ID
const selectedUserId = ref(null)
// 获取选中的用户详情
const { data: selectedUser, isLoading: isLoadingUser } = useUser(selectedUserId)
const selectUser = (id) => {
selectedUserId.value = id
}
</script>
<template>
<div class="container">
<div class="users-list">
<h2>用户列表</h2>
<div v-if="isLoadingUsers">加载中...</div>
<ul v-else>
<li
v-for="user in users"
:key="user.id"
@click="selectUser(user.id)"
:class="{ active: selectedUserId === user.id }">
{{ user.name }}
</li>
</ul>
</div>
<div class="user-details">
<h2>用户详情</h2>
<div v-if="!selectedUserId">请选择一个用户</div>
<div v-else-if="isLoadingUser">加载中...</div>
<div v-else>
<h3>{{ selectedUser.name }}</h3>
<p>邮箱: {{ selectedUser.email }}</p>
<p>电话: {{ selectedUser.phone }}</p>
<p>网站: {{ selectedUser.website }}</p>
</div>
</div>
</div>
</template>性能优化
查询选项
typescript
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
// 查询配置选项
staleTime: 1000 * 60 * 5, // 数据 5 分钟内视为新鲜
cacheTime: 1000 * 60 * 30, // 未使用的数据缓存 30 分钟
refetchOnWindowFocus: true, // 窗口获得焦点时重新获取
refetchOnMount: true, // 组件挂载时重新获取
refetchOnReconnect: true, // 网络重连时重新获取
retry: 3, // 失败时重试次数
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 重试延迟
keepPreviousData: true, // 加载新数据时保留旧数据
suspense: false, // 是否使用 Suspense
})依赖查询
vue
<script setup lang="ts">
import { ref } from 'vue'
import { useQuery } from '@tanstack/vue-query'
const userId = ref(null)
// 第一个查询获取用户
const { data: user, isLoading: isLoadingUser } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId.value),
enabled: !!userId.value,
})
// 第二个查询依赖于第一个查询的结果
const { data: userPosts, isLoading: isLoadingPosts } = useQuery({
queryKey: ['userPosts', userId],
queryFn: () => fetchUserPosts(userId.value),
// 只有当用户数据加载完成后才执行
enabled: !!user.value,
})
</script>错误处理
全局错误处理
typescript
// main.ts
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
const app = createApp(App)
app.use(VueQueryPlugin, {
queryClientConfig: {
defaultOptions: {
queries: {
// 全局错误处理
onError: (error) => {
console.error('查询错误:', error)
// 可以触发全局错误通知
},
},
mutations: {
// 全局变更错误处理
onError: (error) => {
console.error('变更错误:', error)
// 可以触发全局错误通知
},
},
},
},
})
app.mount('#app')组件级错误处理
vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
onError: (err) => {
// 组件级错误处理
console.error('获取用户失败:', err)
// 可以显示错误通知或执行其他操作
},
})
</script>
<template>
<div>
<div v-if="isError" class="error-banner">
<p>{{ error?.message || '发生错误' }}</p>
<button @click="refetch">重试</button>
</div>
<!-- 其他内容 -->
</div>
</template>与其他库集成
与 Axios 集成
typescript
import axios from 'axios'
import { useQuery, useMutation } from '@tanstack/vue-query'
// 创建 axios 实例
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// 在查询中使用 axios
const useUsers = () => {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const { data } = await api.get('/users')
return data
},
})
}
// 在变更中使用 axios
const useCreateUser = () => {
return useMutation({
mutationFn: async (newUser) => {
const { data } = await api.post('/users', newUser)
return data
},
})
}与 Pinia 集成
typescript
// stores/query.ts
import { defineStore } from 'pinia'
import { useQueryClient } from '@tanstack/vue-query'
export const useQueryStore = defineStore('query', () => {
// 获取 Vue Query 客户端实例
const queryClient = useQueryClient()
// 从缓存中获取数据
const getCachedUsers = () => {
return queryClient.getQueryData(['users'])
}
// 刷新用户数据
const refreshUsers = async () => {
await queryClient.invalidateQueries({ queryKey: ['users'] })
}
// 更新缓存中的用户
const updateUserInCache = (userId, userData) => {
// 更新单个用户
queryClient.setQueryData(['user', userId], userData)
// 更新用户列表中的用户
queryClient.setQueryData(['users'], (oldUsers) => {
if (!oldUsers) return oldUsers
return oldUsers.map((user) => (user.id === userId ? { ...user, ...userData } : user))
})
}
return {
getCachedUsers,
refreshUsers,
updateUserInCache,
}
})实际应用示例
完整的数据管理示例
vue
<!-- UserManager.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
import UserForm from './UserForm.vue'
import UserList from './UserList.vue'
import UserDetails from './UserDetails.vue'
// 获取查询客户端
const queryClient = useQueryClient()
// 选中的用户 ID
const selectedUserId = ref(null)
// 获取用户列表
const {
data: users,
isLoading: isLoadingUsers,
isError: isUsersError,
error: usersError,
refetch: refetchUsers,
} = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('https://api.example.com/users')
if (!response.ok) throw new Error('获取用户列表失败')
return response.json()
},
})
// 获取选中用户详情
const { data: selectedUser, isLoading: isLoadingUser } = useQuery({
queryKey: ['user', selectedUserId],
queryFn: async () => {
const response = await fetch(`https://api.example.com/users/${selectedUserId.value}`)
if (!response.ok) throw new Error('获取用户详情失败')
return response.json()
},
enabled: !!selectedUserId.value,
})
// 创建用户
const { mutate: createUser, isLoading: isCreating } = useMutation({
mutationFn: async (newUser) => {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newUser),
})
if (!response.ok) throw new Error('创建用户失败')
return response.json()
},
onSuccess: () => {
// 刷新用户列表
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
// 更新用户
const { mutate: updateUser, isLoading: isUpdating } = useMutation({
mutationFn: async ({ id, ...data }) => {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) throw new Error('更新用户失败')
return response.json()
},
onSuccess: (data) => {
// 更新缓存
queryClient.setQueryData(['user', data.id], data)
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
// 删除用户
const { mutate: deleteUser, isLoading: isDeleting } = useMutation({
mutationFn: async (id) => {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'DELETE',
})
if (!response.ok) throw new Error('删除用户失败')
return id
},
onSuccess: (deletedId) => {
// 如果删除的是当前选中的用户,清除选择
if (selectedUserId.value === deletedId) {
selectedUserId.value = null
}
// 刷新用户列表
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
const handleSelectUser = (id) => {
selectedUserId.value = id
}
const handleCreateUser = (userData) => {
createUser(userData)
}
const handleUpdateUser = (userData) => {
updateUser(userData)
}
const handleDeleteUser = (id) => {
deleteUser(id)
}
</script>
<template>
<div class="user-manager">
<h1>用户管理系统</h1>
<div class="error-container" v-if="isUsersError">
<div class="error-message">{{ usersError?.message }}</div>
<button @click="refetchUsers">重试</button>
</div>
<div class="layout">
<!-- 用户列表 -->
<UserList
:users="users"
:isLoading="isLoadingUsers"
:selectedId="selectedUserId"
@select="handleSelectUser"
@delete="handleDeleteUser" />
<!-- 用户详情 -->
<UserDetails
v-if="selectedUserId"
:user="selectedUser"
:isLoading="isLoadingUser"
:isUpdating="isUpdating"
@update="handleUpdateUser" />
<!-- 创建用户表单 -->
<UserForm :isCreating="isCreating" @create="handleCreateUser" />
</div>
</div>
</template>