前言
先前文章提到:如何优雅的于 Vue 正确的处理资料载入点出了一些获取外部资料延伸带来的问题与实际解方,这次我想通过 TanStack Query 的视角挖掘怎么构建更好的前端资料获取体验。
前端面对「服务器状态」
- 远端持久化到无法控制或拥有的位置
- 需要异步 API 来获取和更新数据
- 共享所有权,可能会在不知情的情况下改动,不小心会「过期」
前端需要频繁的与「服务器状态 Server State」打交道,过程中的状态获取、管理、同步与缓存都是让人头痛的问题。实际工作规模通常会遇到「满天飞的状态」与「重复逻辑」,八成都与服务器状态脱不了关系。
必须说我的前端生涯花费太多时间处理与理解琐碎的服务器状态了,期望使用恰当的 TanStack Query 抽象,可以极大改善异步状态开发体验,甚至用户体验!
TanStack Query 解决什么问题?
- 自动缓存 + 自动重新获取(Stale While Revalidate、窗口重新聚焦、轮询)
- 并行 + 依赖请求
- Mutations(服务器副作用)
- 多层缓存 + 自动垃圾回收
- 分页 + Cursor-based 查询
- 无限加载 Query
- 请求取消
- 专用开发者工具
像是进阶一点的元框架如:Next 或 Nuxt 会有既定的方式与整合好的数据获取方案希望你采用。而 TanStack Query 是一款轻巧支持所有热门前端框架的异步状态管理方案,从以下简单的范例与三个概念开始:useQuery
、queryKey
、queryFn
。
<script setup>import { useQuery } from '@tanstack/vue-query'
const { isPending, isFetching, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: getTodos,})</script>
const fetchTodos = (): Promise<Todo[]> => axios.get('/todos').then((response) => response.data)
自动缓存与数据同步
每一次的 API 请求都会被自动缓存,并根据 queryKey
自动识别与合并。当在不同组件中使用相同的 queryKey
查询只会执行一次网络请求并共享结果。这也意味着切换页面时无需重新请求,节省资源与提升用户体验。
背景更新
Stale-While-Revalidate(SWR):「先用旧的资料马上回应使用者,同时在背景中更新新的资料,下一次再用新的资料。」
资料可能随时在服务器端被其他用户或行为更新,TanStack Query 支持自动或手动触发「背景更新」,让 UI 保持最新资讯但不打断用户操作流程。例如滑动列表时不会闪烁,但背景已经默默更新完毕。
过期逻辑
const { isPending, isFetching, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: getTodos, staleTime: 60 * 1000, // 1 minute})
可以精细控制数据何时被视为「过期」,通过 staleTime 参数告诉系统在多久之内数据算是有效的,避免每次渲染都触发 API。帮助在「数据即时性」与「性能」之间找到平衡。
并行 Fetching
原生 JS 除非使用 Promise.all
否则 async
await
使用上实际是串行执行,而 TanStack Query 默认最大化并行执行,且提供更方便优雅的方式管理请求状态:
// Demo-1await fetchUser();await fetchPosts();
// Demo-2await Promise.all([ fetchUser(), fetchPosts(), fetchComments(),]);
// TanStack Queryconst usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: fetchProjects,})
依赖 Fetching
一个 Query 依赖另外一个 Query 执行也是常有的事,举例来说需要取得某用户再取得该用户的资源:
// Get the userconst { 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 projectsconst { isIdle, data: projects } = useQuery({ queryKey: ['projects', userId], queryFn: () => getProjectsByUser(userId.value), enabled, // The query will not execute until `enabled == true`})
执行具副作用行为使用 useMutation
const { mutate, isPending } = useMutation({ mutationFn: async (newTodo) => { const { data } = await axios.post("/todos", newTodo) return data }, onSucess: () => { queryClient.invalidateQueries({ queryKey: ["todos"]}) }})
相较 useQuery
声明式的读取数据,useMutation
命令式的处理副作用(创建、更新、删除)且可以手动调用 invalidateQueries
来让相关缓存失效并自动重新拉数据。这样不需要手动维护数据同步的逻辑。
总结
基本上 useQuery
、useMutation
和 invalidateQueries
足以应付管理大多数 CRUD 应用,一些进阶的功能如乐观更新、分页都可以到官方文件了解更多。
延伸阅读
- React Query in 100 Seconds - Fireship
- 深入浅出 TanStack Query(一):在调用 useQuery 后发生了什么事
- 深入浅出 TanStack Query(二):在调用 useMutation 后发生了什么事
- 深入浅出 TanStack Query(三):在调用 invalidateQueries 后发生了什么事
- Vue3 教学 - Vue Query Part.1 使用 useQuery 与 useMutation