Relay 命令式数据获取:fetchQuery 完整 API 指南与源码剖析

原创2026-09-20 18:07:08795 阅读
文章标签:前端开发工具

fetchQuery 是 React Relay 提供的一个命令式(imperative)数据获取 API,允许你在 React 组件之外(例如事件回调、非 React 代码、导航逻辑)发起 GraphQL 查询,并得到一个可订阅的 Observable 对象。本文以 Relay v13 官方 API 文档 为骨架,结合仓库中 fetchQuery.jsfetchQueryInternal.js 的源码实现,系统讲解其参数、返回值、事件语义、去重机制与数据保留策略,帮助你安全、正确地把它用在真实项目中。

为什么需要 fetchQuery

在 Relay 中,绝大多数数据获取发生在 React 渲染阶段,通过 useLazyLoadQueryuseQueryLoader 等 Hook 完成。但实际业务里总有一些场景需要在 React 之外主动发起请求,例如:

  • 页面路由跳转前预取数据;
  • 点击事件、定时任务、Web Worker 回调中获取数据;
  • 将数据写入全局状态后由组件消费;
  • 需要精细控制请求生命周期(订阅、取消)的副作用代码。

fetchQuery 正是为这类场景设计:它执行一次查询、把结果写入 Relay store、并返回一个可观察对象,让你用订阅式(observable)或 Promise 式(toPromise)两种风格消费数据。

基本用法:订阅一个查询

fetchQueryreact-relay 导出,核心签名是 fetchQuery(environment, query, variables, options?)。最简单的调用方式是传入环境、graphql 模板字面量定义的查询以及变量,然后调用返回 Observable 的 .subscribe()

import type {AppQuery} from 'AppQuery.graphql';

// 建议优先传入 useRelayEnvironment() 返回的环境
const MyEnvironment = require('MyEnvironment');
const {fetchQuery} = require('react-relay');

fetchQuery<AppQuery>(
  environment,
  graphql`
    query AppQuery($id: ID!) {
      user(id: $id) {
        name
      }
    }
  `,
  {id: 4},
)
.subscribe({
  start: () => {...},
  complete: () => {...},
  error: (error) => {...},
  next: (data) => {...}
});

几点需要特别说明:

  • 类型导入 import type {AppQuery} from 'AppQuery.graphql' 中的 AppQuery 是 Relay 编译器自动生成的类型,通过它 fetchQuery 可以静态校验返回数据的形状以及 variables 是否符合查询声明的 GraphQL 变量类型(详见下文"类型参数 TQuery"一节)。
  • 如果你在 React 组件内部发起请求,建议通过 useRelayEnvironment 获取当前环境,而不是使用全局单例,以保证多环境(例如多租户、测试环境)下的正确性。
  • 从源码看,fetchQuery 会先通过 getRequest(query) 取得查询节点,并用 invariant 断言 queryNode.params.operationKind === 'query',即只接受 query 操作;传入 mutation 或 subscription 会直接抛出 'fetchQuery: Expected query operation' 错误。

参数详解

environment(必需)

要执行请求的 Relay Environment 实例。如上文所述,若请求在 React 组件内发起,请使用 useRelayEnvironment 返回的环境;若在组件外,则传入你自己创建或全局共享的环境。

query(必需)

使用 graphql 模板字面量定义的 GraphQL 查询。变量需要在查询体内声明,并在 variables 参数中传入对应的值。

variables(必需)

包含查询变量的对象。这些变量的键与类型必须与查询内声明的 GraphQL 变量一致,TQuery 类型参数会保证这一点在编译期得到校验。

options(可选)

一个可选的 options 对象,目前支持以下配置:

配置项 类型 说明
networkCacheConfig Object 网络缓存配置对象
networkCacheConfig.force Boolean true 时绕过网络响应缓存;默认值为 true
fetchPolicy 'network-only' | 'store-or-network' 数据获取策略,默认 'network-only'

其中 fetchPolicy 虽然在本版本文档正文中未展开,但源码 fetchQuery.js 明确支持两种取值:

  • network-only(默认):始终发起网络请求,无视 store 中是否已有可用数据;
  • store-or-network:先调用 environment.check(operation) 检查查询可用性,若 queryAvailability.status === 'available' 则直接从 store 读取(environment.lookup(operation.fragment)),否则才走网络;同时会通过 environment.__log 记录 fetchquery.fetch 事件(含 fetchPolicyqueryAvailabilityshouldFetch 字段)供诊断。

