news 2026/9/23 5:23:05

Relay `requestSubscription` API 完全指南:以命令式方式建立 GraphQL 订阅

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Relay `requestSubscription` API 完全指南:以命令式方式建立 GraphQL 订阅

requestSubscription是 Relay(packages/react-relay)提供的命令式(imperative)订阅 API,用于在任意时机(例如事件回调、服务端推送触发时)建立 GraphQL Subscription,并在收到服务端事件流数据后自动将其规范化并写入 Relay Store,驱动依赖数据的组件实时刷新。阅读本文后,你将掌握requestSubscription(environment, config)的完整签名、GraphQLSubscriptionConfig中每一个配置字段的含义与底层实现、Disposable返回值的使用方式,以及如何借助updater、声明式指令(declarative directives)和网络层 WebSocket 配置将其落地到真实应用。

本文以 version-v17.0.0 的 requestSubscription 参考文档 为主体骨架,结合relay-runtimereact-relay的源码实现与单元测试展开讲解。

requestSubscription是什么

requestSubscription是一个命令式 API,用于建立 GraphQL Subscription。它与 React Hooks 中声明式的useSubscription形成互补:Hook 适合在组件挂载时自动建立订阅,而requestSubscription可以在任何 JavaScript 环境中调用——包括事件处理器、定时任务、非组件模块等场景。

import {graphql, requestSubscription} from 'react-relay'; const subscription = graphql` subscription UserDataSubscription($input: InputData!) { # ... } `; function createSubscription(environment: IEnvironment): Disposable { return requestSubscription(environment, { subscription, variables: {input: {userId: '4'}}, }); }

从源码看(requestSubscription.js),requestSubscription内部的核心调用链为:

  1. 通过getRequest(config.subscription)解析graphql标签生成的请求描述;
  2. 校验operationKind必须是subscription,否则直接抛错'requestSubscription: Must use Subscription operation'
  3. 使用createOperationDescriptor(subscription, variables, cacheConfig)构造操作描述符;
  4. 调用environment.executeSubscription({operation, updater})获得一个RelayObservable
  5. 对该 Observable.subscribe({complete, error, next}),把回调映射为onCompleted/onError/onNext
  6. 返回一个封装了sub.unsubscribeDisposable

参数(Arguments)

requestSubscription接受两个参数:

  • environment:一个 Relay Environment(实现 RelayStoreTypes 中的 IEnvironment)。通常来自useRelayEnvironment(),或者应用顶层创建的RelayModernEnvironment实例。
  • config:类型为GraphQLSubscriptionConfig<TSubscriptionPayload>的配置对象,其字段详见下文。

GraphQLSubscriptionConfig配置字段详解

以下字段定义源自 GraphQLSubscriptionConfig.md,同时与 requestSubscription.d.ts 中的 TypeScript 类型一一对应:

字段类型必填说明
subscriptionGraphQLTaggedNode使用graphql模板字符串声明的订阅操作
variablesVariables传递给订阅操作的变量
cacheConfigCacheConfig缓存与传输配置(见下文)
onCompleted() => void服务端结束订阅(complete)时执行的回调
onError(error: Error) => void订阅出错时执行的回调
onNext(payload: TSubscriptionPayload) => void收到新数据时执行的回调
updaterSelectorStoreUpdater命令式读写 Relay Store 的更新函数
configsArray<DeclarativeMutationConfig>声明式变更配置(与updater互斥)

subscription:使用graphql标签声明订阅

与查询(query)和变更(mutation)一样,订阅也通过graphql标签声明,区别在于顶层关键字是subscription。Relay 编译器会为订阅操作生成对应的类型,因此GraphQLSubscriptionConfig可以携带泛型参数实现静态类型检查——这是官方推荐的最佳实践。

一个订阅示例(源自 graphql-subscriptions.mdx):

subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) { feedback_like_subscribe(data: $input) { feedback { like_count } } }

feedback_like_subscribe是订阅根字段(subscription root field),它在后端建立事件流;订阅建立后,每当该事件流产生事件,客户端就会收到一个形如下面的 payload,Relay 会将其规范化并合并进 Store:

