【免费下载链接】apollo
🚀 Apollo/GraphQL integration for VueJS
本文围绕 @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 指令,结果仍在流式到达中 |
complete | result 是完全满足的查询结果(来自缓存或网络) |
为什么推荐使用 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 支持三种形态:
- 按 key 的响应式值对象(如上例):每个值可以是 ref、getter 或普通值;
- 返回整个 variables 对象的 ref 或 getter:适合从其他响应式状态计算变量;
- 普通对象:静态变量,不参与响应。
对应的类型定义是 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(源码中isEnabledcomputed 为 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、errordocument:当前查询的 GraphQL documentvariables:实际发送给 Apollo 的变量(防抖/节流之后的最终值)options:当前生效的 optionsquery:底层的 ApolloObservableQuery实例(查询停止时为undefined)
方法
start()/stop()/restart():手动启停与重启查询refetch(variables?):命令式重新执行,可传一次性变量(注意:传入的变量仅用于本次请求,不会更新暴露的variablesref)fetchMore(options):分页/无限滚动加载更多(配合updateQuery回调或缓存字段策略合并数据;fetchPolicy: 'no-cache'时必须提供updateQuery)updateQuery(mapFn):直接修改缓存中的查询结果(适合乐观更新,但会绕过 Apollo 常规缓存更新机制,建议谨慎使用)subscribeToMore(options):通过 GraphQL subscription 给查询接入实时更新,组件卸载或查询停止时自动清理onNextState/onResult/onCompleteResult/onPartialResult/onStreamingResult/onError:事件钩子
十三、下一步
- Refetching:按需或定时重新执行查询;
- Lazy Queries:响应用户动作才执行的查询;
- Mutations:更新数据并让查询反映变更;
- Error Handling:错误分类与恢复策略。
【免费下载链接】apollo
🚀 Apollo/GraphQL integration for VueJS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



