Published on

初识 Vue Query

Authors
  • avatar
    Name
    Deng Hua
    Twitter

目录

构建现代大型应用程序最具挑战性的挑战之一是数据获取。如loading、错误状态、分页、过滤、排序、缓存等功能的添加会增加复杂性,并且经常会因大量样板代码而使应用程序变得臃肿。

这就是 Vue Query 库的用武之地。它使用声明性语法处理和简化数据获取,并在幕后为我们处理所有这些重复性任务。

可以理解为react-query的Vue版本,如果你使用过react-query的话。

认识 Vue Query

Vue 查询不能替代 Axios 或 fetch。它是它们之上的抽象层。

管理服务器状态所面临的挑战与管理客户端状态不同且更加复杂。我们需要解决:

  • 缓存

  • 在后台更新过时的数据

  • 知道数据何时过期

  • 尽快更新数据

  • 将同一数据的多个请求合并为单个请求

  • 性能优化,例如分页和延迟加载

  • 管理服务器状态的内存和垃圾收集

  • 通过结构共享来记忆查询结果

示例

这里模拟了一个日常开发中常见的列表分页加载的场景,包含了loading,分页等基础功能。

这里我省略了<Post>组件和allData假数据的代码,详情可以fork此示例。

<template>
  <div>
    <h1 class="text-lg">Vue Query Simple Demo</h1>
    <button class="mb-10 w-100 rounded-md text-white px-4 py-1 bg-sky-500" @click="nextPage">next page</button>

    <template v-if="isLoading">
      <h3>loading</h3>
    </template>
    <template v-else>
      <Post :list="postList" />
    </template>

  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import allData, { type ExampleData } from './components/data';
import Post from './components/Post.vue'

const isLoading = ref<boolean>(false);
const pageNum = ref<number>(1);
const postList = ref<Array<ExampleData>>([])

const fetchData = (page: number): Promise<ExampleData[]> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(allData[`page${page}`])
    }, 1500);
  })
}

const getData = async (page: number) => {
  isLoading.value = true;
  const data = await fetchData(page);
  isLoading.value = false;

  postList.value = data;
}

onMounted(() => {
  getData(pageNum.value);
})

const nextPage = () => {
  getData(++pageNum.value);
}
</script>

安装Vue Query

目前Vue Query已被Tan Query以MonoRepo方式集合到内部,可直接安装。

先关文档: Tan Query Vue

Vue Query

安装 Vue Query:

pnpm add @tanstack/vue-query

初始化:

import { VueQueryPlugin } from '@tanstack/vue-query'

app.use(VueQueryPlugin)

要使用Vue Query中的Query,使用useQuery钩子即可:

import { useQuery } from '@tanstack/vue-query';
//...

const { isLoading, data: postList } = useQuery({
  queryKey: ['uniqueKey'],
  queryFn: () => fetchData(pageNum.value)
})
// ...

在上面例子中:

  • uniqueKey是用于缓存的唯一标识符

  • queryFn 是一个返回Promise的函数

  • isLoading 表明 Promise 调用是否已完成

  • data 是 Promise中被resolve的值

让我们合并到我们的示例中:

<template>
  <div>
    <h1 class="text-lg">Vue Query Simple Demo</h1>
    <button class="mb-10 w-100 rounded-md text-white px-4 py-1 bg-sky-500">next page</button>

    <template v-if="isLoading">
      <h3 class="flex justify-center">loading</h3>
    </template>
    <template v-else>
      <Post :list="postList" />
    </template>

  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import allData, { type ExampleData } from './components/data';
import Post from './components/Post.vue'
import { useQuery } from '@tanstack/vue-query';

const pageNum = ref<number>(1);

const fetchData = (page: number): Promise<ExampleData[]> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(allData[`page${page}`])
    }, 1500);
  })
}

const { isLoading, data: postList } = useQuery({
  queryKey: ['uniqueKey'],
  queryFn: () => fetchData(pageNum.value)
})

</script>

这里我删除了点击加载下一页数据的逻辑,为了先从简单的例子说明。

可以看到,由于isLoadingdata由Vue Query来处理,因此代码量下降了很多。