{ "feedback_like_subscribe": { "feedback": { "id": "feedback-id", "like_count": 321 } } }

由于Feedback类型包含id字段,Relay 编译器会自动为订阅补上id的选择;收到响应后,Relay 会在 Store 中按id匹配对应记录并更新字段值,凡是依赖这些字段的组件都会自动重渲染。

variables:订阅变量

与查询/片段一样,订阅可以引用 GraphQL 变量,variables字段用于传入这些变量的具体值。示例中{input: {userId: '4'}}即传入订阅所需的输入参数。

cacheConfig:缓存与传输配置

cacheConfig的类型为CacheConfig,其可选字段定义于 CacheConfig.md:

字段类型说明
forcebooleantrue时无条件发起请求,忽略任何配置的响应缓存状态
pollnumber以指定毫秒间隔轮询实现实时更新(该值会被传给setTimeout
liveConfigIdstring通过调用 GraphQLLiveQuery 实现实时更新,表示做 live query 时网关的一种配置
metadataobject用户自定义元数据
transactionIdstring用户提供、用于唯一标识某次操作执行实例的值

在 requestSubscription.js 中,cacheConfig会被透传给createOperationDescriptor,最终由environment.executeSubscription传递给网络层执行函数(见 RelayModernEnvironment.js 中this.getNetwork().execute(operation.request.node.params, operation.request.variables, operation.request.cacheConfig || {}, null))。因此,metadata等字段可以原样到达你的网络层 fetch/subscribe 函数,用于埋点、追踪等场景——这一点也被测试用例requestSubscription() cacheConfig所验证(requestSubscription-test.js)。

onCompleted/onError/onNext:订阅生命周期回调

这三个回调分别对应订阅的三个生命周期事件:

  • onNext:收到订阅 payload 时执行,参数是订阅响应数据(在片段展开边界处停止);源码中会先通过environment.lookup(selector).data从 Store 读取数据再传给回调(requestSubscription.js)。
  • onError:订阅出错时执行,参数为错误对象。
  • onCompleted:服务端结束订阅(流关闭)时执行。

updater:命令式更新 Relay Store

updater的类型为SelectorStoreUpdater,签名是(store: RecordSourceSelectorProxy, data) => void。通过它你可以命令式地直接读写 Relay Store,从而对订阅 payload 的落库方式拥有完全控制权:可以创建全新的记录(record),也可以更新或删除已有记录。完整的 Store 读写 API 可参见 store 参考文档。

从源码看,当configsupdater同时提供时,requestSubscription会输出一条 warning:'requestSubscription: Expected only one of updater and configs to be provided'(requestSubscription.js),即两者互斥,只能二选一。

configs:声明式变更配置

configs允许你使用DeclarativeMutationConfig以声明方式描述 Store 变更,最常见的场景是把新到达的数据追加到某个 connection。源码中若提供了configs,会通过RelayDeclarativeMutationConfig.convert(configs, subscription, null /* optimisticUpdater */, config.updater)将其转换为updater(requestSubscription.js)。

测试用例Config: RANGE_ADD(requestSubscription-test.js)演示了完整用法:用RANGE_ADD配置把新评论追加到FeedbackCommentQuery_comments连接上:

const configs = [ { type: 'RANGE_ADD', connectionName: 'comments', connectionInfo: [ { key: 'FeedbackCommentQuery_comments', rangeBehavior: 'append', }, ], parentID: feedbackId, edgeName: 'feedbackCommentEdge', }, ]; requestSubscription(environment, { configs, subscription: CommentCreateSubscription, variables: { input: {feedbackId, text: secondCommentBody}, }, });

测试随后通过environment.mock.nextValue(CommentCreateSubscription, subscriptionPayload)模拟服务端推送,并断言 Store 中的评论列表追加了新条目。

返回值(Return Type):Disposable

requestSubscription返回一个Disposable对象,用于清理订阅。其接口定义于 Disposable.md:

type Disposable = { dispose: () => void, };

调用dispose()即取消订阅(源码中即sub.unsubscribe,requestSubscription.js)。典型用法是在组件卸载、路由切换或业务结束时调用,避免内存泄漏与多余的网络连接。

行为(Behavior)与底层实现细节

操作类型强制校验

requestSubscription只接受operationKind === 'subscription'的操作,否则抛出'requestSubscription: Must use Subscription operation'错误(requestSubscription.js)。这保证了你不会误把 query 或 mutation 传给该 API。

environment.executeSubscription调用链

订阅的实际执行委托给 Environment 的executeSubscription方法(RelayModernEnvironment.js):

executeSubscription({operation, updater}) { return this._execute({ createSource: () => this.getNetwork().execute( operation.request.node.params, operation.request.variables, operation.request.cacheConfig || {}, null, ), isClientPayload: false, operation, optimisticConfig: null, updater, }); }

即:通过网络层拿到一个「多值响应流」的RelayObservable,随后_execute会逐条规范化响应数据并提交到发布队列(publish queue),最后通过 Store 通知订阅了相关数据的组件重渲染。注意executeSubscriptionexecuteMutation不同,它不携带乐观更新(optimisticConfig: null),也不强制force: true

onNext中的 rootID 处理

一个值得注意的实现细节:当响应带有extensions.__relay_subscription_root_id时,requestSubscription会通过createReaderSelector用该 ID 替换operation.fragment的 dataID 后再lookup(requestSubscription.js)。这样即使订阅期间数据被其他操作重写,onNext也能读取到正确根节点下的最新数据。测试用例reads the data using the correct rootID in onNext(requestSubscription-test.js)验证了这一点,并确认onNext返回的数据会在片段展开边界处截断、updater恰好被调用一次。

不会覆盖已有数据

requestSubscription的更新是「增量合并」而非「整体替换」:测试用例does not overwrite existing data(requestSubscription-test.js)展示了订阅 payload 中只包含config字段时,Store 中已存在的其它字段(如 fragment 展开所需的isEnabled)不会被清空,多次推送会在连接中依次追加MarkZuck两条记录。

实战:把requestSubscription用在真实应用中

场景一:组件外建立订阅

useSubscription的底层其实就是requestSubscription——useSubscription.js 在useEffect中调用requestSubscription(environment, config)并在清理函数中调用dispose。因此,当你需要在组件生命周期之外(例如全局事件总线、Web Worker 消息、导航守卫)建立订阅时,直接使用requestSubscription并妥善保管返回的Disposable

const disposable = requestSubscription(environment, { subscription, variables: {input}, }); // 不再需要订阅时 disposable.dispose();

场景二:用片段展开驱动组件刷新

与其在订阅中手动挑选字段,更推荐直接展开组件对应的片段,例如:

subscription FeedbackLikeSubscription($input: FeedbackLikeSubscribeData!) { feedback_like_subscribe(data: $input) { feedback { ...FeedbackDisplay_feedback ...FeedbackDetail_feedback } } }

这样每当事件流产生事件,FeedbackDisplayFeedbackDetail组件所依赖的数据都会被一并更新,且相比手动重查数据,单次往返即可取回全部所需字段。

场景三:声明式指令与@deleteRecord

声明式变更指令(declarative mutation directives)在订阅中同样生效,例如删除记录:

subscription DeletePostSubscription($input: DeletePostSubscribeData!) { delete_post_subscribe(data: $input) { deleted_post { id @deleteRecord } } }

场景四:配置网络层以支持订阅

订阅通常通过 WebSocket 传输,需要为Network.create提供第二个参数subscribe函数(详见 graphql-subscriptions.mdx 的网络层配置)。以graphql-ws为例:

import {Network, Observable} from 'relay-runtime'; import {createClient} from 'graphql-ws'; const wsClient = createClient({url: 'ws://localhost:3000'}); const subscribe = (operation, variables) => { return Observable.create((sink) => { return wsClient.subscribe( { operationName: operation.name, query: operation.text, variables, }, sink, ); }); }; const network = Network.create(fetchQuery, subscribe);

注意事项与最佳实践

  • updaterconfigs不可同时提供,否则会触发 warning,且configs优先被转换。
  • 务必保存并调用返回的Disposable,在不需要订阅时调用dispose(),防止连接泄漏。
  • onNext的 payload 会在片段展开边界处截断:若需要访问片段内部数据,请通过updater读写 Store。
  • 传入 Hook 的配置对象需要记忆化useMemo),否则每次渲染都会重建订阅——这一点对基于requestSubscription的封装同样适用。
  • 订阅事件流与所选字段无必然关联:事件流是任意的,服务端推送的 payload 与客户端选择的字段之间不存在「值一定变化」的保证,Relay 只负责把收到的数据规范化并合并进 Store。

