在 Vue 3 中使用 useQuery 获取 GraphQL 数据:@vue/apollo-composable 查询开发实战

  • 前端
  • GraphQL

【免费下载链接】apollo

🚀 Apollo/GraphQL integration for VueJS

项目地址: https://gitcode.com/gh_mirrors/apollo2/apollo
点击查看 免费下载

本文围绕 @vue/apollo-composable 提供的核心 composable——useQuery,系统讲解在 Vue 3 组件中声明式获取 GraphQL 数据的完整方法:从最基础的查询执行、响应式变量传参、缓存与加载状态处理,到 fetch policy 配置、查询禁用、事件钩子、防抖节流与 keepPreviousResult 数据保留等进阶能力。读完本文,你将能写出类型安全、可响应、可维护的 Vue + Apollo 查询代码,并理解其底层实现原理。

一、快速开始:执行第一个查询

useQuery 是 @vue/apollo-composable 中用于执行 GraphQL 查询的核心 composable,完整 API 定义见 useQuery 函数文档。在 <script setup> 中调用它并传入一个 GraphQL document,即可开始查询:

<script setup lang="ts">
import { TypedDocumentNode } from '@apollo/client'
import { useQuery } from '@vue/apollo-composable'

declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ dogs: { id: string, breed: string }[] }, {}>
const { current } = useQuery(gql`
  query GetDogs {
    dogs {
      id
      breed
    }
  }
`)
</script>

<template>
  <div v-if="current.loading">
    Loading...
  </div>
  <div v-else-if="current.error">
    Error: {{ current.error.message }}
  </div>
  <ul v-else-if="current.resultState === 'complete'">
    <li v-for="dog in current.result.dogs" :key="dog.id">
      {{ dog.breed }}
    </li>
  </ul>
</template>

useQuery 返回一个 current ref,其值是一个判别联合类型(discriminated union),包含 result、resultState、loading、networkStatus、error 等属性。从源码看,current 的类型由 ResultState 与 VueApolloState 组合而来:Apollo 上报的 data/dataState 被重命名为 result/resultState,并叠加了 Vue-Apollo 自己跟踪的 pending 与 isPreviousResult 两个字段。

resultState 共有四种取值,完整定义见 useQuery.Current 接口文档:

取值含义
empty缓存或网络都没有提供数据,result 为 undefined
partial缓存只满足了部分字段(仅在 returnPartialData: true 时出现),result 不完整
streaming使用了 @defer/@stream 指令,结果仍在流式到达中
completeresult 是完全满足的查询结果(来自缓存或网络)

为什么推荐使用 current

useQuery 用两种形态暴露同样的信息:一个是包含判别联合的 current ref;另一个是 result、loading、error 等可单独解构的独立 ref。

官方文档和源码都明确建议:任何需要读取 result 的代码都应使用 current。原因是 current 通过 resultState 对 result 做类型收窄(type narrowing),让 TypeScript 推断出精确的数据形状:

const { current } = useQuery(gql`
  query GetUser {
    user { name }
  }
`)

if (current.value.resultState === 'complete') {
  // current.value.result 此时被推断为 { user: { name: string } }
  console.log(current.value.result.user.name)
}

而独立解构的 result.value 总是被推断为 TData | undefined,没有任何上下文信息。把 loading、网络状态和数据收窄集中到 current 一个地方,读写起来都更省心。不过,当只关心某一个状态且不需要访问 result 时(比如只想在模板里判断 loading),独立的 ref(result、loading、error、networkStatus)依然可用。

二、响应式变量:三种传参形态

1. 每个变量一个响应式值

在 options 对象中传入 variables,每个 key 的值可以是 ref、getter 或普通值:

<script setup lang="ts">
import { TypedDocumentNode } from '@apollo/client'
import { useQuery } from '@vue/apollo-composable'
import { ref } from 'vue'

declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ dog: { id: string, name: string } }, { breed: string }>
const breed = ref('bulldog')

