01-异步状态管理新范式:为什么在 Vue 3 中使用 vue-query?
前言
在现代 Web 应用开发中,API 数据请求与状态同步是前端架构的核心环节。在 Vue 3 项目中,开发者通常习惯于使用 Axios 结合组件生命周期钩子(如 onMounted)来获取数据,并手动维护 loading、error 等响应式状态。
然而,随着应用复杂度的提升,这种命令式的数据管理模式在性能、代码复用性以及用户体验上面临诸多挑战。本文将从架构设计的角度,探讨如何通过引入 @tanstack/vue-query 实现从“命令式数据获取”向“声明式异步状态管理”的范式转变,并阐述其与 Axios 的协同工作机制。
传统前端请求的痛点
在没有引入专门的异步状态管理工具前,一个典型的 Vue 3 数据请求组件通常采用如下模式:
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { getUserDetail, type UserDetail } from "@/api/user";
const data = ref<UserDetail | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
const fetchData = async () => {
loading.value = true;
error.value = null;
try {
data.value = await getUserDetail();
} catch (err) {
error.value = err as Error;
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchData();
});
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">出错了:{{ error.message }}</div>
<div v-else-if="data">用户:{{ data.name }}</div>
</template>这种命令式写法在简单场景下直观易懂,但在中大型项目中会暴露以下架构缺陷:
- 状态冗余与样板代码:每个需要请求数据的组件都必须重复定义
loading、error、data这一套响应式变量,并编写相似的try-catch-finally逻辑,导致代码冗余。 - 重复请求与带宽浪费:若页面中多个独立挂载的子组件依赖同一份数据(例如当前登录用户信息),它们各自挂载时会触发多次相同的 API 请求,缺乏统一的请求合并机制。
- 缓存缺失与页面闪烁:用户在不同路由间切换时,组件的销毁与重新挂载会频繁触发网络请求,导致页面出现“空白 -> 加载中 -> 渲染”的闪烁,无法提供即时响应的体验。
- 数据过期与同步困难:客户端难以感知服务器端数据的实时变化。当用户在 A 页面修改了数据,B 页面缓存的数据无法自动或优雅地同步更新。
核心概念:Axios 与 Vue Query 的本质区别
要解决上述痛点,首先需要理清网络传输与状态管理在前端架构中的职责边界。
Axios 的定位:传输层(Transport Layer)
- 它是一个优秀的 HTTP 客户端,专注于“如何安全、正确地执行网络通信”。
- 其职责包括:请求/响应拦截、Token 刷新、超时处理、请求重试、数据序列化等网络细节。
- 它不关心数据在客户端的生命周期、共享范围以及缓存策略。
Vue Query 的定位:异步状态管理层(Asynchronous State Management)
- 它是一个强大的状态管理器,专注于“如何管理、同步和缓存客户端的服务器状态”。
- 其职责包括:数据缓存生命周期管理、请求去重、后台静默刷新、条件触发、垃圾回收等。
- 它不关心底层的网络请求是通过 Axios、Fetch 还是 WebSocket 实现的。
协同工作模式
在优秀的架构设计中,两者并非竞争关系,而是互补的。Axios 负责发送请求,Vue Query 负责管理请求结果。
graph LR
Component[Vue 组件] -->|useQuery| VueQuery[Vue Query 缓存层]
VueQuery -->|未命中缓存/已过期| Axios[Axios 传输层]
Axios -->|发起 HTTP 请求| Server[后端服务器]
Server -->|返回数据| Axios
Axios -->|交付数据| VueQuery
VueQuery -->|响应式更新| Component引入 Vue Query 带来的核心收益
声明式数据获取
开发者不再需要手动维护 loading、error 状态。Vue Query 的 useQuery 会自动解构出这些状态,使组件逻辑更加聚焦于 UI 渲染:
const { data, isLoading, isError, error } = useQuery({
queryKey: ["user-detail"],
queryFn: getUserDetail
});自动请求去重(Deduplication)
当多个组件在同一时刻调用相同的 useQuery(拥有相同的 queryKey)时,Vue Query 会将它们合并。底层的网络请求只会发送一次,所有组件共享同一个响应结果。
开箱即用的缓存与后台静默刷新(SWR 机制)
Vue Query 采用了经典的 SWR (Stale-While-Revalidate) 缓存失效策略:
- 当组件挂载时,如果缓存中已有数据,Vue Query 会立即返回缓存数据,让用户瞬间看到内容。
- 与此同时,它会在后台静默发起请求,获取最新数据。
- 当最新数据返回后,它会静默更新缓存并响应式地渲染页面,确保数据最终一致性。
极简的全局状态共享
无需通过 Pinia 编写繁琐的 action 和 state,直接通过相同的 QueryKey 即可在任意组件间共享数据,降低了状态管理的复杂度。
项目实战:它们是如何在项目中优雅配合的?
在实际项目中,我们通过将 Axios 实例与 Vue Query 的全局配置进行解耦,实现了职责分离。
统一的初始化与职责分离
在项目初始化阶段,我们配置全局的 QueryClient。由于底层的 Axios 实例已经处理了网络层面的请求重试和异常拦截(我自己封装的axios),我们在 Vue Query 层进行了相应的适配:
import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 1. 底层 Axios 已经实现了请求重试机制,所以这里关闭自动重试,避免双重重试
retry: false,
// 2. 默认关闭窗口聚焦时自动刷新,按需在单个接口开启,节省服务器流量
refetchOnWindowFocus: false
}
}
});声明式查询封装示例
在业务开发中,我们将 API 请求函数与 Vue Query 的缓存配置封装为统一的 Composable 函数。以下是一个标准的用户详情查询封装:
import { useQuery } from "@tanstack/vue-query";
import { getUserDetail } from "@/api/user"; // 封装好的 Axios 请求函数
export const USER_DETAIL_KEY = ["user-detail"];
export function useUserDetail(isLogin: Ref<boolean>) {
return useQuery({
queryKey: USER_DETAIL_KEY,
queryFn: getUserDetail,
// 只有在用户登录后,才允许发起请求(条件触发)
enabled: isLogin,
// 5分钟内,数据被认为是“新鲜”的,不需要重复请求
staleTime: 5 * 60 * 1000
});
}通过这种设计,底层的 Axios 专注于处理网络传输细节,而上层的 useUserDetail 则专注于处理缓存有效期、登录依赖等业务状态逻辑,实现了清晰的关注点分离。
总结
从“命令式 Axios 请求”到“声明式 Vue Query 缓存管理”,改变的不仅是几行代码,更是前端架构思维的转变。
- Axios 解决了“如何通信”的问题。
- Vue Query 解决了“如何同步状态”的问题。
在下一篇文章中,我们将正式进入实战,手把手教你如何在项目中从零开始使用 Vue Query,并掌握其最核心的 API 与配置。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据