延伸阅读

  • GraphQL 订阅使用指南:useSubscription/requestSubscription的完整实战讲解
  • useSubscription 源码:Hook 对requestSubscription的封装
  • requestSubscription 源码:本文引用的核心实现
  • requestSubscription 测试:覆盖RANGE_ADDcacheConfigupdater、rootID 等行为的单元测试
  • TypeScript 类型声明:GraphQLSubscriptionConfig的精确类型
  • store 参考文档:updater中可用的 Store 读写 API
  • updating-data 系列指南:关于 Store 更新方式的更多说明
  • 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay

点击查看免费下载

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 5:22:55

手机排行前十名数据实战:3000行源码教你搞定排行榜项目

手机排行前十名数据实战:3000行源码教你搞定排行榜项目 看了一堆教程还是不会写项目?这大概是90%的开发者在接手“手机排行前十名”这类需求时的真实写照。你背熟了SQL语法,能写出复杂的JOIN,但一旦老板让你做一个实时更新的、带缓存策略的排行榜实战项目,你脑子里一片空白。…

作者头像 李华
网站建设 2026/9/23 5:22:48

5年老兵教你一文搞懂网页制作工具底层逻辑

5年老兵教你一文搞懂网页制作工具底层逻辑 看了一堆教程还是不会写项目?别慌,这锅不背你,要背那些只会教“点哪里”的割韭菜视频。很多人花了几千块买课,学会了拖拽,结果换个需求就抓瞎,根本不知道浏览器到底在干嘛。今天咱们不玩虚的, 一文搞懂 网页制作工具背后的真实运行机制。…

