- 前端
- GraphQL
【免费下载链接】apollo-client
The industry-leading GraphQL client for TypeScript, JavaScript, React, Vue, Angular, and more. Apollo Client delivers powerful caching, intuitive APIs, and comprehensive developer tools to accelerate your app development.
导读
@apollo/client/incremental是 Apollo Client 中处理 GraphQL 增量交付(incremental delivery)的核心模块,负责解析服务端通过@defer与@stream指令分批下发的响应,并将多个分片逐步合并进查询结果。本指南以 .api-reports/api-report-incremental.api.md 公开 API 报告为骨架,结合 src/incremental 目录下的真实实现,系统讲解增量交付的类型体系、三种 Handler 的职责与差异、底层分片合并原理,以及如何通过ApolloClient构造选项接入。读完本文,你将掌握 Apollo Client 增量交付的完整类型契约,并能根据服务端协议版本正确选择与配置 Handler。
模块定位:为什么 Apollo Client 需要独立的 incremental 子包
GraphQL 的增量交付允许服务端先返回查询的主要结果,再陆续下发@defer(延迟片段)与@stream(流式列表)对应的后续数据。客户端必须能够:
- 识别响应中哪些是普通结果、哪些是增量分片;
- 合并多个分片为完整的
FormattedExecutionResult; - 上报合并过程中的错误与 extensions。
在 src/core/ApolloClient.ts 中可以看到,ApolloClient构造函数接收一个incrementalHandler选项,其默认值为new NotImplementedHandler()。也就是说,默认情况下客户端并不处理增量交付,只有显式传入 Handler 才会启用。所有与增量协议相关的类型与实现都被收敛到 src/incremental/index.ts 这个入口中导出:
export type { Incremental } from "./types.js"; export { NotImplementedHandler } from "./handlers/notImplemented.js"; export { Defer20220824Handler, Defer20220824Handler as GraphQL17Alpha2Handler, } from "./handlers/defer20220824.js"; export { GraphQL17Alpha9Handler } from "./handlers/graphql17Alpha9.js";公共类型定义位于 src/incremental/types.ts,三个 Handler 的实现分别位于 defer20220824.ts、graphql17Alpha9.ts 与 notImplemented.ts。
核心类型契约:Incremental 命名空间
API 报告开头的Incremental命名空间定义了增量交付模块的全部公共类型,是实现自定义 Handler 的基础接口。
Path:定位增量数据的路径
export type Path = ReadonlyArray<string | number>;Path描述增量分片在结果对象中的写入位置:字符串段表示对象字段名,数字段表示数组索引。例如["user", "friends", 3]表示result.user.friends[3]。@stream分片正是借助路径末端的数字索引把流式项插入数组的正确位置。
Handler:增量协议的处理器接口
Handler是模块中最核心的接口,任何增量处理器都必须实现四个成员(定义于 src/incremental/types.ts):
export interface Handler< Chunk extends Record<string, unknown> = Record<string, unknown>, > { isIncrementalResult: (result: ApolloLink.Result<any>) => result is Chunk; prepareRequest: (request: ApolloLink.Request) => ApolloLink.Request; extractErrors: ( result: ApolloLink.Result<any> ) => readonly GraphQLFormattedError[] | undefined | void; startRequest: <TData extends Record<string, unknown>>(request: { query: DocumentNode; }) => IncrementalRequest<Chunk, TData>; }四个成员各自承担一道关键职责:
| 成员 | 职责 | 触发时机 |
|---|---|---|
isIncrementalResult | 类型守卫,判断一次ApolloLink.Result是否为该协议下的增量分片 | 每次请求结果返回时 |
prepareRequest | 改写链路请求,通常用于在 HTTP context 中声明可接受的增量协议 | 请求发出前 |
extractErrors | 从分片中提取所有GraphQLFormattedError,供错误策略与errorPolicy使用 | 结果返回时 |
startRequest | 为一次查询创建状态化的IncrementalRequest,用于跨分片累积数据 | 查询开始时 |
IncrementalRequest:跨分片的状态化累积器
export interface IncrementalRequest<Chunk, TData> { hasNext: boolean; handle: ( cacheData: TData | DeepPartial<TData> | undefined | null, chunk: Chunk ) => FormattedExecutionResult<TData>; }IncrementalRequest是每次查询内部的状态机:handle接收当前已合并的数据(cacheData,来自 Apollo 缓存)与新的分片chunk,返回合并后的完整FormattedExecutionResult;hasNext指示是否还有后续分片。注意cacheData允许为undefined或null——源码注释明确指出,在no-cache获取策略下会传入undefined,此时实现需要回退到自身累积的旧值(两个内置 Handler 都通过= this.data作为默认参数实现这一行为)。
StreamFieldInfo:流式字段的分片边界标记
export interface StreamFieldInfo { isFirstChunk: boolean; isLastChunk: boolean; }该接口(标注@internal)用于标记某个流式数组字段当前分片是否为第一片或最后一片,供内部判断数组边界,在GraphQL17Alpha9Handler的streamInfoTrie 中被实际使用。
Defer20220824Handler:基于 2022-08-24 规范草案的实现
Defer20220824Handler实现了@defer/@stream的历史规范提交(源码注释指向 graphql-spec 仓库48cf726提交),对应 HTTP 响应格式multipart/mixed;deferSpec=20220824。在 API 报告中它还以别名GraphQL17Alpha2Handler导出,以兼容早期 graphql-js 17 alpha 版本的命名。
分片类型体系
该 Handler 的分片类型由InitialResult与SubsequentResult联合构成(defer20220824.ts):
export type InitialResult<TData = Record<string, unknown>> = { data?: TData | null | undefined; errors?: ReadonlyArray<GraphQLFormattedError>; extensions?: Record<string, unknown>; hasNext: boolean; incremental?: ReadonlyArray<IncrementalResult<TData>>; }; export type SubsequentResult<TData = Record<string, unknown>> = { extensions?: Record<string, unknown>; hasNext: boolean; incremental?: Array<IncrementalResult<TData>>; };差异要点:首片(InitialResult)携带主体data(允许为null/undefined,例如整段结果被错误吞掉时),后续片(SubsequentResult)不再包含顶层data,只通过incremental数组携带分片数据。
incremental数组中的每个元素(defer20220824.ts)区分两类:
IncrementalDeferResult:@defer分片,含data、path、可选的label与errors;IncrementalStreamResult:@stream分片,含items(流式数组项)、path,同样支持label与errors。
两者的判别在合并逻辑中通过"items" in incremental完成。
对外方法的行为
isIncrementalResult:以"hasNext" in result作为类型守卫判断——这是 20220824 协议最直观的标记;prepareRequest:当查询包含defer或stream指令时,向request.context.http.accept头部数组前插入"multipart/mixed;deferSpec=20220824"(defer20220824.ts);extractErrors:递归收集顶层errors与每个incremental分片内的errors;startRequest:返回DeferRequest实例。
合并原理:DeferRequest 内部状态
DeferRequest私有维护errors数组、extensions对象与累积的data,每次handle调用执行以下步骤(defer20220824.ts):
- 用
chunk.hasNext更新hasNext,并将cacheData(或累积值)设为当前数据基底; - 对
incremental数组逐片处理:items为null的流式分片:将该字段路径(去掉末尾数组索引)加入ignoredImpossibleStreamPaths集合,后续对该路径的更新一律跳过——源码注释解释这是为了防止在非空列表中出现空项时产生稀疏数组或运行时崩溃;@defer的data: null:合并时回退为undefined,避免把null覆盖进结果;- 数组项重定位:若路径末位是数字且
data是数组,则按startingIdx + idx逐个重新定位合并,保证乱序到达的流式项各就各位;
- 通过
new DeepMerger({ arrayMerge: "truncate" }).merge(...)将分片合并进累积数据,errors追加、extensions覆盖合并; - 最终返回
{ data, errors?, extensions? },其中errors与extensions仅在非空时才出现在结果中。
GraphQL17Alpha9Handler:面向 graphql.js 17.0.0-alpha.9 的实现
GraphQL17Alpha9Handler是针对graphql包17.0.0-alpha.9增量规范(对应multipart/mixed;incrementalSpec=v0.2,即 Apollo 增量交付规范 v0.2)的处理器。相比 20220824 协议,它引入了id 机制来管理多个并发的延迟分组。
分片类型体系差异
新协议的分片类型(graphql17Alpha9.ts)具有明显不同的结构:
export type InitialResult<TData = Record<string, unknown>> = { data: TData; errors?: ReadonlyArray<GraphQLFormattedError>; pending: ReadonlyArray<PendingResult>; hasNext: boolean; extensions?: Record<string, unknown>; }; export type SubsequentResult<TData = unknown> = { hasNext: boolean; pending?: ReadonlyArray<PendingResult>; incremental?: ReadonlyArray<IncrementalResult<TData>>; completed?: ReadonlyArray<CompletedResult>; extensions?: Record<string, unknown>; };与 20220824 版本相比的关键变化:
- 新增
pending数组:在首片中声明哪些延迟分组已就绪(PendingResult含id、path与可选label); - 新增
completed数组:后续片中用于标记已完成的分组(CompletedResult含id与可选errors),处理@stream截断与@defer收尾; - 每个
IncrementalDeferResult/IncrementalStreamResult均携带id字段,增量数据改用subPath(相对路径)替代绝对path,实际写入位置由pending.path.concat(incremental.subPath ?? [])计算得出。
GraphQL17Alpha9Handler的四个方法在 API 报告中均被标注@internal @deprecated,但类本身为@public,意味着方法是内部实现细节,公开的是以Incremental.Handler接口约定的能力。
合并原理:IncrementalRequest 的流式定位策略
IncrementalRequest(graphql17Alpha9.ts)是该协议下的状态累积器,其实现亮点包括:
streamPositions映射:以pending.id为键记录下一次流式项的插入位置,替代依赖数组长度计算位置的朴素做法——源码注释说明缓存中的数组引用可能被后续分片修改,因此必须显式追踪位置,避免覆盖缓存更新;- 稀疏数组合并:流式分片到达时构造
parent[i + streamPositions[id]] = items[i]的稀疏数组,再由DeepMerger依据Object.keys正确落位; - 流式信息 Trie:借助
@wry/trie构建StreamInfoTrie,跟踪每个流式数组的isFirstChunk/isLastChunk状态; - completed 处理:分组完成时,若存在
streamPositions记录,则将对应数组按位置slice截断以处理仅含hasNext: false+completed的收尾分片,并从pending中删除该分组; - extensions 透传:当 Trie 仍持有强引用时,在结果 extensions 中注入以
streamInfoSymbol为键的WeakRef(指向StreamInfoTrie),QueryInfo通过引用相等性判断触发最终缓存写入,同时WeakRef避免长期持有内存。
extractErrors还额外处理completed分组中的错误,而 20220824 版本没有该路径。
NotImplementedHandler:未配置处理器时的兜底行为
NotImplementedHandler是ApolloClient的默认incrementalHandler(ApolloClient.ts),其isIncrementalResult恒返回false,startRequest置为undefined as any(该路径在运行时不可达),而prepareRequest是关键防线(notImplemented.ts):
prepareRequest(request: ApolloLink.Request) { invariant( !hasDirectives(["defer", "stream"], request.query), "`@defer` and `@stream` are not supported without specifying an incremental handler. Please pass a handler as the `incrementalHandler` option to the `ApolloClient` constructor." ); return request; }即:一旦查询中出现@defer或@stream指令而用户未配置 Handler,客户端会在请求准备阶段抛出 invariant 错误,提示显式传入处理器。这就是"默认不支持增量交付"的设计意图——协议版本必须由使用者按服务端能力显式声明。
实战接入:如何选择与配置 Handler
在 ApolloClient 构造函数中启用
在 src/core/ApolloClient.ts 中,incrementalHandler的声明类型为Incremental.Handler<any>。实际接入方式如下:
import { ApolloClient, InMemoryCache, } from "@apollo/client"; import { Defer20220824Handler, GraphQL17Alpha9Handler, } from "@apollo/client/incremental"; // 服务端支持 incrementalSpec=v0.2(graphql-js 17 alpha 系列)时: const client = new ApolloClient({ uri: "https://api.example.com/graphql", cache: new InMemoryCache(), incrementalHandler: new GraphQL17Alpha9Handler(), }); // 服务端基于 2022-08-24 defer 规范草案时: const clientLegacy = new ApolloClient({ uri: "https://api.example.com/graphql", cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), });选择依据:
GraphQL17Alpha9Handler:对应multipart/mixed;incrementalSpec=v0.2,适用于支持pending/completed/id机制的新版服务端;Defer20220824Handler:对应multipart/mixed;deferSpec=20220824,适用于旧版@defer/@stream草案实现,并以GraphQL17Alpha2Handler别名兼容早期 graphql-js 命名;NotImplementedHandler:不显式传入时的默认值,遇到@defer/@stream会立即报错提示。
运行时接入链
配置生效后,Handler 会贯穿查询全生命周期:
QueryManager在请求前调用prepareRequest(QueryManager.ts),为含defer/stream指令的查询注入正确的Accept头;- 结果返回时,
QueryInfo调用isIncrementalResult判断是否增量分片,若是则调用startRequest创建状态化累积器(QueryInfo.ts); - 后续每个分片经由
IncrementalRequest.handle合并,hasNext: false表示流结束。
类型层面的接入(HKT 类型覆盖)
API 报告中的每个 Handler 都声明了TypeOverrides接口,如:
interface TypeOverrides { AdditionalApolloLinkResultTypes: Defer20220824Result; }Defer20220824Result、GraphQL17Alpha9Result、NotImplementedResult均继承HKT(高阶类型,来自 @apollo/client/utilities),通过arg1/arg2表示TData/TExtensions两个类型参数、return表示分片类型。这套机制让 TypeScript 可以将增量分片类型注入ApolloLink的结果类型系统,使defer/stream响应在类型层面可追踪。若需为自定义协议编写 Handler,实现Incremental.Handler并声明对应的TypeOverrides即可获得同样的类型推断支持。
质量保障:测试如何验证两个协议
仓库为两个 Handler 各维护了独立且详尽的测试套件(src/incremental/handlers/tests),并复用了大量 graphql-js 官方测试用例:
- defer20220824/defer.test.ts:覆盖延迟标量片段、
if参数禁用、顶层字段延迟、延迟片段内嵌套延迟、延迟片段抛错、非空错误冒泡、多分片顺序等; - defer20220824/stream.test.ts:覆盖列表流式、
initialCount默认值、多维列表、Promise 列表、async iterable、非空项返回null、跨延迟边界的null过滤等; - graphql17Alpha9/defer.test.ts:进一步覆盖 inline fragment 延迟、不同 label 的延迟分组分别下发、同对象多次 defer 去重、跨 defer 边界 null 冒泡、结果不可合并时过滤分片等;
- graphql17Alpha9/stream.test.ts:验证新协议下的流式行为与位置定位。
此外,src/core/tests下还有大量端到端集成测试(如client.watchQuery/defer20220824.test.ts、client.watchQuery/streamGraphQL17Alpha9.test.ts等),每个用例都以incrementalHandler: new Defer20220824Handler()或new GraphQL17Alpha9Handler()配置客户端,验证 Handler 与watchQuery、useQuery、useSuspenseQuery、useBackgroundQuery等 API 组合下的真实行为。阅读这些测试是理解两种协议差异最快的路径。
小结
@apollo/client/incremental模块以"协议可插拔"为设计核心:Incremental.Handler抽象出协议识别、请求改写、错误提取与分片累积四步能力;Defer20220824Handler与GraphQL17Alpha9Handler分别是旧版草案与新版 v0.2 规范的两套落地实现,前者通过path定位、后者通过id+pending/completed管理多个并发延迟分组;NotImplementedHandler则保证未显式配置时行为可预期。接入时只需在ApolloClient构造函数中根据服务端协议选择对应 Handler,即可让@defer/@stream的增量响应被正确、完整地合并进查询结果。
- 前端
- GraphQL
【免费下载链接】apollo-client
The industry-leading GraphQL client for TypeScript, JavaScript, React, Vue, Angular, and more. Apollo Client delivers powerful caching, intuitive APIs, and comprehensive developer tools to accelerate your app development.
相关推荐
Apollo Client 错误体系全解析:@apollo/client/errors 模块 API 深度指南
Apollo Client 错误体系全解析:@apollo/client/errors 模块 API 深度指南 Apollo Client 将运行时可能遇到的错
前端GraphQLApollo Client HttpLink 模块完全指南:从 API 报告到源码级解析
Apollo Client HttpLink 模块完全指南:从 API 报告到源码级解析 本指南以 Apollo Client 仓库中的公开 API 报告 .a
前端GraphQLApollo Client 4.0 被移除 API 全解析:`@apollo/client/v4-migration` 迁移清单与 `Removals` 分类指南
Apollo Client 4.0 被移除 API 全解析: @apollo/client/v4 migration 迁移清单与 Removals 分类指南 导
前端GraphQL
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考