前言

在现代 Web 应用开发中,API 数据请求与状态同步是前端架构的核心环节。在 Vue 3 项目中,开发者通常习惯于使用 Axios 结合组件生命周期钩子(如 onMounted)来获取数据,并手动维护 loadingerror 等响应式状态。

然而,随着应用复杂度的提升,这种命令式的数据管理模式在性能、代码复用性以及用户体验上面临诸多挑战。本文将从架构设计的角度,探讨如何通过引入 @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>

这种命令式写法在简单场景下直观易懂,但在中大型项目中会暴露以下架构缺陷:

  1. 状态冗余与样板代码:每个需要请求数据的组件都必须重复定义 loadingerrordata 这一套响应式变量,并编写相似的 try-catch-finally 逻辑,导致代码冗余。
  2. 重复请求与带宽浪费:若页面中多个独立挂载的子组件依赖同一份数据(例如当前登录用户信息),它们各自挂载时会触发多次相同的 API 请求,缺乏统一的请求合并机制。
  3. 缓存缺失与页面闪烁:用户在不同路由间切换时,组件的销毁与重新挂载会频繁触发网络请求,导致页面出现“空白 -> 加载中 -> 渲染”的闪烁,无法提供即时响应的体验。
  4. 数据过期与同步困难:客户端难以感知服务器端数据的实时变化。当用户在 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 带来的核心收益

声明式数据获取

开发者不再需要手动维护 loadingerror 状态。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 与配置。

分类: vue 项目实战 标签: vuevue-query@tanstack/vue-query

评论

暂无评论数据

暂无评论数据

目录