const { current } = useQuery(gql`
  query GetDog($breed: String!) {
    dog(breed: $breed) {
      id
      name
    }
  }
`, {
  variables: { breed },
})
</script>

当 breed.value 变化时,查询会使用新值自动重新执行。这正是源码中 variables computed 的逻辑:对每个 key 的值调用 toValue 解包,再传给 Apollo(见 useQuery.ts 中的变量解析)。

variables 支持三种形态:

  1. 按 key 的响应式值对象(如上例):每个值可以是 ref、getter 或普通值;
  2. 返回整个 variables 对象的 ref 或 getter:适合从其他响应式状态计算变量;
  3. 普通对象:静态变量,不参与响应。

对应的类型定义是 ReactiveVariablesParameter(见 type-aliases/ReactiveVariablesParameter.md)。

2. 变量写成 getter:避免丢失响应性

对于 props 或随时间变化的值,用 getter 可以避免失去响应性:

<script setup lang="ts">
import { TypedDocumentNode } from '@apollo/client'
import { useQuery } from '@vue/apollo-composable'

declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ dog: { id: string } }, { breed: string }>
const { breed } = defineProps<{ breed: string }>()

const { current } = useQuery(gql`
  query GetDog($breed: String!) {
    dog(breed: $breed) { id }
  }
`, {
  variables: () => ({ breed }),
})
</script>

注意:props 在解构后如果只把值塞进普通对象,响应性会断开;写成 () => ({ breed }) 这样的 getter 才能持续追踪最新值。

3. 节流与防抖:throttle 与 debounce

对于搜索框、滑块这类高频更新的输入,throttle 和 debounce 可以显著减少查询重新执行的次数:

const term = ref('')

const { current } = useQuery(SEARCH, {
  variables: { q: term },
  debounce: 300, // 最后一次变化后等待 300ms 再重新执行
})

两者的使用原则:

  • throttle:限制执行频率上限(如每 100ms 至多一次);
  • debounce:等待输入稳定后再执行(适合搜索联想);
  • 二者互斥,不要同时设置。

源码中二者的实现分别基于 useDebounceFn 和 useThrottleFn(节流采用 trailing edge 模式),在 variables 的 watcher 中根据配置选择提交路径,详见 useQuery.ts 的防抖节流实现。

测试用例也验证了这一行为:debounce: 100 时,连续快速修改 message 为 second、third、fourth,最终只执行 first 和 fourth 两次查询,中间的更新被合并跳过(见 useQuery.test.ts 中的防抖测试)。

三、缓存:同一查询的秒回

Apollo Client 使用规范化内存缓存(normalized in-memory cache)存储查询结果。当以相同变量再次执行相同查询时,结果会直接来自缓存,无需网络请求:

<script setup lang="ts">
import { TypedDocumentNode } from '@apollo/client'
import { useQuery } from '@vue/apollo-composable'

declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ dog: { id: string, photo: string } }, { breed: string }>
const { breed } = defineProps<{ breed: string }>()

// 第一次以该 breed 调用:命中网络。
// 后续以相同 breed 调用:直接返回缓存。
const { current } = useQuery(gql`
  query GetDogPhoto($breed: String!) {
    dog(breed: $breed) {
      id
      photo
    }
  }
`, {
  variables: () => ({ breed }),
})
</script>

缓存如何命中、如何失效、字段如何合并,是 Apollo 数据层的核心话题,建议进一步阅读 缓存概览。

四、加载状态:loading 与 networkStatus

查询进行中时 current.loading 为 true:

<script setup lang="ts">
const { current } = useQuery(QUERY)
</script>

<template>
  <div v-if="current.loading">
    Loading...
  </div>
  <div v-else-if="current.resultState === 'complete'">
    {{ current.result }}
  </div>
</template>

如果需要更细粒度的控制,可以使用 current.networkStatus(来自 Apollo 的 NetworkStatus 枚举):