force 默认值 true 也在源码中得到印证——fetchQuery.js 中以 {force: true, ...options?.networkCacheConfig} 的方式合并配置,因此即使不传 options,网络响应缓存默认也被绕过。若需要命中响应缓存,可显式传入 {networkCacheConfig: {force: false}}

Flow 类型参数 TQuery

fetchQuery<TQuery>(...) 中的 TQuery 是对应查询的 Flow 类型,可以从编译器自动生成的 <query_name>.graphql.js 文件中导入。它的作用有两方面:

  1. 保证 Observable 提供给 next 回调的 data 形状与查询定义匹配;
  2. 强制 variables 输入与查询期望的变量类型一致。

类型定义见 fetchQuery.d.ts

export function fetchQuery<T extends OperationType>(
    environment: Environment,
    taggedNode: GraphQLTaggedNode,
    variables: T['variables'],
    cacheConfig?: {
        networkCacheConfig?: CacheConfig | null | undefined;
        fetchPolicy?: FetchQueryFetchPolicy | null | undefined;
    } | null,
): RelayObservable<T['response']>;

可以看到,底层返回的 RelayObservable 的泛型参数正是 T['response'],类型安全性由此贯穿整个链路。

返回值:Observable 详解

fetchQuery 返回一个 observable 实例。注意:调用 fetchQuery 本身并不会立即发起请求,只有调用 .subscribe().toPromise() 后请求才开始。

subscribe(observer)

订阅网络请求,返回一个 subscription 对象;调用 subscription.unsubscribe() 可以取消网络请求。observer 对象可包含以下事件处理函数:

事件 回调参数 触发时机
start subscription 网络请求开始,参数代表对该网络 observable 的订阅
next data 每次从网络收到 payload 时触发;data 是该时刻从 Relay store 读取的查询数据快照
complete 网络请求成功完成
error error 网络请求发生错误
unsubscribe subscription 订阅被取消时触发

源码注释(fetchQuery.js)进一步阐明了这些事件的语义:

fetchQuery(environment, query, variables).subscribe({
  // Called when network requests starts
  start: (subscription) => {},

  // Called after a payload is received and written to the local store
  next: (payload) => {},

  // Called when network requests errors
  error: (error) => {},

  // Called when network requests fully completes
  complete: () => {},

  // Called when network request is unsubscribed
  unsubscribe: (subscription) => {},
});

特别值得注意 next 的语义:payload 收到后会先写入本地 store,再回调 next,因此 data从 store 读取的最新快照,而不是网络的原始响应。实现上,fetchQuery 内部通过 getNetworkObservablefetchQueryInternal.fetchQuery 的网络 observable 与 environment.lookup(operation.fragment) 的 store 快照映射在一起(见 fetchQuery.js),并通过 handlePotentialSnapshotErrors 处理字段级错误。

此外,当启用 Relay 的增量数据投递(Incremental Data Delivery,参见 streaming-pagination)能力时,next 可能被调用多次以接收来自服务器的多个 payload。

toPromise()

将请求转换为 Promise:

  • 返回值是一个 Promise,在收到服务器第一个网络响应时 resolve;
  • 若请求失败则 reject;
  • 不可取消

关于 toPromise 的更多限制与风险,见下文专门章节。

三条核心行为

官方文档明确了 fetchQuery 的三个关键行为,理解它们对正确使用至关重要:

  1. 自动写入 store 并通知订阅者fetchQuery 会把获取到的数据自动保存到内存中的 Relay store,并通知订阅了相关数据的组件重新渲染。

  2. 不保留(retain)查询数据fetchQuery 不会 retain 查询数据,因此请求完成后数据不保证仍然保留在 store 中(可能被 Relay 的垃圾回收机制清理)。如果你希望数据在请求作用域之外继续存活,需要直接调用 environment.retain() 手动保留查询。详见 Controlling Relay's GC Policy。这一行为在测试中被显式验证:fetchQuery-test.js 中"fetches request and does not retain data"用例断言请求完成后 retained.length === 0(见 fetchQuery-test.js)。

  3. 自动去重(de-dupe):同一时刻、同一环境、同一查询与变量、且同样由 fetchQuery 发起的**在途(in-flight)**请求会被自动去重,不会重复发起网络请求。

