- 前端
- GraphQL
【免费下载链接】apollo
🚀 Apollo/GraphQL integration for VueJS
useLazyQuery是@vue/apollo-composable(本项目 Apollo/GraphQL 与 Vue 的集成层)中用于按需执行 GraphQL 查询的组合式函数。它适用于变量在组件挂载时未知、必须等用户操作(搜索提交、按钮点击、弹窗打开)后才发起请求的场景。读完本文,你将掌握load()触发机制、完整 Options/Result 契约、错误处理与 SSR 行为,并能基于源码理解其底层实现原理。
为什么需要 useLazyQuery:与 useQuery 的分工
在 packages/docs/advanced/lazy-queries.md 中,官方给出了三者的选择矩阵:
| 场景 | 工具 |
|---|---|
| 变量来自用户动作(搜索框提交、按钮点击) | useLazyQuery |
| 变量已知,但希望按条件控制是否执行 | useQuery({ enabled }) |
| 变量已知,且挂载后立即执行 | useQuery |
其核心区别在于:useQuery一旦调用就会立即发起请求,而useLazyQuery初始处于禁用状态,必须显式调用它暴露的load(variables?)函数才会启动。一旦第一次load完成,查询便退化为普通的useQuery行为——变量变为响应式、结果自动更新、作用域销毁时自动停止。
从源码看,这一“先禁用后激活”的实现位于 packages/vue-apollo-composable/src/useLazyQuery.ts:useLazyQuery本身只是对内部实现useQueryImpl(query, options, true)的薄封装,其中第三个参数lazy = true。在 packages/vue-apollo-composable/src/useQuery.ts 中,lazy标志被转换为内部 refforceDisabled(const forceDisabled = ref(lazy)),并参与最终启用态的计算:
const isEnabled = computed(() => enabledOption.value && !forceDisabled.value && !!document.value)也就是说,只要forceDisabled为true,watch(isEnabled)就不会创建ObservableQuery(observableQuery.value = client.watchQuery(...)不会执行),请求自然不会被发出;直到load()调用start()(forceDisabled.value = false)后,查询才真正激活。
基本用法:从文档示例到可运行代码
官方函数文档(packages/docs/api/composable/functions/useLazyQuery.md)给出了最简示例。下面是一个结合响应式模板的完整形态:
<script setup lang="ts"> import { TypedDocumentNode } from '@apollo/client' import { useLazyQuery } from '@vue/apollo-composable' import { ref } from 'vue' const SearchUsers: TypedDocumentNode<{ users: { id: string, name: string }[] }, { term: string }> = gql` query SearchUsers($term: String!) { users(search: $term) { id name } } ` const term = ref('') const { load, current } = useLazyQuery(SearchUsers) async function search() { const result = await load({ term: term.value }) console.log('Found users:', result?.users) } </script> <template> <form @submit.prevent="search"> <input v-model="term"> <button>Search</button> </form> <div v-if="current.loading"> Searching... </div> <ul v-else-if="current.resultState === 'complete'"> <li v-for="user in current.result.users" :key="user.id"> {{ user.name }} </li> </ul> </template>要点:
query参数类型为MaybeRefOrGetter<DocumentNode | TypedDocumentNode<TData, TVariables>>,即可以传文档本身,也可以传一个返回文档的 ref 或 getter;options?参数类型同样为MaybeRefOrGetter<Options>,支持响应式配置;- 返回值
Result提供响应式 refs 与load函数(详见下文 Result 章节); load(variables?)的行为是:把传入变量合并进已有变量(源码中为variablesRef.value = { ...variablesRef.value, ...variables }),然后调用start()启动查询,返回一个 Promise——查询完成时以结果数据 resolve,出错时以错误 reject。
结果除了出现在load的返回值里,也会同时落入current.result以及result、loading、error等独立的响应式 refs 中,与useQuery的用法完全一致。
多次加载:变量合并语义
load可以被重复调用,每次调用都会用新变量重新执行查询:
const { load } = useLazyQuery(SEARCH_USERS) await load({ term: 'alice' }) // 第一次搜索 await load({ term: 'bob' }) // 新搜索每次调用都会把新变量“叠加”在已加载的变量之上(merge 而非 replace)。因此如果第一次传了{ term: 'alice' },第二次传{ limit: 10 },最终发送的变量会是{ term: 'alice', limit: 10 }。
需要留意的是源码中的超时与等待逻辑:load内部使用@vueuse/core的until(...).toBe(true, { timeout: 30000 })等待当前状态变为resultState === 'complete'(且非isPreviousResult)或出现error,超时上限 30 秒;随后若存在错误则直接 throw,否则返回queryResult.result.value。这解释了为什么load的 Promise 既可能 resolve 数据,也可能 reject 错误。
Options:完整的配置契约
useLazyQuery的选项与useQuery完全一致,唯一例外是enabled不可用——因为查询在load()调用之前永远是懒的。从源码类型定义看(packages/vue-apollo-composable/src/useLazyQuery.ts):
export type Options<TData, TVariables> = Omit<useQuery.Options<TData, TVariables>, 'enabled'>选项按用途分为四组(完整参考:Options.md)。
1. 操作选项(Operation options)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
errorPolicy | ErrorPolicy | none | 决定查询在同时返回 GraphQL 错误与部分结果时如何处理;none表示结果包含错误详情但不含部分数据 |
variables | ReactiveVariablesParameter | — | 查询所需的全部 GraphQL 变量对象,每个 key 对应变量名,值对应变量值 |
其中variables支持多种响应式形态(见 useQuery.ts 中ReactiveVariablesParameter的定义):可以传整个变量的 ref/getter,也可以传一个“逐个变量映射到 ref/getter”的对象,例如:
const id = ref(1) useQuery(query, { variables: { id } })useLazyQuery场景下,options.variables充当load()的默认变量:先调用load()不传参时使用选项中的响应式变量,调用load({...})传参时则将其合并覆盖。
2. 网络选项(Networking options)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
context | DefaultContext | — | 若使用 Apollo Link,作为沿 link 链传递的context对象的初始值 |
notifyOnNetworkStatusChange | boolean | false | 为true时,每当网络状态变化或发生网络错误就触发下一次状态事件 |
pollInterval | number | 0(不轮询) | 查询轮询更新结果的间隔(毫秒) |
skipPollAttempt() | () => boolean | — | 轮询期间每次尝试 refetch 前调用;返回true则跳过本次 refetch,直到下一个轮询周期再试 |
3. 缓存选项(Caching options)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fetchPolicy | WatchQueryFetchPolicy | cache-first | 查询与 Apollo Client 缓存的交互方式(是否先查缓存再发请求) |
initialFetchPolicy | WatchQueryFetchPolicy | 跟随fetchPolicy | 指定变量变化时应回退到的策略(除非nextFetchPolicy介入) |
nextFetchPolicy | 策略或回调 | — | 本次查询完成之后使用的FetchPolicy |
refetchWritePolicy | RefetchWritePolicy | merge(兼容 Apollo Client 3.x) | NetworkStatus.refetch操作写入缓存时,是合并现有字段数据还是整体覆盖;覆盖通常更合适 |
returnPartialData | boolean | false | 为true时,若缓存未包含全部查询字段,允许返回部分结果 |
4. Vue-Apollo 专属选项(Vue-Apollo options)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
awaitComplete | boolean | false | 是否等待数据完整后再 resolve;配合@stream/@defer指令使用,仅影响 SSR 预取与await useQuery() |
clientId | string | — | 指定要使用的具名 Apollo Client 的 ID(替代默认 client) |
debounce | number | — | 变量更新的防抖毫秒数 |
keepPreviousResult | boolean | false | 加载新数据时保留上一份结果;保留结果仍以正常结果上报(resultState、result、partial描述它),并带isPreviousResult: true标记以便区分 |
prefetch | boolean | true | 是否在服务端预取 |
throttle | number | — | 变量更新的节流毫秒数 |
从 useQuery.ts 的实现可以看到这些选项如何生效:Apollo 相关选项(fetchPolicy、context、pollInterval等)被提取为watchQueryOptionscomputed,最终与query、variables合并成传给client.watchQuery()的完整选项;Vue-Apollo 专属选项则被提取为vueApolloQueryOptions,其中debounce/throttle通过useDebounceFn/useThrottleFn(flush: 'sync'的 watcher)延迟currentVariables的提交,pending标志会在这段窗口期内保持为true。
Result:完整的返回值契约
useLazyQuery的返回值Result继承自useQuery.Result,并额外扩展了load函数(完整参考:Result.md)。按用途可分为六组。
1. 操作数据(Operation data)
| 字段 | 类型 | 说明 |
|---|---|---|
current | Ref<Current> | 当前状态的判别联合类型(discriminated union) |
error | Ref<ErrorLike \| undefined> | 最近一次查询执行中发生的错误 |
isPreviousResult | Ref<boolean> | 为true时,result是keepPreviousResult保留的旧变量结果,不对应当前variables;此时resultState/result/partial描述保留结果,loading/networkStatus/error描述正在替换它的请求 |
result | Ref<object \| null> | 查询完成后的结果对象;若查询产生一个或多个错误,可能为undefined(取决于errorPolicy) |
current中的resultState取值为'empty' | 'complete' | 'streaming' | 'partial':empty表示缓存或网络都无法提供数据;partial仅在returnPartialData: true时出现;streaming表示延迟查询(@defer)仍在流式返回中;complete表示结果是完整满足的。
2. 网络信息(Network info)
| 字段 | 类型 | 说明 |
|---|---|---|
loading | Ref<boolean> | 查询忙碌中:覆盖请求在途、变量等待debounce/throttle计时器,以及变量已接受但请求尚未发出的交接期。比networkStatus < 7的范围更广 |
networkStatus | Ref<NetworkStatus> | 查询请求的网络状态编号;只描述网络本身——pending为true时它仍保持ready。与notifyOnNetworkStatusChange配合使用 |
pending | Ref<boolean> | 为true时,variables已变化且请求已提交,但因debounce/throttle尚未发出;loading同样覆盖这段窗口,用pending可区分“计时器未到”与“请求真的在网络上” |
3. Refs(引用)
| 字段 | 类型 | 说明 |
|---|---|---|
document | Ref<DocumentNode> | 正在查询的 GraphQL 文档 |
options | Ref<Options> | 当前选项 |
query | Ref<ObservableQuery \| undefined> | 底层 Apollo ObservableQuery 实例;查询停止时为undefined |
variables | Ref<OperationVariables> | 正在发送给查询的变量(经过 debounce/throttle 之后) |
4. 生命周期(Lifecycle)
load(variables?):加载查询。若未启动则启动之;传入的变量会与选项中的变量合并。返回Promise<object | undefined>,查询完成时 resolve 数据,出错时 reject。useLazyQuery的独有方法,也是它区别于useQuery的关键;start():启动查询;若查询已激活或enabled为false则无效果;stop():停止查询,可随时通过start()重新启动;restart():停止并重启查询,返回Promise<void>。
5. 查询方法(Query methods)
fetchMore(options):为分页/无限滚动加载更多数据,完成后响应式 refs 自动更新。数据合并有两种方式:一是updateQuery回调手动合并fetchMoreResult与previousQueryResult;二是在 Apollo 缓存配置中定义字段级merge函数。注意:若使用fetchPolicy: 'no-cache',必须提供updateQuery。
const { result, fetchMore } = useQuery(GetPosts, { variables: { offset: 0, limit: 10 } }) async function loadMore() { await fetchMore({ variables: { offset: result.value.posts.length }, updateQuery: (previousQueryResult, { fetchMoreResult }) => ({ ...previousQueryResult, posts: [...previousQueryResult.posts, ...fetchMoreResult.posts] }) }) }refetch(variables?):可选携带新变量重新执行查询,适合“刷新”按钮、下拉刷新等命令式场景。注意两点:- 需要与响应式变量保持同步的变量应放在
options.variables中(如路由参数、表单输入),一次性命令式获取才用refetch(); - 传入
refetch的变量是临时的:仅用于这一次请求,暴露的variablesref 不会更新;之后响应式options.variables变化时,查询会使用选项中的值而非 refetch 传入的值。
- 需要与响应式变量保持同步的变量应放在
await refetch() // 用当前变量重新执行 await refetch({ id: 'other-user' }) // 仅本次使用的新变量subscribeToMore(options):通过 GraphQL subscription 为查询增加实时数据,订阅数据到达时由updateQuery合并进现有结果。订阅在查询停止或组件卸载时自动清理,也可调用返回的函数手动取消:
const unsubscribe = subscribeToMore({ document: OnMessageAdded, variables: { channelId }, updateQuery: (_, { previousData, subscriptionData }) => { if (!subscriptionData.data) return previousData return { ...previousData, messages: [...previousData.messages, subscriptionData.data.messageAdded] } } })updateQuery(mapFn):直接更新缓存的查询结果,适用于乐观更新或免网络请求的数据修改,写入缓存后响应式 refs 自动更新。文档建议谨慎使用:多数缓存更新应优先考虑缓存字段策略或client.writeQuery,updateQuery绕过了 Apollo 常规缓存更新机制,适合快速本地修改:
function addOptimisticTodo(todo: Todo) { updateQuery((_, { previousData }) => ({ ...previousData, todos: [...previousData.todos, todo] })) }6. 事件(Events)
| 事件 | 触发时机 |
|---|---|
onNextState | 最细粒度事件,每次状态更新都会触发(含 loading 状态、网络状态变化、结果更新) |
onResult | 收到查询结果数据时;resultState为'complete'、'partial'、'streaming'时触发,'empty'状态与错误不触发 |
onCompleteResult | 仅当resultState为'complete'时触发 |
onPartialResult | 仅当resultState为'partial'时触发(需要returnPartialData: true) |
onStreamingResult | 仅当resultState为'streaming'时触发(配合@defer/@stream指令) |
onError | 查询发生错误时触发 |
事件底层由@vueuse/core的createEventHook实现(见 useQuery.ts 的 Events 区域),事件分发通过triggerResultEvents依据resultState路由到对应 hook。
等待结果与错误处理
load返回 Promise,因此可以内联使用结果:
async function fetchUser(id: string) { try { const data = await load({ id }) if (data) { console.log('Loaded:', data.user) } } catch (e) { console.error('Failed to load:', e) } }Promise 在结果完整时以查询数据 resolve;查询出错时以错误 reject。无论成败,响应式 refs(current、result、error)都会同步更新。这一行为由源码中的等待循环保证:load等待resultState === 'complete'(且非保留结果)或error != null二者之一,超时 30 秒;一旦queryResult.error.value存在即 throw。
SSR 场景下的行为
useLazyQuery不会在服务端运行——因为服务端不存在触发它的用户动作。如果某个值需要在 SSR 阶段就可用,请改用useQuery(参见 packages/docs/ssr/overview.md)。从源码也可印证:useQueryImpl中注册onServerPrefetch时要求isEnabled.value为真才会预取,而懒查询在服务端渲染阶段forceDisabled仍为true,故不会触发预取。
兼容层:与 v4 行为的映射
仓库中还有一套兼容实现 packages/vue-apollo-composable/src/compat/useLazyQuery.ts,用于对齐 v4 的useLazyQuery语义,差异值得了解:
- load 签名:
load(document?, variables?)可携带文档覆盖与变量覆盖;v4 语义下变量是替换(replace)而非 v5 的合并(merge); - 重复调用:首次调用启动查询并返回 Promise;后续调用返回
false而不重新执行。要换变量重新查询,需要修改响应式变量 ref/getter 或调用refetch(variables); - 错误策略:
errorPolicy为all或ignore时,v5 的onError会先于 compat 的onResult触发,compat 层用queueMicrotask调整顺序,保证与 v4 一致地优先 resolve。
小结与延伸阅读
useLazyQuery的定位非常明确:当变量来自用户动作、无法在挂载时确定时,用它替代useQuery;一旦load()触发,它就完全复用useQuery的响应式机制、缓存策略与查询方法,无需学习两套心智模型。其实现简洁优雅——useLazyQuery仅约 40 行,核心是useQueryImpl(..., true)加上一个合并变量并等待完成的load()。
相关仓库资源:
- 实现源码:packages/vue-apollo-composable/src/useLazyQuery.ts
- 底层实现:packages/vue-apollo-composable/src/useQuery.ts
- 进阶指南:packages/docs/advanced/lazy-queries.md
- API 参考:useLazyQuery 函数、Options、Result
- 查询基础:packages/docs/data/queries.md、重新获取与轮询对比:packages/docs/data/refetching.md、多查询加载状态:packages/docs/advanced/loading-states.md
- 前端
- GraphQL
【免费下载链接】apollo
🚀 Apollo/GraphQL integration for VueJS
相关推荐
Apollo Vue 组合式 API 实战:useLazyQuery 按需查询完整指南
Apollo Vue 组合式 API 实战:useLazyQuery 按需查询完整指南 在 Vue 应用中,并非所有 GraphQL 查询都应该在组件挂载时立即
前端GraphQLHowToCook 小炒黄牛肉全解:湘味大火爆炒的食材量化、工序拆解与嫩滑原理
HowToCook 小炒黄牛肉全解:湘味大火爆炒的食材量化、工序拆解与嫩滑原理 导读 本篇以程序员做饭指南仓库 HowToCook 中的 小炒黄牛肉菜谱 htt
前端GraphQL@vue/apollo-composable useLazyQuery Result 接口全解析:按需查询的响应式结果对象与 load 生命周期
@vue/apollo composable useLazyQuery Result 接口全解析:按需查询的响应式结果对象与 load 生命周期 本指南围绕 @
前端GraphQL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考