<script setup lang="ts">
import { NetworkStatus, TypedDocumentNode } from '@apollo/client'
import { useQuery } from '@vue/apollo-composable'

const { current } = useQuery(QUERY)
</script>

<template>
  <div v-if="current.networkStatus === NetworkStatus.refetch">
    Refetching...
  </div>
</template>

如果要在多个查询之间聚合 loading 状态(例如"整页加载中"指示器),useQuery 在创建时还会通过 trackQuery 把 loading ref 注册进全局加载计数器,loadingTracking.ts 中维护着按组件作用域和全局两份计数,对应的组合式函数(如 useGlobalQueryLoading、useQueryLoading)可参阅 Loading States 进阶指南。

五、错误处理

current.error 中保存了最近一次执行发生的错误(类型为 ErrorLike):

<script setup lang="ts">
const { current } = useQuery(QUERY)
</script>

<template>
  <div v-if="current.error" class="error">
    {{ current.error.message }}
  </div>
</template>

注意:在 current 联合中,error 是可选字段,配合 resultState 等字段可以做精确的分支判断。完整的错误处理策略(error policy、partial data、错误类型分类)参见 错误处理指南。

六、Fetch Policies:控制查询与缓存的交互

fetchPolicy 决定查询在缓存与网络之间的取值策略:

const { current } = useQuery(QUERY, {
  fetchPolicy: 'network-only',
})
策略说明
cache-first先查缓存,仅当缓存未命中才发起网络请求(默认值)
cache-and-network立即返回缓存数据,同时从网络拉取最新数据并更新
network-only总是从网络获取,但结果写入缓存
cache-only只读缓存,绝不发起网络请求
no-cache总是从网络获取,且不写入缓存

此外还有两个相关的策略选项:

  • nextFetchPolicy:首次请求完成后切换到的策略;
  • initialFetchPolicy:当变量变化时重置回某个指定策略(默认取 fetchPolicy 的初始值,除非 nextFetchPolicy 介入)。

这些选项与 refetchWritePolicy、errorPolicy、returnPartialData、context、pollInterval、notifyOnNetworkStatusChange、skipPollAttempt 等一起,被源码通过 watchQueryOptions computed 从 options 中剥离出来透传给 Apollo 的 watchQuery(见 useQuery.ts 的选项解析)。完整参数说明参见 useQuery.Options 接口文档。

从源码可以进一步看到底层机制:shouldReobserve 函数(复制自 Apollo Client 的 useQuery 实现)会比较新旧 options——只有当 document 变化、变量不相等、或 fetchPolicy 在 standby 与其他策略之间切换时,才会调用 reobserve 重建观察者;否则仅调用 applyOptions 原地应用新选项(见 useQuery.ts 的 shouldReobserve)。

七、禁用查询:enabled 选项

设置 enabled: false 可以跳过查询执行,直到某个条件满足:

<script setup lang="ts">
import { TypedDocumentNode } from '@apollo/client'
import { useQuery } from '@vue/apollo-composable'

declare const gql: (literals: TemplateStringsArray, ...placeholders: any[]) => TypedDocumentNode<{ user: { id: string } }, { id: string }>
const { userId } = defineProps<{ userId?: string }>()

const { current } = useQuery(
  gql`
    query GetUser($id: ID!) {
      user(id: $id) { id }
    }
  `,
  () =>
    userId == null
      ? { enabled: false }
      : { variables: { id: userId } },
)
</script>

这里 options 本身也是一个 getter,Vue-Apollo 会根据当前 props 状态动态切换"禁用"与"带变量执行"两套配置。

行为要点:

  • enabled 为 false 时,不存在 observable query(源码中 isEnabled computed 为 false,会触发 watcher 将 observableQuery 置为 undefined,见 useQuery.ts 的 enabled 状态管理);
  • 当 enabled 翻转为 true,查询随即启动,此后行为与普通 useQuery 完全一致;
  • 源码中的 DisabledOptions 类型还支持 enabled: false 时省略 variables,此时 TypeScript 会放宽变量要求。