源码剖析:去重机制如何工作

去重的核心实现在 fetchQueryInternal.js。其机制可以概括为"环境级请求缓存 + 可重放的 ReplaySubject":

  • 每个 environment 对应一个 Map<RequestIdentifier, RequestCacheEntry> 请求缓存,整体存储在一个 WeakMap(支持时)中(fetchQueryInternal.js);
  • 请求标识符 RequestIdentifier 由查询与变量计算得出;
  • 首个订阅者触发真实网络请求,并把响应转发给一个 RelayReplaySubject;后续同标识符的订阅者直接订阅该 subject,从而共享同一次网络请求;
  • 请求结束时(无论成功还是失败)通过 .finally(() => requestCache.delete(identifier)) 从缓存中移除条目(fetchQueryInternal.js);
  • 当某个订阅者取消时,如果该请求已无任何观察者(subject.getObserverCount() === 0),底层网络请求订阅也会被一并取消并从缓存删除(fetchQueryInternal.js)。

边界情况:如果一个请求同步完成,那么在同一 tick 内再次以相同参数调用 fetchQuery 不会触发去重,因为该请求已不再处于在途状态(源码注释 fetchQuery.js 明确记录了这一行为)。

另外,fetchQueryInternal.js 还导出了 getPromiseForActiveRequestgetObservableForActiveRequest 等低层工具,供非 fetchQuery 场景复用同一套请求缓存。

慎用 .toPromise():数据可能缺失

toPromise 的便利背后有一个重要陷阱。官方文档原文给出警告:toPromise 会启动查询,并在第一份数据从服务器返回时 resolve,同时取消后续处理。这意味着查询中任何 deferred(延迟)字段或 3D 数据可能尚未被处理,从而造成数据缺失。因此官方明确建议一般情况下不要使用 toPromise()

import type {AppQuery} from 'AppQuery.graphql';

const {fetchQuery} = require('react-relay');

fetchQuery<AppQuery>(
  environment,
  graphql`
    query AppQuery($id: ID!) {
      user(id: $id) {
        name
      }
    }
  `,
  {id: 4},
)
.toPromise() // NOTE: don't use, this can cause data to be missing!
.then(data => {...})
.catch(error => {...});

配套的限制还包括:请求失败时 Promise 会 reject;Promise 一旦发出无法取消。如果确实需要 Promise 语义,请评估上述数据缺失与不可取消的代价是否可接受;否则推荐使用 subscribe 并配合 complete/unsubscribe 事件管理生命周期。

与测试用例的相互印证

仓库中的 fetchQuery-test.js 覆盖了本文涉及的大部分行为,可作为阅读与调试的参考:

  • 不 retain 数据fetches request and does not retain data 断言完成时 retained.length === 0
  • next 回调数据provides data snapshot on next 断言 next 收到从 store 读取的快照 {node: {id: '4'}}
  • 错误处理handles error correctly 断言 error 事件收到 'Oops' 错误;
  • 取消订阅unsubscribes when request is disposed 验证 unsubscribe 事件;
  • toPromise 语义fetches request and does not retain query data 验证 resolve 后数据、请求仍在途(isLoading 为 true)直至手动 complete;
  • store-or-network 策略fetches data if not cached yetreads from store if cached already 分别验证"store 无数据走网络"与"store 有数据直接读"两条分支;
  • 字段级错误fetchQuery with missing @required value 用例验证了缺失 @required 值时 next 仍会触发、并通过 relayFieldLogger 记录告警的行为。

总结与选型建议

一句话总结 fetchQuery 的使用要点:

  • 在 React 之外做命令式查询,用它;在组件渲染中做数据获取,优先考虑 useLazyLoadQuery / useQueryLoader 等 Hook;
  • 默认 force: true 绕过网络响应缓存,默认 fetchPolicy: 'network-only' 永远走网络;需要读缓存时改用 'store-or-network'
  • 永远记得它不 retain 数据,需要长期保留时配合 environment.retain()(参考 availability-of-data);
  • 尽量使用 .subscribe() 管理请求生命周期,避免 toPromise() 导致 deferred/3D 数据缺失。

更完整的函数级文档可继续阅读 fetchQuery.d.ts 与当前版本的 fetch-query 文档(新版新增了 fetchPolicynetworkCacheConfig 的完整说明)。

登录后查看全文
relay