Learn
Vue/16-data-fetching

数据请求

组件里发请求谁都会写,难的是缓存、重试、竞态、loading/error 状态这些横切问题。Vue 生态用 TanStack Query(原 Vue Query) 统一解决;也常自己封一个 useFetch composable。

1. 手写 useFetch(最小可用)

回顾第 8 章的 useFetch:用 watchEffect + toValue 自动响应参数变化。

import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue';
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const data = ref<T | null>(null);
  const error = ref<unknown>(null);
  const loading = ref(true);
  watchEffect((onCleanup) => {
    const u = toValue(url);
    const ctrl = new AbortController();
    onCleanup(() => ctrl.abort()); // 竞态:新请求到来时取消旧的
    loading.value = true;
    fetch(u, { signal: ctrl.signal })
      .then((r) => r.json())
      .then((d) => { data.value = d; })
      .catch((e) => { if (e.name !== 'AbortError') error.value = e; })
      .finally(() => { loading.value = false; });
  });
  return { data, error, loading };
}

关键是 onCleanup(() => ctrl.abort()):参数变化触发新请求时,旧的 AbortController 被取消,避免「慢响应覆盖快响应」的竞态。

2. TanStack Query:useQuery

手写版能跑,但缓存、后台刷新、重试、分页、mutation 全得自己写。TanStack Query 把这些变成声明式:

import { useQuery } from '@tanstack/vue-query';
import { toValue } from 'vue';
 
const props = defineProps<{ id: string }>();
const { data, error, isLoading, refetch } = useQuery({
  queryKey: ['user', toValue(() => props.id)], // key 变了自动重新请求
  queryFn: () => fetch('/api/user/' + props.id).then((r) => r.json()),
  staleTime: 30_000,                            // 30s 内视为新鲜,不发请求
});

queryKey 是缓存的钥匙:props.id 变 → key 变 → 自动重新拉取;相同 key 直接复用缓存。staleTime 控制「新鲜期」。还有 useMutation 处理写操作、useInfiniteQuery 处理分页。

3. 为什么用 Query 而不是只靠 Pinia

关注点仅 PiniaPinia + TanStack Query
缓存(避免重复请求)手动内置(按 key)
后台/聚焦刷新手动内置
重试 / 重试退避手动内置
loading/error 状态手动内置
写后失效手动invalidateQueries

经验:Pinia 管「客户端状态」(UI 偏好、表单草稿);Query 管「服务端状态」(来自 API、需缓存与失效)。别把服务端数据塞进 Pinia 当缓存用。

4. 配合 Suspense

useQuery 返回的 data 是响应式 ref,组件里可直接用;若想用 <Suspense> 等待,可把 query 包成 async setup 读取 data.value 的 Promise 形态(TanStack Query 提供 useQuery 的 promise 形态配合 Suspense)。

小结

  • 手写 useFetch:watchEffect + toValue 响应参数,AbortController 处理竞态
  • TanStack Query useQuery:queryKey 驱动缓存与重取,staleTime 控新鲜期
  • useMutation 处理写、useInfiniteQuery 分页、invalidateQueries 失效
  • 分工:Pinia 管客户端状态,Query 管服务端状态
🎯练习 1:给 useFetch 加 staleTime 概念(伪代码)

现有手写 useFetch 每次参数变化都发请求。说明如何用「上一次成功时间戳」避免 30 秒内的重复请求(写出核心逻辑即可)。

🎯练习 2:用 useQuery 改写

把练习 1 的手写 useFetch 用 TanStack Query 的 useQuery 改写,要求「id 变化时自动重新请求且复用缓存」。

🎯练习 3:Pinia 还是 Query?

判断下面两种数据分别该放 Pinia 还是 TanStack Query,并说明原因: (a)当前登录用户的「深色模式偏好」。 (b)商品列表(来自 /api/products,多页面共用,需缓存)。