平时工作中,跟接口打交道是家常便饭的事了,日复一日的请求接口,缓存数据,添加状态,一成不变。
直到偶然间发现一个 🐂 🍺 的库,才发现这些都是可以简便起来的——TanStack Query。
什么是TanStack Query
ChatGPT上是这么介绍的——TanStack Query是一个用于现代前端应用的数据获取和异步状态管理库,主要用于“服务器状态”的管理。
在前端开发领域中,静态页面和动态数据之间的桥梁,普遍认为是像axios、fetch、ajax这类的请求库,但这其中还有一个特别容易忽视的一环,那就是数据的状态,平时我们开发,一般都是封装到axios这一步为止了,请求的时候通过封装的方法获取数据,然后自己管理数据和页面的状态;如果一个页面有多种类型的请求,代码里面就会充斥着loading = true/false,如果需要更新,那就更不得了了;
还有一种场景:公共组件库的数据缓存;最典型的例子就是select了,大多数情况下,我们都会使用pinia或者vuex这类的状态管理库来存储数据以便全局使用,同样也需要实现他们的Store,如果请求之后,能直接缓存我们的数据,生命周期跟着整个应用一起,那这些事情我们都不需要去做了。
TanStack Query就是帮我们去做这个事的。
如何使用
<script setup>
import { useQueryClient, useQuery, useMutation } from "@tanstack/vue-query";
// 获取 QueryClient 实例
const queryClient = useQueryClient();
// 查询
const { isPending, isError, data, error } = useQuery({
queryKey: ["todos"],
queryFn: getTodos,
});
// 修改
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// 失效并重新请求
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
function onButtonClick() {
mutation.mutate({
id: Date.now(),
title: "Do Laundry",
});
}
</script>
<template>
<span v-if="isPending">Loading...</span>
<span v-else-if="isError">Error: {{ error.message }}</span>
<!-- 可以假设此时 `isSuccess === true` -->
<ul v-else>
<li v-for="todo in data" :key="todo.id">{{ todo.title }}</li>
</ul>
<button @click="onButtonClick">Add Todo</button>
</template>
这是官网上的例子,其中getTodos就是使用axios或者fetch封装的请求方法;返回的isPending、isError、data本身就已经是响应式数据了,在模板或者函数中直接使用就行,非常简单和灵活。
这些只是TanStack Query提供的功能中的冰山一角,它还提供诸如以下前端工程中常用的方法:
自动重试
重新聚焦查询
乐观更新
数据缓存
...
封装
虽然TanStack提供的功能已经非常好用了,但是针对自己的业务还是有很多可以优化的方向,比如说缓存策略、统一默认行为、QueryKey体系化;
缓存策略
TanStack Query中控制缓存的主要是staleTime和gcTime;
staleTime:数据是否新鲜,超过配置的时间后,数据仍然可用,但是如果满足条件(断网重连、组件重新mount、浏览器重新聚焦、手动refresh),会重新请求数据。
gcTime:数据多久时间后被回收;当没有组件使用query、Query实例被回收则开始倒计时(默认 5 分钟)。
为做到缓存的统一管理,避免staleTime在业务中滥用,所以需要在封装代码中进行收口,使用isCache代替staleTime:
/**
* 是否缓存
* - -1 : 禁用缓存, 数据的生命周期等于组件的生命周期, 且数据唯一缓存不共享,组件卸载会清除缓存
* - 0 : 启用缓存,数据生命周期贯穿整个应用
* - 1 : 启用缓存,数据生命周期贯穿整个应用, 永不过期
* - 2 : 启用缓存,永不过期,当没有活动的查询时会清除缓存
*/
isCache?: -1 | 0 | 1 | 2
useEffect(() => {
const queryKey = {
queryKey: requestFn.spliceQueryKey(params.value, id),
}
const queryCache = queryClient.getQueryCache()
const query = queryCache.find(queryKey)
return () => {
if (isCache === -1) {
queryClient.removeQueries(queryKey)
}
else if (isCache === 2) {
// 没有活动状态时才会删除
if (query && !query.isActive()) {
queryClient.removeQueries(queryKey)
}
}
}
}, [params])
// 除非传入具体的 staleTime 否则使用 isCache 的值
useQuery({
...queryConfig,
staleTime: queryConfig.staleTime ?? ((isCache === 1 || isCache === 2) ? Infinity: 0)
})
注:useEffect是使用effectScope实现的,类似React中的useEffect。
通过以上代码,实现 4 中不同的缓存策略,在不同的场景中使用;
isCache = -1:组件级私有缓存
const { data } = getUserApi.useQuery({ id: 1 }, { isCache: -1 });
- QueryKey 追加唯一 ID:即使参数相同,不同组件实例也不共享缓存
- staleTime:默认
0(数据立即 stale) - 清理时机:组件卸载或参数变化时,立即删除对应缓存
- 典型场景:表单编辑页、列表页、每个实例需要独立数据的场景
isCache = 0:标准共享缓存(默认)
const { data } = getUserListApi.useQuery({ page: 1 });
// 等价于
const { data } = getUserListApi.useQuery({ page: 1 }, { isCache: 0 });
- 缓存共享:相同 QueryKey 的组件共享同一份缓存
- staleTime:默认
0(数据立即 stale) - 清理时机:不主动清理,由 QueryClient 的
gcTime控制 - 典型场景:常规数据列表、需要实时性的数据
isCache = 1:永不过期的共享缓存
const { data } = getDictApi.useQuery({ type: "status" }, { isCache: 1 });
- 缓存共享:相同 QueryKey 的组件共享同一份缓存
- staleTime:
Infinity(数据永远 fresh,不会自动重新请求) - 清理时机:不主动清理,由 QueryClient 的
gcTime控制 - 典型场景:字典数据、系统配置、极少变化的静态数据
isCache = 2:永不过期 + 无人使用时清理
const { data } = getDetailApi.useQuery({ id: 1 }, { isCache: 2 });
- 缓存共享:相同 QueryKey 的组件共享同一份缓存
- staleTime:
Infinity(数据永远 fresh) - 清理时机:当最后一个使用该缓存的组件卸载(且
query.isActive() === false)时删除 - 典型场景:详情页数据(多组件共享时保留,全部卸载后清理)
统一默认行为 + 参数为 null 时阻止请求
- 允许
queryParams返回null来“阻止请求”(enabled=false),避免组件里写一堆条件判断。 queryFn从query.queryKey[1]取params,保证“请求用的参数”与QueryKey对齐。
useQuery({
...other,
enabled() {
if (params.value === null) {
return false;
}
return toValue(qc.enabled) ?? true;
},
queryKey: requestFn.spliceQueryKey(queryParams, id, false),
queryFn(query) {
const params = query.queryKey[1];
if (params === null) {
return null as any;
}
return requestFn(params as any);
},
});
QueryKey体系化
queryKey:METHOD:URL(字符串);spliceQueryKey(...):生成Vue Query用的数组key(可带params/ 可带id);baseQueryKey:只含METHOD:URL的数组,方便用QueryClient做“按接口维度”匹配(例如批量失效/刷新)。
requestFn.baseQueryKey = [`${method}:${requestConfig.url}`];
requestFn.queryKey = `${method}:${requestConfig.url}`;
requestFn.spliceQueryKey = (queryParams, _id, isToValue = true) => {
const v = isToValue ? toValue(queryParams) : queryParams;
const qk: any[] = [requestFn.queryKey];
if (_id) {
qk.push(v);
qk.push(_id);
return qk;
}
if (v !== undefined) {
qk.push(v);
}
return qk;
};
统一使用挂载在requestFn上的spliceQueryKey方法生成queryKey,加上baseQueryKey,同时支持精确匹配和“模糊匹配”。
总结
通过对@tanstack/vue-query的封装,在开发中根据不同的业务场景使用不同的缓存策略,可以提高开发效率,减少代码量,同时也可以提高代码的可读性,以及可维护性。