对于需要响应用户动作才执行的查询(如点击搜索按钮、提交表单),更推荐使用 useLazyQuery。它的实现正是复用 useQueryImpl 并传入 lazy = true,初始通过 forceDisabled 强制禁用,直到调用 load() 才启动(见 useLazyQuery.ts 实现)。

八、事件钩子:命令式响应生命周期

useQuery 暴露了多个事件钩子,可以命令式地响应查询生命周期:

const { onNextState, onResult, onError } = useQuery(QUERY)

// 每次状态变化都会触发(loading、networkStatus、result 等)
onNextState((state) => {
  console.log('State:', state.resultState, state.loading)
})

// 当结果数据到达时触发(complete、partial 或 streaming)
onResult((data) => {
  console.log('Data received:', data)
})

// 当发生错误时触发
onError((error) => {
  console.error('Query failed:', error)
})

onResult 有三个可单独监听的变体,普通 onResult 会覆盖三者:

钩子触发条件
onCompleteResult仅当 resultState === 'complete'
onPartialResult仅当 resultState === 'partial'(需要 returnPartialData: true)
onStreamingResult仅当 resultState === 'streaming'(配合 @defer/@stream 指令)
onResult以上三种都会触发;empty 状态和错误不触发

源码中用 createEventHook 创建了 5 组事件通道,triggerResultEvents 依据新到达状态的 resultState 分发到对应通道(见 useQuery.ts 的事件实现)。另外 onNextState 由 watch(current) 驱动,因此它同样会覆盖防抖/节流的等待窗口。

九、保留旧数据:keepPreviousResult

默认情况下,当变量变化、新请求在途时,result 会被清空。设置 keepPreviousResult: true 可以让旧数据一直显示到新数据到达:

const id = ref('1')

const { current } = useQuery(QUERY, {
  variables: { id },
  keepPreviousResult: true,
})
// 当 id 变化时,current.result 保留旧值,
// 直到新查询完成。

这一模式非常适合筛选器和分页场景——每次变化都闪现空状态会非常突兀。

需要理解的是保留结果的语义:被保留的结果仍作为正常结果上报——resultState、result 和 partial 描述的仍然是当前正在展示的数据,因此基于 resultState === 'complete' 的类型收窄依然成立。区分新旧数据靠 isPreviousResult 字段,而 loading 描述的是正在替换它的新请求:

<script setup lang="ts">
const term = ref('')

const { current } = useQuery(SearchProducts, {
  variables: { term },
  keepPreviousResult: true,
})
</script>

<template>
  <ul v-if="current.resultState === 'complete'" :class="{ stale: current.isPreviousResult }">
    <li v-for="product in current.result.products" :key="product.id">
      {{ product.name }}
    </li>
  </ul>
  <p v-else-if="!current.loading">
    No products found.
  </p>
</template>

一句话总结:resultState 描述的是被交还的数据,loading、networkStatus、error 描述的是请求本身,数据保留只会改变前者。

源码中 applyState 实现了这一逻辑:当新状态为 empty 且开启了 keepPreviousResult 且此前结果非空时,会从旧状态中保留 result/resultState/partial,同时用新状态的 loading/networkStatus/error 覆盖,并置 isPreviousResult: true(见 useQuery.ts 的 applyState)。测试用例验证:变量从 first 切到 second 后,界面上仍显示 first、resultState 仍为 complete、isPreviousResult 为 true、loading 为 true,直到新数据到达才恢复(见 useQuery.test.ts 的 keepPreviousResult 测试)。

十、loading 与防抖窗口:pending 的语义

一个容易踩坑的细节:loading 从查询"接受新变量"的那一刻起就为 true,而不是从请求真正发出才为 true。因此在配置了 debounce 或 throttle 时,loading 会覆盖计时器尚未走完的窗口——搜索框在按下按键的瞬间就显示 spinner,而不是等延迟结束后才显示:

