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-runtime与react-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内部的核心调用链为:
- 通过
getRequest(config.subscription)解析graphql标签生成的请求描述; - 校验
operationKind必须是subscription,否则直接抛错'requestSubscription: Must use Subscription operation'; - 使用
createOperationDescriptor(subscription, variables, cacheConfig)构造操作描述符; - 调用
environment.executeSubscription({operation, updater})获得一个RelayObservable; - 对该 Observable
.subscribe({complete, error, next}),把回调映射为onCompleted/onError/onNext; - 返回一个封装了
sub.unsubscribe的Disposable。
参数(Arguments)
requestSubscription接受两个参数:
environment:一个 Relay Environment(实现 RelayStoreTypes 中的 IEnvironment)。通常来自useRelayEnvironment(),或者应用顶层创建的RelayModernEnvironment实例。config:类型为GraphQLSubscriptionConfig<TSubscriptionPayload>的配置对象,其字段详见下文。
GraphQLSubscriptionConfig配置字段详解
以下字段定义源自 GraphQLSubscriptionConfig.md,同时与 requestSubscription.d.ts 中的 TypeScript 类型一一对应:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
subscription | GraphQLTaggedNode | 是 | 使用graphql模板字符串声明的订阅操作 |
variables | Variables | 是 | 传递给订阅操作的变量 |
cacheConfig | CacheConfig | 否 | 缓存与传输配置(见下文) |
onCompleted | () => void | 否 | 服务端结束订阅(complete)时执行的回调 |
onError | (error: Error) => void | 否 | 订阅出错时执行的回调 |
onNext | (payload: TSubscriptionPayload) => void | 否 | 收到新数据时执行的回调 |
updater | SelectorStoreUpdater | 否 | 命令式读写 Relay Store 的更新函数 |
configs | Array<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:
| 字段 | 类型 | 说明 |
|---|---|---|
force | boolean | 为true时无条件发起请求,忽略任何配置的响应缓存状态 |
poll | number | 以指定毫秒间隔轮询实现实时更新(该值会被传给setTimeout) |
liveConfigId | string | 通过调用 GraphQLLiveQuery 实现实时更新,表示做 live query 时网关的一种配置 |
metadata | object | 用户自定义元数据 |
transactionId | string | 用户提供、用于唯一标识某次操作执行实例的值 |
在 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 参考文档。
从源码看,当configs与updater同时提供时,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 通知订阅了相关数据的组件重渲染。注意executeSubscription与executeMutation不同,它不携带乐观更新(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)不会被清空,多次推送会在连接中依次追加Mark与Zuck两条记录。
实战:把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 } } }这样每当事件流产生事件,FeedbackDisplay与FeedbackDetail组件所依赖的数据都会被一并更新,且相比手动重查数据,单次往返即可取回全部所需字段。
场景三:声明式指令与@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);注意事项与最佳实践
updater与configs不可同时提供,否则会触发 warning,且configs优先被转换。- 务必保存并调用返回的
Disposable,在不需要订阅时调用dispose(),防止连接泄漏。 onNext的 payload 会在片段展开边界处截断:若需要访问片段内部数据,请通过updater读写 Store。- 传入 Hook 的配置对象需要记忆化(
useMemo),否则每次渲染都会重建订阅——这一点对基于requestSubscription的封装同样适用。 - 订阅事件流与所选字段无必然关联:事件流是任意的,服务端推送的 payload 与客户端选择的字段之间不存在「值一定变化」的保证,Relay 只负责把收到的数据规范化并合并进 Store。
延伸阅读
- GraphQL 订阅使用指南:
useSubscription/requestSubscription的完整实战讲解 - useSubscription 源码:Hook 对
requestSubscription的封装 - requestSubscription 源码:本文引用的核心实现
- requestSubscription 测试:覆盖
RANGE_ADD、cacheConfig、updater、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
相关推荐
Relay requestSubscription API 深度指南:以命令式方式建立 GraphQL 订阅
Relay requestSubscription API 深度指南:以命令式方式建立 GraphQL 订阅 requestSubscription 是 Rel
前端开发工具Relay requestSubscription 命令式 GraphQL 订阅 API 完全指南
Relay requestSubscription 命令式 GraphQL 订阅 API 完全指南 导读 requestSubscription 是 Relay
前端开发工具Relay 18 `requestSubscription` 完全指南:命令式建立 GraphQL 订阅的 API 详解
Relay 18 requestSubscription 完全指南:命令式建立 GraphQL 订阅的 API 详解 导读 requestSubscriptio
前端开发工具