【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
本文基于 HowToGraphQL(The Fullstack Tutorial for GraphQL)仓库中 React + Relay 前端教程的 Subscriptions 章节(content/frontend/react-relay/7-subscriptions.md),完整讲解如何在 React 应用中通过 Relay Modern 的requestSubscriptionAPI 与subscriptions-transport-ws包接入 GraphQL 订阅,实现"其他用户投票时,所有在线用户的票数字段无需刷新页面即可实时变化"的效果。读完本文,你将掌握 RelayEnvironment中订阅端点的配置方式、订阅查询与缓存updater的编写方法,以及订阅的挂载位置选择与验证手段。
GraphQL Subscriptions 是什么:从请求-响应到事件流
GraphQL 的三种操作类型中,subscription 与 query、mutation 有着本质区别:
- query 与 mutation 遵循"请求-响应循环"(request-response-cycle):客户端发一次请求,服务端回一次响应,连接即结束;
- subscription 则代表一条数据流(stream):客户端向服务端"订阅"某类事件,之后每当该事件在服务端真实发生时,服务端都会主动把对应数据推送给客户端。
这里的"事件"通常对应 mutation 引发的数据变更——数据的创建、更新或删除。在 HowToGraphQL 的 HackerNews 示例中,具体目标就是:当其他用户对某个 link 投票(Vote被创建)时,当前页面显示的votes.count立即自增。
订阅最常见的实现方式是 WebSocket:服务端与已订阅的客户端之间维持一条长连接,事件发生时通过该连接下发数据。这也是本教程前端使用wss协议端点的原因。
Relay Modern 的订阅 API:requestSubscription
需要特别说明的是:订阅能力直到 Relay Modern(Relay 1.0)才进入 Relay。在此之前,Relay Classic 并不提供订阅支持。Relay Modern 提供了requestSubscription函数,用于向服务端发起订阅。
requestSubscription的使用方式与前面 mutation 章节中的commitMutation非常相似:
- 同样传入当前项目的
environment实例; - 同样可以提供一个
updater回调,指明收到服务端新数据后应如何更新 Relay 缓存(Store); - 同样支持
onError回调处理错误。
但有一点不同:为了让requestSubscription真正工作,必须改造 Relay 的Network——Network除了需要"发起查询/变更请求"的函数外,还需要第二个函数,专门负责"知道订阅端点地址、能够建立并维持到订阅端点的连接"。如果订阅基于 WebSocket,该端点使用wss协议而非http(s)。
后端前提:Graphcool 的 Simple API 与 Relay API 之分
本教程后端使用 Graphcool(教程使用的是其 legacy 版本,见 content/frontend/react-relay/1-getting-started.md 中的说明)。Graphcool 为每个项目暴露两套类型定义略有差异的 GraphQL API:
- Simple API:提供所有模型类型的直觉式 CRUD 能力;
- Relay API:满足 Relay 对 GraphQL schema 的要求(如
Viewer、Node、Edge等类型)。
当时的限制是:订阅只直接支持 Simple API。要在 Relay API 中使用订阅,需要对你喂给relay-compiler的schema.graphql做手工调整(在 schema 中补充Subscription类型定义)。HowToGraphQL 已替学习者完成了这些调整:教程第 3 章要求从https://graphqlbin.com/hn-relay-full.graphql下载 schema,该文件里已经包含了手工添加的Subscription类型。你可以直接查看自己项目根目录下的schema.graphql中的Subscription类型,确认它定义了形如Vote(新增投票)这样的订阅字段。
第一步:为项目引入 WebSocket 支持
在 Relay 项目根目录执行:
yarn add subscriptions-transport-ws@0.8.3注意教程特意锁定了0.8.3版本,原因是该版本提供了SubscriptionClient这个 API(教程原文注明后续会更新到最新版 API)。SubscriptionClient实现了标准的 GraphQL over WebSocket 协议,与 Graphcool 的 subscriptions API 协议兼容,因此是这里的合适选择。
第二步:改造 Relay Environment,注册订阅端点
打开src/Environment.js,把此前仅含一个查询函数的Network.create改造为接收两个函数的形式:
import { SubscriptionClient } from 'subscriptions-transport-ws' // 1 const fetchQuery = (operation, variables) => { return fetch('https://api.graph.cool/relay/v1/__PROJECT_ID__', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem(GC_AUTH_TOKEN)}` }, body: JSON.stringify({ query: operation.text, variables, }), }).then(response => { return response.json() }) } // 2 const setupSubscription = (config, variables, cacheConfig, observer) => { const query = config.text const subscriptionClient = new SubscriptionClient('wss://subscriptions.__REGION__.graph.cool/v1/__PROJECT_ID__', {reconnect: true}) subscriptionClient.subscribe({query, variables}, (error, result) => { observer.onNext({data: result}) }) } // 3 const network = Network.create(fetchQuery, setupSubscription)三段代码各自的职责:
fetchQuery:与教程早期章节完全相同的查询/变更请求闭包,只是从匿名函数抽成了具名变量,以便与下面的函数一起传给Network.create。其中的__PROJECT_ID__需要替换为你 Graphcool 项目的实际 ID;Authorization头携带第 5 章(Authentication 章节)存入localStorage的令牌。setupSubscription:Network用于"与订阅端点通信"的第二个函数。它接收config(其中config.text就是订阅查询文本,决定客户端关心什么事件、要接收哪些字段),内部用SubscriptionClient建立并维持到wss://subscriptions.__REGION__.graph.cool/v1/__PROJECT_ID__的连接(reconnect: true表示断线自动重连)。每当服务端推送数据,就通过observer.onNext({data: result})把结果交给 Relay 运行时,后者再去执行你在requestSubscription里声明的updater。这里有两个占位符要替换:__PROJECT_ID__为项目 ID;__REGION__为你的 API 所在的 AWS 区域(例如ap-northeast-1)。Network.create(fetchQuery, setupSubscription):用两个函数创建Network,随后照旧用于实例化 RelayEnvironment。
关于占位符的查证方法(教程给出的两条路径):
- 查看项目文件
project.graphcool的 frontmatter(第一行注释# project: <id>即项目 ID);或在终端执行graphcool endpoints,命令会输出 Relay API 与 Subscriptions API 等全部端点; __REGION__的确认方法:打开 Graphcool 控制台,点击左下角Endpoints按钮,查看Subscriptions API一行给出的完整地址,例如wss://subscriptions.ap-northeast-1.graph.cool/v1/<project-id>,把其中的区域名填回占位符即可。
第三步:编写 NewVoteSubscription 订阅封装
延续本教程"每种 mutation/subscription 都放在专门文件里、对外导出一个便捷函数"的约定:
- 在
src下新建subscriptions目录; - 在其中创建
NewVoteSubscription.js,写入以下内容:
import { graphql, requestSubscription } from 'react-relay' import environment from '../Environment' const newVoteSubscription = graphql` subscription NewVoteSubscription { # 1 Vote { # 2 node { id user { id } link { id _votesMeta { count } } } } } ` // 3 export default () => { const subscriptionConfig = { subscription: newVoteSubscription, variables: {}, updater: proxyStore => { const createVoteField = proxyStore.getRootField('Vote') const newVote = createVoteField.getLinkedRecord('node') const updatedLink = newVote.getLinkedRecord('link') const linkId = updatedLink.getValue('id') const newVotes = updatedLink.getLinkedRecord('_votesMeta') const newVoteCount = newVotes.getValue('count') const link = proxyStore.get(linkId) link.getLinkedRecord('votes').setValue(newVoteCount, 'count') }, onError: error => console.log(`An error occured:`, error) } requestSubscription( environment, subscriptionConfig ) }逐段拆解:
1. 订阅的根字段表达"事件"。Vote字段声明客户端关心Vote类型上发生的事件(本例即"有新的投票被创建")。Graphcool 会为每个模型类型在Subscription类型下生成同名字段作为事件入口。
2. payload 决定每次推送携带什么数据。node字段代表刚创建的Vote记录,每次有人投票,服务端都会推送:新投票的id、投票者user.id,以及被投链接的link.id和link._votesMeta.count(_votesMeta是 Graphcool 在 Relay API 中为关系提供的元数据连接,count即该链接当前的总票数——注意这里是服务端算好的绝对值,不是增量)。
3. 导出函数封装requestSubscription。导出的默认函数可以在应用任何位置调用,其内部组装subscriptionConfig并真正向服务端提交订阅。其中updater是订阅生效的关键,它与第 6 章投票 mutation 中commitMutation的updater(参见 content/frontend/react-relay/6-more-mutations-and-updating-the-store.md)使用的是同一套 Relay Store 代理 API:
proxyStore.getRootField('Vote'):Vote是订阅的根字段,订阅 payload 相对 mutation payload 多包了一层node,所以要先getLinkedRecord('node')取出新投票;- 再依次遍历
link→_votesMeta,用getValue('count')拿到标量票数; proxyStore.get(linkId)按 Relay ID 取出缓存中的链接记录,link.getLinkedRecord('votes').setValue(newVoteCount, 'count')将票数字段直接改写为服务端下发的最新值。由于第 6 章已在Link组件的 fragment 中查询了votes { count },此处改写会立即反映到所有渲染该字段的组件上,无需任何手动重渲染逻辑。
第四步:挂载订阅——为什么放在 LinkList 而不是 Link
订阅的调用位置看似无关紧要(它没有依赖组件上下文的变量参数),但有一个硬约束:订阅只能被发起一次。如果把它放进Link组件,页面上渲染多少条链接就会发起多少条相同的 WebSocket 订阅,既浪费连接又造成重复推送。
因此教程把它放进只挂载一次的LinkList组件中。打开src/components/LinkList.js,添加:
import NewVoteSubscription from '../subscriptions/NewVoteSubscription' componentDidMount() { NewVoteSubscription() }componentDidMount保证组件进入 DOM 后才发起订阅,且LinkListPage路由下该组件只会挂载一次,从而满足"仅订阅一次"的约束。
第五步:编译 GraphQL 代码并验证
由于NewVoteSubscription.js中新增了带graphql标签的订阅文档,必须重新运行 Relay Compiler 让它通过 schema 校验并生成编译产物:
relay-compiler --src ./src --schema schema.graphql说明:schema 文件就是教程第 1 章中用
get-graphql-schema从 Relay API 端点下载、并包含手工添加的Subscription类型的schema.graphql。若编译器在此报"未知字段 Vote",通常说明你用的还是未调整的 schema。
然后运行yarn start启动应用。教程推荐的验证方式是开两个浏览器窗口(或标签页)同时运行该应用:在一个窗口中对某条链接投票,另一个窗口中该链接的票数字应立即自动 +1,全程不刷新页面。这正是订阅链路完整跑通的表现——WebSocket 推送 →setupSubscription中的observer.onNext→ Relay 执行updater→ Store 变更 → 组件重新渲染。
小结与常见排查点
把本章节放入整个 React + Relay 教程的脉络中看(章节结构见 meta/structure/react-relay.md):
| 环节 | 关键 API / 命令 | 说明 |
|---|---|---|
| WebSocket 客户端 | subscriptions-transport-ws@0.8.3的SubscriptionClient | 教程锁定 0.8.3 以获得SubscriptionClientAPI |
| 环境配置 | Network.create(fetchQuery, setupSubscription) | 第二个函数负责建立并维持wss订阅连接 |
| 订阅发起 | requestSubscription(environment, config) | 与commitMutation同构,支持updater/onError |
| 缓存更新 | proxyStore.getRootField('Vote')→getLinkedRecord→setValue | 订阅 payload 比 mutation 多一层node |
| 编译 | relay-compiler --src ./src --schema schema.graphql | 每次修改graphql标签代码后必须执行 |
| 验证 | 双窗口投票测试 | 一个窗口投票,另一窗口票数即时变化 |
几个容易踩坑的点:
- 占位符未替换:
__PROJECT_ID__与__REGION__必须替换为graphcool endpoints输出中的真实值,region以控制台 Endpoints 面板中 Subscriptions API 一行为准; - schema 缺少
Subscription类型:Relay API 需要手工调整 schema 才能通过 compiler 校验(教程已通过graphqlbin.com/hn-relay-full.graphql预置); - 订阅被重复发起:确认订阅调用只出现在单例挂载的组件(本例为
LinkList.componentDidMount)中; - 页面看不到更新:先检查
onError是否打印了错误,再确认Link组件的 fragment 确实查询了votes { count }——只有被查询过的字段参与缓存失效与重渲染。
最后留一个与原文一致的自检问题:Relay 提供的、用于向服务端发起订阅的函数名叫什么?(答案:requestSubscription)
【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
相关推荐
howtographql React & Relay 教程:用 Relay 指令式 API 实现投票 Mutation 与 Store 缓存更新
howtographql React & Relay 教程:用 Relay 指令式 API 实现投票 Mutation 与 Store 缓存更新 本文基于 Ho
HowToGraphQL React + Apollo 实战:用 GraphQL Subscriptions 与 WebSocketLink 实现实时数据推送
HowToGraphQL React + Apollo 实战:用 GraphQL Subscriptions 与 WebSocketLink 实现实时数据推送
howtographql 中 Angular + Apollo 实战:用 GraphQL Subscriptions 实现 Hacker News 克隆的实时更新
howtographql 中 Angular + Apollo 实战:用 GraphQL Subscriptions 实现 Hacker News 克隆的实时更
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考