const term = ref('')

const { loading, pending } = useQuery(SearchProducts, {
  variables: { term },
  debounce: 300,
})
// loading: 从按键开始为 true,直到结果落地
// pending: 仅在防抖计时器运行期间为 true
  • pending:把"计时器未走完"这段单独分离出来,用于需要区分二者的场景(请求埋点、取消按钮);
  • networkStatus:只描述网络本身,在防抖窗口期间始终保持 ready;
  • 因此 loading 刻意比 networkStatus < 7 更宽:它还包括变量已被接受但请求尚未发出这段交接期。

从源码可以精确还原这三者关系:pending 的实现是 !equal(variables, currentVariables)——当防抖/节流生效时,只要"最新变量"尚未提交到 Apollo 即为 true;loading 则定义为 pending || isCommitting || currentState.loading,其中 isCommitting 填补了 pending 清零与下一次 flush 之间重新观察的间隙(见 useQuery.ts 的状态计算)。

另外注意:内容深度相等(deeply equal)的变量重建不会触发 pending,因为不会有请求跟随;变量在计时器走完前"变了又变回去"同样不会触发 pending。

十一、等待查询完成:PromiseLike 与 Suspense

useQuery 返回的对象实现了 PromiseLike 接口,当初始数据可用时 resolve。与 <Suspense> 组合使用,可以得到服务端渲染的初始状态和统一的加载 fallback:

<script setup lang="ts">
import { gql } from '@apollo/client'
import { useQuery } from '@vue/apollo-composable'

const { current } = await useQuery(gql`
  query GetUsers {
    users { id name }
  }
`)
</script>

实现层面,源码的 then 方法通过 isAwaited 判断当前状态是否满足等待条件:未满足时注册一个 watch(current) 在同步 flush 下等待状态收敛;等待判定会跳过 isPreviousResult 的保留结果(它属于旧变量,不能算新数据),并受 awaitComplete 选项影响——该选项配合 @stream/@defer 指令时,要求等待到 complete 而非任意非空状态(见 useQuery.ts 的 Promise 实现)。同时在服务端渲染环境中,onServerPrefetch 会按 prefetch 选项(默认 true)触发同样的 prefetch,让 SSR 首屏直接拥有数据。

完整的 Suspense 模式(含 SSR 与流式渲染考量)参见 Suspense 指南。

十二、返回的完整 API:refs 与方法

除了本文重点讲解的 current 之外,useQuery 的返回值还包括(完整类型见 useQuery.Result 接口文档):

响应式 refs

  • result、loading、pending、isPreviousResult、networkStatus、error
  • document:当前查询的 GraphQL document
  • variables:实际发送给 Apollo 的变量(防抖/节流之后的最终值)
  • options:当前生效的 options
  • query:底层的 Apollo ObservableQuery 实例(查询停止时为 undefined)

方法

  • start() / stop() / restart():手动启停与重启查询
  • refetch(variables?):命令式重新执行,可传一次性变量(注意:传入的变量仅用于本次请求,不会更新暴露的 variables ref)
  • fetchMore(options):分页/无限滚动加载更多(配合 updateQuery 回调或缓存字段策略合并数据;fetchPolicy: 'no-cache' 时必须提供 updateQuery)
  • updateQuery(mapFn):直接修改缓存中的查询结果(适合乐观更新,但会绕过 Apollo 常规缓存更新机制,建议谨慎使用)
  • subscribeToMore(options):通过 GraphQL subscription 给查询接入实时更新,组件卸载或查询停止时自动清理
  • onNextState / onResult / onCompleteResult / onPartialResult / onStreamingResult / onError:事件钩子

十三、下一步

  • 前端
  • GraphQL

【免费下载链接】apollo

🚀 Apollo/GraphQL integration for VueJS

项目地址: https://gitcode.com/gh_mirrors/apollo2/apollo
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值