@Tanstack/vue-query 的使用介绍
前言
在今年的vue conf 会议上,提到了vue-query
这个库,这里对它的基本使用做一个介绍。
会议资料地址: https://vueconf.cn/
Tanstack-query
的前身是react-query
,是一个本地的服务端状态管理的库,常和“乐观更新”配合使用。它会在本地维护一个基于内存的全局状态管理,当缓存中有数据的时候,优先使用缓存数据来展示,可以让用户获得非常流畅的体验(前提是网络状态正常,如果网络很差,可能长时间显示错误的数据)。同时后台请求数据,来保证数据的正确性。
tanstack-query的优势:
https://tanstack.com/query/latest/docs/framework/vue/typescript
- 专用的devtools
- 自动缓存请求过的数据
- 自动后台查询
- 窗口聚焦自动重新获取
- 轮询/实时查询
- 并行查询
- 关联查询
- 支持mutation API
- 分页查询
- 无限滚动
- 请求取消
- 自动垃圾回收
- 错误重试
- 数据预取
- 支持初始化数据和占位数据
- 自动网络监控
- SSR 支持
- …
简单的demo
可以从官网获取线上demo
进行测试。
https://codesandbox.io/s/github/tanstack/query/tree/main/examples/vue/basic?from-embed=&file=/src/App.vue
🎯 线上demo
热更新响应速度比较慢,而且不可以post
更改数据。
可以使用本地启动demo项目,增加了json-server
,因此可以使用restful
风格的api
实现增删改查的功能。
demo
地址:https://github.com/hekang456/vue-query-example
如果需要使用json-server
发其他请求,参考https://github.com/typicode/json-server
使用方式
这里使用最简单的代码覆盖最常见的使用场景,其他场景见文档。
1、获取数据
使用useQuery
获取数据,需要设置queryKey
和queryFn
。queryKey
只要是可以序列化的数组即可,但需要保证全局唯一;queryFn
是一个返回Promise
的函数。
import { useQuery } from '@tanstack/vue-query';
const { isPending, isError, isFetching, data, error, refetch } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsApi
});
当页面打开的时候,自动请求了 ["posts"]
接口,注意看 devtools
中数据的状态,从 Fetching
(获取中)变成了 Stale
(已过期)。
这是默认的配置,数据获取之后立刻变成“已过期”,这样当触发了一些条件的时候就会立马重新请求,比如页面聚焦、页面重新变成活跃状态等。
2、页面聚焦/页面重新变成活跃重新请求数据
这里以页面重新变成活跃为例。
从Posts.vue
页面,切换到Post.vue
页面,可以看到请求了 ["post", "1"]
的数据, ["post", "1"]
也是从 Fetching
变成了 Stale
,而["posts"]
数据状态变成了inactive
(非活跃)。
当点击back
回到Posts.vue
页面的时候,立刻展示了缓存中的数据,同时 ["posts"]
对应的接口立马重新请求后端。因为数据没有变化,所以页面也没有改变,提供了流畅的体验。
3、自动缓存
我们以第一条数据为例
当数据请求过之后,就会缓存起来,当再次显示时,会直接展示缓存中的数据,同时取请求后端数据。如果数据没有变化,页面也没有改变;否则修改页面。
4、修改过期时间
默认情况下,数据请求后会立刻获取,但是某些数据,开发者明确知道它很长时间都不会更新,这时候就可以设置过期时间。
这里单独修改["posts"]
的过期时间.
const { isPending, isError, isFetching, data, error, refetch } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsApi,
// 一小时
staleTime: 10 * 60 * 1000,
});
可以看到,["posts"]
数据不再会立刻过期,哪怕页面重新变成活跃状态,也只是使用缓存数据渲染,而没有请求新的数据。
5、手动刷新
调用refresh
方法,来手动刷新接口,这里以["posts"]
接口为例。
在Posts.vue
文件中,增加一个按钮,调用uesQuery
返回的refresh
方法。
<button @click="refetch">refresh Posts</button>
每次调用refresh
,都会发送一个新的请求,即使数据还是Fresh
状态。
6、数据轮询
增加refetchInterval
属性。如果配合enable
属性,可以方便的控制轮询暂停和继续。见 9。
const { isPending, isError, isFetching, data, error, refetch } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsApi,
staleTime: 10 * 60 * 1000,
// 2s轮询一次接口
refetchInterval: 2 * 1000
});
这是一个非常实用的功能,避免了手写轮询带来的复杂代码逻辑。
7、自动重试
如果接口报错,默认会自动重试,但是只在get
接口生效。这是为了防止其他类型的请求会多次修改后端数据。
因为前端报错的原因是多种的,比如后端处理时间太长,导致前端超时报错。但只要请求发到了后端,就会执行操作。
为了演示这种情况,需要让接口报错。我们改一下fetchPostsApi
,让它报错。
export const fetchPostsApi = async (): Promise<Post[]> =>
await fetch('http://localhost:3000/posts').then((response) => {
// return response.json();
throw new Error('error');
});
可以看到,如果接口报错,会默认重试三次,并且重试的间隔时间翻倍。在重试过程中,数据一致处于pending
状态,最后变成了error
状态。
8、数据预取
假设当数据移动到某一条post
数据上时,用户大概率是会点击查看的。这时候可以对这条数据预取,用户点击时可以及时显示内容。
在每一条数据上都绑定一个mouseenter
事件,实现这个功能需要queryClient
来实现。
需要指定唯一的queryKey
,这个key
和Post.vue
组件中的请求方法是一样的,所以二者可以共用一份缓存数据。
<script lang="ts" setup>
import { useQuery, useQueryClient } from '@tanstack/vue-query';
import { fetchPostsApi, fetchPostApi } from './fetch';
// ......
const queryClient = useQueryClient();
const prefetchPost = async (id: number) => {
await queryClient.prefetchQuery({
queryKey: ['post', id],
queryFn: () => fetchPostApi(id)
});
};
</script>
<template>
<h1>Posts</h1>
// ......
<div v-else-if="data">
<ul>
<li v-for="item in data" :key="item.id">
<a
@mouseenter="prefetchPost(item.id)"
@click="$emit('setPostId', item.id)"
href="#"
:class="{ visited: isVisited(item.id) }"
>{{ item.title }}</a
>
</li>
</ul>
</div>
</template>
当打开详情时,数据是瞬间展示的,并没有loading
的过程。
9、控制数据获取的时机
有时候我们不希望数据立刻获取,而是等到触发了某些条件的时候在获取。
比如,我们假设["post"]
中的数据,不是进入页面就需要获取,而是一些条件满足后才可以请求。以一个变量为例。
<script>
const validate = ref(false)
const { isPending, isError, isFetching, data, error, refetch } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsApi,
// 只有验证通过,才可以发请求
enabled: validate,
});
</script>
<template>
<button @click="validate = true">validate</button>
</template>
开始时,请求处于disabled
状态,没有发送,直到点击了按钮,请求才发出。
10、关联请求
依然是用enable
属性来实现的,比如官方的例子中,只有user
有id
属性,才去发出请求。
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => getUserByEmail(email.value),
})
const userId = computed(() => user.value?.id)
const enabled = computed(() => !!user.value?.id)
// Then get the user's projects
const { isIdle, data: projects } = useQuery({
queryKey: ['projects', userId],
queryFn: () => getProjectsByUser(userId.value),
enabled, // The query will not execute until `enabled == true`
})
11、并行请求
多个请求之间,默认是并行的,比如:
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
同时,对于同一类请求,可以使用useQueries
来并行请求。
const users = computed(...)
const queries = computed(() => users.value.map(user => {
return {
queryKey: ['user', user.id],
queryFn: () => fetchUserById(user.id),
}
})
);
const userQueries = useQueries({queries: queries})
12、分页请求
需要修改App.vue
文件,把分页组件显示出来。具体代码可以看代码仓库中的写法。
<Post v-if="postId > -1" :postId="postId" @setPostId="setPostId" />
<!-- <Posts v-else :isVisited="isVisited" @setPostId="setPostId" /> -->
<PagePosts v-else />
可以看到,请求下一页,在请求新接口的过程中,数据依然是使用上一页的,而没有展示loading
。请求成功后进行替换。
而点击上一页,会直接用缓存数据渲染,同时后台发出请求,如果数据一致,页面UI不发生变化。
这是乐观更新的一种实现。
13、修改数据
修改数据的功能比较简单,我们给列表增加一个数据。
修改App.vue
文件,增加以下代码,调用useMutation
。
插入数据成功后,手动让["post"]
数据缓存失效,这样的话,这个接口会重新发送请求。exact: true
意思是精确匹配,否则数组中包含posts
的所有缓存都会失效
<script lang="ts" setup>
import { addPostApi } from './fetch';
import { useMutation, useQueryClient } from '@tanstack/vue-query';
const queryClient = useQueryClient();
const { isLoading, mutate } = useMutation({
mutationFn: addPostApi,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'], exact: true });
}
});
const addPost = () => {
mutate({
userId: 1,
id: Date.now(),
title: 'bbbbbb',
body: 'bbbbbb'
});
};
</script>
<template>
<button @click="addPost">add one postId</button>
</template>
这里点击按钮后,发送了两个请求,第一个post
请求,插入一条数据,第二个是onSuccess
回调中让列表缓存失效,从而重新请求列表数据
🚀 从这里也可以看出来,App.vue
文件中并没有Post.vue
组件的数据,但是只要修改对应的queryKey
,同样可以影响到Post.vue
组件。这避免了数据逐级传递和父组件管理全部子组件数据带来的复杂度问题。
14、初始数据和占位数据
先看初始数据,initialData
的数据被认为是有效的,可以存储在缓存中,为了显示效果,给它设置一个过期时间。
const { isPending, isError, isFetching, data, error, refetch } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsApi,
staleTime: 1000 * 60,
initialData: [
{
id: 10000,
userId: 1,
title: 'First Post',
body: 'Hello World'
}
]
});
可以看到,数据不过期,就不会请求接口
再看一下占位数据的表现。
const { isPending, isError, isFetching, data, error, refetch } = useQuery({
queryKey: ['posts'],
queryFn: fetchPostsApi,
staleTime: 1000 * 60,
placeholderData: [
{
id: 10000,
userId: 1,
title: 'First Post',
body: 'Hello World'
}
]
});
占位数据不会缓存到内存中,当真正的数据返回时,占位数据会被替换。
15、网络状态和数据状态
数据状态:
- pending
- success
- error
网络状态:
- fetching
- paused
- idle
🚁 数据状态和网络状态并不总是对应,数据状态对应的是缓存中的数据状态,网络状态对应的是当次网络请求。
举个例子:
1 假如["post"]
接口对应的数据已经请求成功了,那么数据状态是success
,网络状态是idle
;如果这个请求被再次激活,数据状态依然是success
,网络状态是fetching --> idle
。
2 假如数据状态是pending
,这时候可能在网络请求过程中,网络状态是fetching
,也可能网络中断,网络状态是paused
。