Vue Query 实例解析:轻松掌握核心用法

前置假设:

  • 你已经安装了 @tanstack/vue-query
  • 你已经在项目的入口文件(如 main.ts)中设置好了 QueryClientVueQueryPlugin(如之前介绍中所示)。
  • 你有一个发送 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 的作用、useQueryuseMutation 的基本用法,以及如何利用 QueryClient 的方法(尤其是 invalidateQueriessetQueryData)来管理缓存和状态更新。根据你的具体需求调整这些模式即可。

相关推荐
z-robot2 分钟前
Nginx 配置代理
前端
用户479492835691510 分钟前
Safari 中文输入法的诡异 Bug:为什么输入 @ 会变成 @@? ## 开头 做 @ 提及功能的时候,测试同学用 Safari 测出了个奇怪的问题
前端·javascript·浏览器
没有故事、有酒22 分钟前
Ajax介绍
前端·ajax·okhttp
朝新_26 分钟前
【SpringMVC】详解用户登录前后端交互流程:AJAX 异步通信与 Session 机制实战
前端·笔记·spring·ajax·交互·javaee
裴嘉靖28 分钟前
Vue 生成 PDF 完整教程
前端·vue.js·pdf
毕设小屋vx ylw28242630 分钟前
Java开发、Java Web应用、前端技术及Vue项目
java·前端·vue.js
冴羽1 小时前
今日苹果 App Store 前端源码泄露,赶紧 fork 一份看看
前端·javascript·typescript
蒜香拿铁1 小时前
Angular【router路由】
前端·javascript·angular.js
brzhang2 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构
西洼工作室2 小时前
高效管理搜索历史:Vue持久化实践
前端·javascript·vue.js