作者头像 李华
网站建设 2026/9/23 5:22:26

3个图解原理破解星空软件卡顿面试必问

3个图解原理破解星空软件卡顿面试必问 看了一堆教程还是不会写项目?别急,问题不在你不够努力,而在你没看懂代码底层的“呼吸”。很多刚入行或者准备跳槽去 星空软件 这种大厂的同学,面试时最头疼的不是八股文,而是问:“这段代码为什么慢?”、“怎么优化?”。…

作者头像 李华
网站建设 2026/9/23 5:22:20

张云云面试突击:3个核心考点解决代码跑不通与性能优化难题

张云云面试突击:3个核心考点解决代码跑不通与性能优化难题 复制来的代码跑不通,报错信息看得人头皮发麻,根本不知道从哪下手调。这种“黑盒”状态最磨人,改一行崩一行,最后只能硬背八股文应付面试。别急,今天直接拆解【张云云】相关的技术栈高频考点,直击 性能优化 与底层逻辑,让你不再当“复制粘贴侠”。…

作者头像 李华
网站建设 2026/9/23 5:22:14

3个代码坑带你搞定光纤法兰监控最佳实践

3个代码坑带你搞定光纤法兰监控最佳实践 面试被问原理答不上来,简历上写着“熟悉网络监控”却连光纤法兰的告警逻辑都讲不清,这种尴尬你经历过吗?很多转岗做运维或开发的朋友,在准备技术博客或面试时,往往卡在“光纤法兰”这个具体硬件的管理上。它不是简单的插拔,而是涉及物理层状态、光功率监测和自动切换的复杂场…

作者头像 李华