Skip to content
On this page

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>

要保持清醒 永远不抱有意外的幻想 凭空的期待最要命