- Published on
初识 Vue Query
- Authors
- Name
- Deng Hua
目录
构建现代大型应用程序最具挑战性的挑战之一是数据获取。如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:
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>
这里我删除了点击加载下一页数据的逻辑,为了先从简单的例子说明。
可以看到,由于isLoading
,data
由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 通过提供 isError
和 error
变量提供了一种更直观的方法。
让我们补充一些显示错误信息的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.