平时工作中,跟接口打交道是家常便饭的事了,日复一日的请求接口,缓存数据,添加状态,一成不变。

直到偶然间发现一个 🐂 🍺 的库,才发现这些都是可以简便起来的——TanStack Query

什么是TanStack Query

ChatGPT上是这么介绍的——TanStack Query是一个用于现代前端应用的数据获取和异步状态管理库,主要用于“服务器状态”的管理。

在前端开发领域中,静态页面和动态数据之间的桥梁,普遍认为是像axiosfetchajax这类的请求库,但这其中还有一个特别容易忽视的一环,那就是数据的状态,平时我们开发,一般都是封装到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封装的请求方法;返回的isPendingisErrordata本身就已经是响应式数据了,在模板或者函数中直接使用就行,非常简单和灵活。

这些只是TanStack Query提供的功能中的冰山一角,它还提供诸如以下前端工程中常用的方法:

自动重试
重新聚焦查询
乐观更新
数据缓存
...

封装

虽然TanStack提供的功能已经非常好用了,但是针对自己的业务还是有很多可以优化的方向,比如说缓存策略、统一默认行为、QueryKey体系化;

缓存策略

TanStack Query中控制缓存的主要是staleTimegcTime

staleTime:数据是否新鲜,超过配置的时间后,数据仍然可用,但是如果满足条件(断网重连、组件重新mount、浏览器重新聚焦、手动refresh),会重新请求数据。

gcTime:数据多久时间后被回收;当没有组件使用queryQuery实例被回收则开始倒计时(默认 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 的组件共享同一份缓存
  • staleTimeInfinity(数据永远 fresh,不会自动重新请求)
  • 清理时机:不主动清理,由 QueryClient 的 gcTime 控制
  • 典型场景:字典数据、系统配置、极少变化的静态数据

isCache = 2:永不过期 + 无人使用时清理

const { data } = getDetailApi.useQuery({ id: 1 }, { isCache: 2 });
  • 缓存共享:相同 QueryKey 的组件共享同一份缓存
  • staleTimeInfinity(数据永远 fresh)
  • 清理时机:当最后一个使用该缓存的组件卸载(且 query.isActive() === false)时删除
  • 典型场景:详情页数据(多组件共享时保留,全部卸载后清理)

统一默认行为 + 参数为 null 时阻止请求

  1. 允许queryParams返回null来“阻止请求”(enabled=false),避免组件里写一堆条件判断。
  2. queryFnquery.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体系化

  1. queryKeyMETHOD:URL(字符串);
  2. spliceQueryKey(...):生成 Vue Query 用的数组 key(可带 params / 可带 id);
  3. 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的封装,在开发中根据不同的业务场景使用不同的缓存策略,可以提高开发效率,减少代码量,同时也可以提高代码的可读性,以及可维护性。


本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

命令式弹窗解决方案 下一篇