前置假设:
- 你已经安装了
@tanstack/vue-query
。 - 你已经在项目的入口文件(如
main.ts
)中设置好了QueryClient
和VueQueryPlugin
(如之前介绍中所示)。 - 你有一个发送 HTTP 请求的方式(这里以
axios
为例,但fetch
或其他库同样适用)。假设axios
已配置好,并在需要的地方导入。
示例 1:获取并展示列表数据 (例如:待办事项列表)
这是最基础的用法。
vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query';
import axios from 'axios'; // 假设 axios 已配置
// 定义获取待办事项列表的函数
const fetchTodos = async () => {
console.log('发起请求获取 Todos...'); // 方便观察请求时机
const response = await axios.get('/api/todos');
return response.data; // Vue Query 需要 Promise 返回实际数据
};
// 使用 useQuery Hook
const {
data: todos, // 响应式的 ref,持有获取到的数据 (或 undefined)
status, // 查询状态: 'pending', 'error', 'success'
isLoading, // 响应式的 ref: 初始加载时为 true (v5 前常用)
isPending, // 响应式的 ref: 初始加载时为 true (v5 推荐)
isFetching, // 响应式的 ref: 任何获取过程中都为 true (包括后台刷新)
isError, // 响应式的 ref: 获取失败时为 true
error, // 响应式的 ref: 持有错误对象
refetch // 手动触发重新获取的函数
} = useQuery({
queryKey: ['todos'], // 必需:此查询的唯一标识键 (数组或字符串)
queryFn: fetchTodos, // 必需:用于获取数据的异步函数
// 可选配置:如果数据不经常变动,可以增加 staleTime
// staleTime: 5 * 60 * 1000, // 数据在 5 分钟内视为"新鲜",不会因窗口聚焦等自动刷新
});
</script>
<template>
<div>
<h2>待办事项列表</h2>
<!-- 加载状态 -->
<div v-if="isPending">正在加载待办事项...</div>
<!-- 错误状态 -->
<div v-else-if="isError">发生错误: {{ error?.message }}</div>
<!-- 成功状态 -->
<ul v-else-if="todos">
<li v-for="todo in todos" :key="todo.id">
{{ todo.title }} - {{ todo.completed ? '已完成' : '待办' }}
</li>
</ul>
<!-- 无数据 -->
<div v-else>暂无待办事项。</div>
<!-- 后台刷新指示器 -->
<div v-if="isFetching && !isPending" class="fetching-indicator">
正在后台更新...
</div>
<!-- 手动刷新按钮 -->
<button @click="refetch()" :disabled="isFetching">
{{ isFetching ? '刷新中...' : '手动刷新' }}
</button>
</div>
</template>
<style scoped>
.fetching-indicator {
position: fixed;
bottom: 1rem;
right: 1rem;
background-color: lightblue;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.8em;
}
</style>
说明:
queryKey: ['todos']
: 唯一标识了这个列表数据。如果其他地方也用这个 key 调用useQuery
,它们会共享缓存和状态。queryFn: fetchTodos
: 告诉 Vue Query 如何获取数据。- Vue Query 自动管理了
isPending
,isError
,data
等响应式状态,可以直接在模板中使用。 isFetching
表示任何获取过程,包括 Stale-While-Revalidate 或窗口聚焦触发的后台刷新。
示例 2:根据路由参数获取项目详情
这很常见,比如 /todos/1
显示 ID 为 1 的待办事项。
vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query';
import { useRoute } from 'vue-router';
import { computed, watch } from 'vue';
import axios from 'axios';
const route = useRoute();
// 使用 computed 来响应式地获取路由参数 ID
const todoId = computed(() => route.params.id as string | undefined);
// 获取函数现在需要接收 ID
const fetchTodoById = async (id: string) => {
// 注意:最好处理 id 不存在或无效的情况
if (!id || Number.isNaN(parseInt(id, 10))) {
// 可以返回 null, undefined, 或抛出错误,取决于你希望如何处理
console.warn('无效的 Todo ID:', id);
return null;
// 或者 throw new Error('无效的 ID');
}
console.log(`发起请求获取 Todo ID: ${id}...`);
const response = await axios.get(`/api/todos/${id}`);
return response.data;
};
const {
data: todo,
isLoading, // 还是用 isLoading 吧,更清晰区分首次加载
isError,
error,
status, // 可以直接用 status 判断更细致的状态
// isFetching, // 如果需要后台刷新指示器也可以用
} = useQuery({
// Query Key 现在包含动态 ID。
// 当 todoId.value 变化时,Vue Query 会自动识别为不同的查询并获取数据!
queryKey: ['todo', todoId],
// queryFn 会接收一个包含 queryKey 的上下文对象,但直接用闭包访问 todoId 更方便
queryFn: () => fetchTodoById(todoId.value!), // 传递当前的 ID
// 只有当 ID 存在时才自动执行查询
enabled: computed(() => !!todoId.value && !Number.isNaN(parseInt(todoId.value, 10))),
});
// (可选) 监听 status 或 error 变化来执行副作用,如提示
watch(status, (newStatus) => {
console.log('Todo query status changed:', newStatus);
});
</script>
<template>
<div>
<h2>待办事项详情</h2>
<div v-if="isLoading">正在加载详情...</div>
<div v-else-if="isError">错误: {{ error?.message }}</div>
<div v-else-if="todo">
<h3>{{ todo.title }}</h3>
<p>状态: {{ todo.completed ? '已完成' : '待办' }}</p>
<p>ID: {{ todo.id }}</p>
</div>
<!-- 处理 fetchTodoById 返回 null 或 enabled 为 false 的情况 -->
<div v-else-if="!todoId">请提供有效的待办事项 ID。</div>
<div v-else>未找到该待办事项。</div>
</div>
</template>
说明:
queryKey: ['todo', todoId]
: 关键在于将响应式的todoId
(一个computed
ref) 放入queryKey
。当路由变化导致todoId.value
更新时,Vue Query 会自动检测到queryKey
的变化,并为新的 ID 获取数据(或使用该 ID 的缓存)。enabled: computed(() => !!todoId.value && ...)
: 这是一个非常有用的选项,它确保只有当todoId
有效时,查询才会自动执行,避免了在 ID 无效时发起不必要的 API 调用。
示例 3:添加项目 (Mutation + 缓存失效)
执行一个会改变数据的操作(如 POST 请求),然后更新列表视图。
vue
<script setup lang="ts">
import { ref } from 'vue';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';
const queryClient = useQueryClient(); // 获取 QueryClient 实例,用于与缓存交互
const newTodoTitle = ref('');
// 定义执行变更的异步函数 (添加新的 todo)
const addTodoMutationFn = async (title: string) => {
console.log('发起请求添加 Todo:', title);
const response = await axios.post('/api/todos', { title, completed: false });
return response.data;
};
// 使用 useMutation Hook
const {
mutate: addTodo, // 调用此函数来触发变更操作 (不返回 Promise)
// mutateAsync: addTodoAsync, // 如果需要 Promise,用这个
status: addStatus, // 变更的状态: 'idle', 'pending', 'error', 'success'
isPending: isAddingTodo, // 是否正在执行变更
isError: isAddError,
error: addError,
data: addedTodoData, // 变更成功后 mutationFn 返回的数据
} = useMutation({
mutationFn: addTodoMutationFn,
// --- 关键:成功回调 ---
onSuccess: (newData, variables, context) => {
// 变更成功!最常用的策略:使 'todos' 查询失效。
// 这会告诉 Vue Query 与 ['todos'] 键关联的数据现在"不新鲜"了。
// 所有活跃的 useQuery(['todos']) 实例会自动在后台重新获取数据。
console.log('待办事项添加成功:', newData, ',原始变量:', variables);
// 使列表查询失效
queryClient.invalidateQueries({ queryKey: ['todos'] });
// --- 另一种选择:手动更新缓存 (更即时,但可能更复杂) ---
// 如果你想避免重新请求列表,可以手动更新缓存:
// queryClient.setQueryData(['todos'], (oldData: any[] | undefined) => {
// // 确保 oldData 是数组
// const currentData = Array.isArray(oldData) ? oldData : [];
// return [...currentData, newData]; // 返回更新后的数组
// });
newTodoTitle.value = ''; // 清空输入框
},
// --- 错误处理 ---
onError: (error, variables, context) => {
console.error('添加待办事项失败:', error, ',提交的数据:', variables);
// 可以在这里显示错误提示给用户
alert(`添加失败: ${error.message}`);
},
// onSettled: (data, error, variables, context) => {
// // 无论成功或失败都会执行
// console.log('添加操作完成');
// }
});
// 提交表单的处理函数
const submitNewTodo = () => {
const title = newTodoTitle.value.trim();
if (title) {
addTodo(title); // 调用 mutate 函数,传入需要的数据
}
};
</script>
<template>
<div>
<h3>添加新的待办事项</h3>
<form @submit.prevent="submitNewTodo">
<input type="text" v-model="newTodoTitle" placeholder="输入待办事项标题" :disabled="isAddingTodo" />
<button type="submit" :disabled="isAddingTodo">
{{ isAddingTodo ? '添加中...' : '添加' }}
</button>
</form>
<div v-if="isAddError" style="color: red;">
添加错误: {{ addError?.message }}
</div>
<!-- 列表组件 (如示例 1) 会因为 ['todos'] 查询失效并重新获取而自动更新 -->
</div>
</template>
说明:
useMutation
: 用于执行会改变服务器数据的操作。mutationFn
: 执行实际的 POST/PUT/DELETE 请求。mutate
: 调用这个函数来启动变更过程,并传入需要提交的数据。onSuccess
: 这是核心 。变更成功后:queryClient.invalidateQueries({ queryKey: ['todos'] })
: 这是最常用也通常推荐的方式。它告诉 Vue Query['todos']
这个 key 的数据可能过期了。任何正在使用这个 key 的useQuery
都会自动安排一次后台刷新,从而让列表 UI 更新。简单、可靠。- 注释掉的
setQueryData
展示了手动更新缓存的方式,可以提供更即时的 UI 反馈(无需等待刷新),但需要你精确地维护缓存数据结构。
示例 4:删除项目 (Mutation + 乐观更新)
乐观更新通过在服务器确认前就更新 UI,来提供更流畅的用户体验。
vue
<script setup lang="ts">
import { useMutation, useQueryClient } from '@tanstack/vue-query';
import axios from 'axios';
// 假设这是 TodoItem 组件的一部分,接收一个 todo 对象作为 prop
interface Todo {
id: number;
title: string;
completed: boolean;
}
const props = defineProps<{ todo: Todo }>();
const queryClient = useQueryClient();
// 定义删除操作的函数
const deleteTodoMutationFn = async (id: number) => {
console.log(`发起请求删除 Todo ID: ${id}...`);
// DELETE 请求通常不返回内容,或者只返回状态码
await axios.delete(`/api/todos/${id}`);
};
// 使用 useMutation,配置乐观更新
const { mutate: deleteTodo, isPending: isDeleting } = useMutation({
mutationFn: deleteTodoMutationFn,
// --- 乐观更新逻辑 ---
onMutate: async (idToDelete) => {
console.log(`乐观更新:尝试删除 ID: ${idToDelete}`);
// 1. 取消可能正在进行的针对 'todos' 列表的重新获取请求
// 这可以防止它们覆盖我们的乐观更新。
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 2. 获取当前的 'todos' 列表缓存快照
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
// 3. 乐观地从缓存中移除这一项
if (previousTodos) {
queryClient.setQueryData<Todo[]>(
['todos'], // 要更新的缓存 key
// 更新函数:接收旧数据,返回新数据
(old) => old?.filter(todo => todo.id !== idToDelete) ?? []
);
console.log('乐观更新:UI 上的列表已更新');
} else {
console.log('乐观更新:未找到缓存数据,无法进行乐观更新');
}
// 4. 返回一个包含快照的上下文对象,以便在出错时回滚
return { previousTodos };
},
// --- 错误处理:回滚 ---
onError: (err, idToDelete, context) => {
console.error("删除失败,需要回滚:", err);
// 如果 onMutate 中成功获取了快照,则用它来恢复缓存
if (context?.previousTodos) {
console.log('回滚 UI...');
queryClient.setQueryData<Todo[]>(['todos'], context.previousTodos);
}
// 向用户显示错误信息
alert(`删除失败: ${err.message}`);
},
// --- 最终处理:确保一致性 ---
onSettled: (data, error, idToDelete, context) => {
// 无论删除成功还是失败,最终都要重新从服务器获取一次 'todos' 列表
// 来确保客户端状态与服务器状态最终一致。
console.log(`删除操作完成 (成功或失败),重新验证 'todos' 列表`);
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
// 处理删除按钮点击
const handleDelete = () => {
// 可以加一个确认框
if (confirm(`确定要删除 "${props.todo.title}" 吗?`)) {
deleteTodo(props.todo.id); // 调用 mutate 触发删除
}
};
</script>
<template>
<li>
{{ todo.title }}
<button @click="handleDelete" :disabled="isDeleting" style="margin-left: 10px;">
{{ isDeleting ? '删除中...' : '删除' }}
</button>
</li>
</template>
说明:
- 乐观更新 (Optimistic Update): 核心在于
onMutate
。UI 会在用户点击删除后立即移除该项,感觉非常流畅。 onMutate
:cancelQueries
: 防止后台刷新覆盖乐观更新。getQueryData
: 获取当前缓存。setQueryData
: 立即修改缓存,移除对应项。- 返回
context
: 保存原始数据用于回滚。
onError
: 如果服务器删除失败,使用context.previousTodos
通过setQueryData
将缓存恢复到原始状态。onSettled
: 极其重要 。无论成功或失败,最后都要调用invalidateQueries({ queryKey: ['todos'] })
。这确保了客户端最终会与服务器的真实状态同步,修正了乐观更新可能带来的(暂时性)不一致。
这些示例覆盖了 Vue Query 最常用的一些场景。关键在于理解 queryKey
的作用、useQuery
和 useMutation
的基本用法,以及如何利用 QueryClient
的方法(尤其是 invalidateQueries
和 setQueryData
)来管理缓存和状态更新。根据你的具体需求调整这些模式即可。