tips: useQuery返回属性名为 data的响应值 ,为了重命名它,可以使用es6 解构语法:

const { data: newData } = useQuery(...)

这在执行多个查询时也很用。


queryKey 是什么?

Vue Query 在内部基于queryKey来管理查询缓存。 传递给 Vue Query 的queryKey必须是一个数组。 该数组可以是简单的仅有单个常量字符串的数组,也可以是包含许多嵌套对象及变量字符串的数组。 只要数组的内容是可序列化的,并且对查询的数据来说它是唯一的,那它就是合法的!

对于单次查询,queryKey 最简单形式是一个带有单个常量字符串的数组。在以下情况,我们推荐你使用这种格式:

  • 通用的 List/Index 资源
  • 非分层的(Non-hierarchical)资源
// 通用的 List/Index 资源
useQuery({ queryKey: ['todos'], ... });

// 随便提供一点字符串
useQuery({ queryKey: ['something', 'special'], ... });

对于我们示例中的列表,我们需要再加入一个变量,变量的每次变化都会使缓存的数据失效,进而重新获取数据。

如果你的查询功能依赖于变量,则将其包含在查询键值中

//...
const { isLoading, data: postList } = useQuery({
  queryKey: ['uniqueKey', pageNum],
  queryFn: () => fetchData(pageNum.value)
})

const nextPage = () => {
  pageNum.value += 1;
}
//...

此时,当nextPage函数调用时,会使pageNum值变化,Vue Query缓存的数据失效,从而重新进行数据获取。

完整代码:

<template>
  <div>
    <h1 class="text-lg">Vue Query Simple Demo</h1>
    <button
      class="mb-10 w-100 rounded-md text-white px-4 py-1 bg-sky-500"
      @click="nextPage">
      next page
    </button>

    <template v-if="isLoading">
      <h3 class="flex justify-center">loading</h3>
    </template>
    <template v-else>
      <Post :list="postList" />
    </template>

  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import allData, { type ExampleData } from './components/data';
import Post from './components/Post.vue'
import { useQuery } from '@tanstack/vue-query';

const pageNum = ref<number>(1);

const fetchData = (page: number): Promise<ExampleData[]> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(allData[`page${page}`])
    }, 1500);
  })
}

const { isLoading, data: postList } = useQuery({
  queryKey: ['uniqueKey', pageNum],
  queryFn: () => fetchData(pageNum.value)
})

const nextPage = () => {
  pageNum.value += 1;
}
</script>

默认情况下,缓存的数据被认为是过时的。在以下情况下,它们会在后台自动重新获取:

  • 有新的Query实例被挂载

  • window浏览器重新获得焦点

  • 网络已重新连接

  • query实例配置了re-fetch

此外,失败的查询会以静默方式重试 3 次,并在捕获错误并将其显示到 UI 之前延迟获取。

添加 Error 处理

实际开发中,错误处理应该在 try-catch 块内实现,并且需要一些附加变量来处理错误状态。值得庆幸的是,vue-query 通过提供 isErrorerror 变量提供了一种更直观的方法。

让我们补充一些显示错误信息的html:

<!-- ... -->
<template v-else-if="isError">
  <h3 class="flex justify-center">
    {{ error }}
  </h3>
</template>
<!-- ... -->
// ...
const fetchData = (page: number): Promise<ExampleData[]> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // resolve(allData[`page${page}`])
      reject('something went wrong')
    }, 1000);
  })
}

const { isLoading, data: postList, isError, error } = useQuery({
  queryKey: ['uniqueKey', pageNum],
  queryFn: async () => await fetchData(pageNum.value),
  retry: false
})
//...

当 useQuery 查询失败时(查询函数抛出错误),会重试3次该查询(默认为3次),可以通过全局级别和单个查询级别来配置。这里为了方便演示,设置为不重试。

参考文档:Query Retires


总结

Vue Query 通过用几行直观的逻辑替换复杂臃肿的代码来简化数据fetch。这提高了可维护性。

此外,还有一些高级特性(例如预取、分页查询、相关查询)等,这里篇幅有限不做更多介绍。

End.