数据请求
组件里发请求谁都会写,难的是缓存、重试、竞态、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
| 关注点 | 仅 Pinia | Pinia + 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 管服务端状态
现有手写 useFetch 每次参数变化都发请求。说明如何用「上一次成功时间戳」避免 30 秒内的重复请求(写出核心逻辑即可)。
把练习 1 的手写 useFetch 用 TanStack Query 的 useQuery 改写,要求「id 变化时自动重新请求且复用缓存」。
判断下面两种数据分别该放 Pinia 还是 TanStack Query,并说明原因: (a)当前登录用户的「深色模式偏好」。 (b)商品列表(来自 /api/products,多页面共用,需缓存)。