订阅
从您的 GraphQL 服务器获取实时更新
除了查询和变更之外,GraphQL还支持第三种操作类型:订阅。
像查询一样,订阅允许您获取数据。不同之处在于,订阅是长期存在的操作,其结果会随时间变化。它们可以保持与您的GraphQL 服务器的活跃连接(通常是通过 WebSocket),允许服务器向订阅的结果推送更新。
订阅对于实时通知客户端后端数据的更改非常有用,例如新对象的创建或重要字段的更新。
何时使用订阅
在大多数情况下,您的客户端应不要使用订阅来保持与后端的同步。相反,您应通过查询间歇性地轮询,或者在用户执行相关操作(如点击按钮)时重新执行查询。
您应使用订阅进行以下操作:
对大型对象进行少量增量更改。重复轮询大型对象是非常昂贵的,尤其是当对象的大部分字段很少更改时。相反,您可以使用查询获取对象的初始状态,并且您的服务器可以主动推送到单个字段在发生时的更新。
低延迟、实时更新。例如,聊天应用程序的客户端希望在新消息可用时立即接收。
注意: 订阅不能用于监听本地客户端事件,如订阅缓存中的更改。订阅旨在用于订阅外部数据更改,并存储这些接收到的更改。然后您可以使用Apollo客户端的观察模型来监视缓存中的更改,使用client.watchQuery或useQuery来监视。
支持的订阅协议
The GraphQL规范没有定义用于发送订阅请求的特定协议。Apollo Client支持以下协议进行订阅:
- WebSocket使用以下子协议之一:
graphql-wssubscriptions-transport-ws(⚠️ 未维护)
- HTTP使用分段的多部分响应(Apollo Client 3.7.11及以后版本)
您必须使用与您通信的GraphQL端点相同的协议。
WebSocket子协议
第一个支持WebSocket上订阅的JavaScript库被称为subscriptions-transport-ws。 此库不再积极维护。其继任者是一个名为graphql-ws的库。这这两个库不使用相同的WebSocket子协议,因此您需要使用与您的GraphQL端点相同的子协议。
以下 WebSocket 配置部分使用 graphql-ws。如果你的端点使用 subscriptions-transport-ws,请查看 本节,了解配置差异。
注意: 令人困惑的是,subscriptions-transport-ws 库将其 WebSocket 子协议 称为 graphql-ws,而 graphql-ws 库称其子协议为 graphql-transport-ws!在本篇文章中,我们指的是两个 库(subscriptions-transport-ws 和 graphql-ws),而非两个子协议。
HTTP
要使用 Apollo Client 连接到支持通过 HTTP 进行多部分订阅的 GraphQL 端点,请确保您使用的是 multipart subscriptions over HTTP的版本为 3.7.11或更高版本。
除了更新您的客户端版本外,无需任何其他配置! Apollo Client 会自动将所需的头信息连同请求一起发送,如果传递了一个订阅 HTTPLink 操作。
与 Relay 或 urql 一起使用
在支持 Relay 或 urql 的应用程序中消费通过 HTTP 进行多部分订阅,Apollo Client 提供网络层适配器,用于解析多部分响应格式。
Relay
import { createFetchMultipartSubscription } from '@apollo/client/utilities/subscriptions/relay';import { Environment, Network, RecordSource, Store } from 'relay-runtime';const fetchMultipartSubs = createFetchMultipartSubscription('https://api.example.com');const network = Network.create(fetchQuery, fetchMultipartSubs);export const RelayEnvironment = new Environment({network,store: new Store(new RecordSource()),});
import { createFetchMultipartSubscription } from '@apollo/client/utilities/subscriptions/relay';import { Environment, Network, RecordSource, Store } from 'relay-runtime';const fetchMultipartSubs = createFetchMultipartSubscription('https://api.example.com');const network = Network.create(fetchQuery, fetchMultipartSubs);export const RelayEnvironment = new Environment({network,store: new Store(new RecordSource()),});
urql
import { createFetchMultipartSubscription } from '@apollo/client/utilities/subscriptions/urql';import { Client, fetchExchange, subscriptionExchange } from '@urql/core';const url = 'https://api.example.com';const multipartSubscriptionForwarder = createFetchMultipartSubscription(url);const client = new Client({url,exchanges: [fetchExchange,subscriptionExchange({forwardSubscription: multipartSubscriptionForwarder,}),],});
import { createFetchMultipartSubscription } from '@apollo/client/utilities/subscriptions/urql';import { Client, fetchExchange, subscriptionExchange } from '@urql/core';const url = 'https://api.example.com';const multipartSubscriptionForwarder = createFetchMultipartSubscription(url);const client = new Client({url,exchanges: [fetchExchange,subscriptionExchange({forwardSubscription: multipartSubscriptionForwarder,}),],});
定义订阅
您可以在服务器和客户端定义订阅,就像您定义查询和变更一样。
服务器端
您可以在您的 GraphQL 模式中定义可用的 订阅,作为 Subscription 类型的字段。以下 commentAdded 订阅会在添加特定博客文章(通过 postID 指定)的新评论时通知订阅客户端:
type Subscription {commentAdded(postID: ID!): Comment}
有关在服务器端实现支持 订阅的更多信息,请参阅 Apollo Server 订阅文档。
客户端
在您应用的客户端中,您定义每个 订阅的形状,如下所示:
const COMMENTS_SUBSCRIPTION: TypedDocumentNode<OnCommentAddedSubscription,OnCommentAddedSubscriptionVariables> = gql`subscription OnCommentAdded($postID: ID!) {commentAdded(postID: $postID) {idcontent}}`;
const COMMENTS_SUBSCRIPTION = gql`subscription OnCommentAdded($postID: ID!) {commentAdded(postID: $postID) {idcontent}}`;
Apollo Client 在执行 OnCommentAdded 订阅时,会与您的 GraphQL 服务器建立连接并监听响应数据。与查询不同,没有期望服务器立即处理并返回响应。相反,仅在您的后端发生特定事件时,您的服务器才会向客户端推送数据。
每当您的 GraphQL 服务器 推送数据到订阅客户端时,这些数据符合执行 订阅的结构,就像查询一样:
{"data": {"commentAdded": {"id": "123","content": "What a thoughtful and well written post!"}}}
WebSocket 设置
1. 安装所需的库
Apollo Link 是一个库,可以帮助您自定义 Apollo Client 的网络通信。您可以使用它来定义一个 链链,修改您的 operations 并将它们路由到适当的目的地。
要执行 WebSocket 上的订阅,您可以在链链中添加一个 GraphQLWsLink。此链接需要 graphql-ws 库。如下所示进行安装:
npm install graphql-ws
2. 初始化一个 GraphQLWsLink
在初始化 ApolloClient 的相同项目文件中导入并初始化一个 GraphQLWsLink 对象:
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';import { createClient } from 'graphql-ws';const wsLink = new GraphQLWsLink(createClient({url: 'ws://:4000/subscriptions',}),);
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';import { createClient } from 'graphql-ws';const wsLink = new GraphQLWsLink(createClient({url: 'ws://:4000/subscriptions',}),);
将 url 选项的值替换为您 GraphQL 服务器特定订阅的 WebSocket 终端。如果您使用的是 Apollo Server,请参阅 设置订阅端点。
3. 按操作拆分通信(推荐)
虽然 Apollo Client 可以使用您的 GraphQLWsLink 执行所有操作类型,但在大多数情况下,它应该继续使用 HTTP 执行查询和 mutations。这是因为查询和 mutations 不需要状态或持久连接,在没有 WebSocket 连接的情况下,HTTP 更高效且可扩展。
为此,@apollo/client 库提供了一个 split 函数,它允许您根据布尔检查的结果使用两种不同的 Link 之一。
以下示例在之前的基础上扩展了示例,初始化了 GraphQLWsLink 和 HttpLink。然后使用 split 函数将这两个 Link 合并到一个 single Link,并按执行的操作类型使用其一或另一个。
import { split, HttpLink } from '@apollo/client';import { getMainDefinition } from '@apollo/client/utilities';import { GraphQLWsLink } from '@apollo/client/link/subscriptions';import { createClient } from 'graphql-ws';const httpLink = new HttpLink({uri: 'https://:4000/graphql',});const wsLink = new GraphQLWsLink(createClient({url: 'ws://:4000/subscriptions',}),);// The split function takes three parameters://// * A function that's called for each operation to execute// * The Link to use for an operation if the function returns a "truthy" value// * The Link to use for an operation if the function returns a "falsy" valueconst splitLink = split(({ query }) => {const definition = getMainDefinition(query);return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';},wsLink,httpLink,);
import { split, HttpLink } from '@apollo/client';import { getMainDefinition } from '@apollo/client/utilities';import { GraphQLWsLink } from '@apollo/client/link/subscriptions';import { createClient } from 'graphql-ws';const httpLink = new HttpLink({uri: 'https://:4000/graphql',});const wsLink = new GraphQLWsLink(createClient({url: 'ws://:4000/subscriptions',}),);// The split function takes three parameters://// * A function that's called for each operation to execute// * The Link to use for an operation if the function returns a "truthy" value// * The Link to use for an operation if the function returns a "falsy" valueconst splitLink = split(({ query }) => {const definition = getMainDefinition(query);return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';},wsLink,httpLink,);
使用此逻辑,查询和 mutations 将正常使用 HTTP,而 subscriptions 将使用 WebSocket。
4. 向 Apollo Client 提供链接链
定义完您的链接链后,您可以通过 Apollo Client 的 link 构造函数选项提供:
import { ApolloClient, InMemoryCache } from '@apollo/client';// ...code from the above example goes here...const client = new ApolloClient({link: splitLink,cache: new InMemoryCache()});
import { ApolloClient, InMemoryCache } from '@apollo/client';// ...code from the above example goes here...const client = new ApolloClient({link: splitLink,cache: new InMemoryCache()});
如果您提供 link 选项,它将覆盖 uri 选项(uri 使用提供的 URL 设置一个默认的 HTTP 链)。
5. 通过 WebSocket 进行身份验证(可选)
通常需要在允许客户端接收 subscription 结果之前对其进行身份验证。为此,您可以为 GraphQLWsLink 构造函数提供一个 connectionParams 选项,如下所示:
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';import { createClient } from 'graphql-ws';const wsLink = new GraphQLWsLink(createClient({url: 'ws://:4000/subscriptions',connectionParams: {authToken: user.authToken,},}));
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';import { createClient } from 'graphql-ws';const wsLink = new GraphQLWsLink(createClient({url: 'ws://:4000/subscriptions',connectionParams: {authToken: user.authToken,},}));
您的 GraphQLWsLink 将在连接时将 connectionParams 对象传递给您的服务器。您的服务器接收 connectionParams 对象并可以使用它进行身份验证,以及执行任何其他连接相关的任务。
通过多部分 HTTP 的订阅
不需要额外的库或配置。Apollo Client 在使用初始化链或 Apollo Client 实例指定的 uri 时,将需要的标头添加到请求中,当默认终止的 HTTPLink 在 subscription 操作时。
注意:要在 React Native 应用程序中使用多部分 HTTP 的订阅功能,需要额外的配置。有关更多详细信息,请参阅 React Native 文档。
执行订阅
您可以使用 Apollo Client 的 useSubscription 钩子从 React 中执行订阅。与 useQuery 类似,useSubscription 返回一个对象,该对象包含Apollo Client 中的 loading、error 和 data 属性,您可以使用这些属性来渲染您的 UI。
以下示例组件使用之前定义的订阅来渲染指定博客文章的最新评论。每当 GraphQL 服务器向客户端推送新的评论时,组件会重新渲染并显示新的评论。
const COMMENTS_SUBSCRIPTION: TypedDocumentNode<OnCommentAddedSubscription,OnCommentAddedSubscriptionVariables> = gql`subscription OnCommentAdded($postID: ID!) {commentAdded(postID: $postID) {idcontent}}`;function LatestComment({ postID }: LatestCommentProps) {const { data, loading } = useSubscription(COMMENTS_SUBSCRIPTION,{ variables: { postID } });return <h4>New comment: {!loading && data.commentAdded.content}</h4>;}
const COMMENTS_SUBSCRIPTION = gql`subscription OnCommentAdded($postID: ID!) {commentAdded(postID: $postID) {idcontent}}`;function LatestComment({ postID }) {const { data, loading } = useSubscription(COMMENTS_SUBSCRIPTION,{ variables: { postID } });return <h4>New comment: {!loading && data.commentAdded.content}</h4>;}
订阅查询的更新
当Apollo Client 接收到查询结果时,该结果包含一个 subscribeToMore 函数。您可以使用此函数来执行后续的订阅,以便向原始查询结果推送更新。
与用于处理分页的常见函数 fetchMore 相似,subscribeToMore 具有相似的结构。主要区别在于,fetchMore 执行一个后续 查询,而 subscribeToMore 执行一个 订阅。
以下是一个示例:从一个标准的 查询 开始,该查询获取给定博客文章的所有现有评论:
const COMMENTS_QUERY: TypedDocumentNode<CommentsForPostQuery,CommentsForPostQueryVariables> = gql`query CommentsForPost($postID: ID!) {post(postID: $postID) {comments {idcontent}}}`;function CommentsPageWithData({ params }: CommentsPageWithDataProps) {const result = useQuery(COMMENTS_QUERY,{ variables: { postID: params.postID } });return <CommentsPage {...result} />;}
const COMMENTS_QUERY = gql`query CommentsForPost($postID: ID!) {post(postID: $postID) {comments {idcontent}}}`;function CommentsPageWithData({ params }) {const result = useQuery(COMMENTS_QUERY,{ variables: { postID: params.postID } });return <CommentsPage {...result} />;}
假设我们希望我们的 GraphQL 服务器在帖子中添加新评论时立即向客户端推送更新。首先,我们需要定义当 COMMENTS_QUERY 返回时 Apollo Client 将执行的订阅。
const COMMENTS_SUBSCRIPTION: TypedDocumentNode<OnCommentAddedSubscription,OnCommentAddedSubscriptionVariables> = gql`subscription OnCommentAdded($postID: ID!) {commentAdded(postID: $postID) {idcontent}}`;
const COMMENTS_SUBSCRIPTION = gql`subscription OnCommentAdded($postID: ID!) {commentAdded(postID: $postID) {idcontent}}`;
接下来,我们将 CommentsPageWithData 函数修改为向它返回的 CommentsPage 组件添加一个 subscribeToNewComments 属性。该属性是一个函数,负责在组件挂载后调用 subscribeToMore。
function CommentsPageWithData({ params }: CommentsPageWithDataProps) {const { subscribeToMore, ...result } = useQuery(COMMENTS_QUERY,{ variables: { postID: params.postID } });return (<CommentsPage{...result}subscribeToNewComments={() =>subscribeToMore({document: COMMENTS_SUBSCRIPTION,variables: { postID: params.postID },updateQuery: (prev, { subscriptionData }) => {if (!subscriptionData.data) return prev;const newFeedItem = subscriptionData.data.commentAdded;return Object.assign({}, prev, {post: {comments: [newFeedItem, ...prev.post.comments]}});}})}/>);}
function CommentsPageWithData({ params }) {const { subscribeToMore, ...result } = useQuery(COMMENTS_QUERY,{ variables: { postID: params.postID } });return (<CommentsPage{...result}subscribeToNewComments={() =>subscribeToMore({document: COMMENTS_SUBSCRIPTION,variables: { postID: params.postID },updateQuery: (prev, { subscriptionData }) => {if (!subscriptionData.data) return prev;const newFeedItem = subscriptionData.data.commentAdded;return Object.assign({}, prev, {post: {comments: [newFeedItem, ...prev.post.comments]}});}})}/>);}
在上面的示例中,我们向 subscribeToMore 传递了三个选项:
document指定了要执行的订阅。variables指定了执行订阅时包含的变量。updateQuery是一个函数,它告诉 Apollo Client 如何将查询当前缓存的查询结果(prev)与我们的 GraphQL 服务器推送的订阅数据(subscriptionData)合并。这个函数的返回值会完全替换当前的查询缓存结果。
最后,我们在 CommentsPage 的定义中告诉组件在挂载时调用 subscribeToNewComments:
export function CommentsPage({ subscribeToNewComments }: CommentsPageProps) {useEffect(() => subscribeToNewComments(), []);return <>...</>}
export function CommentsPage({ subscribeToNewComments }) {useEffect(() => subscribeToNewComments(), []);return <>...</>}
useSubscription API 参考文档
注意:如果你正在使用 React Apollo 的 Subscription 渲染属性组件,下面列出的选项/结果细节仍然有效(选项是组件属性,结果传递到渲染属性函数中)。唯一不同的是还需要一个 subscription 属性(它包含由 gql 解析成 AST 的 GraphQL 订阅文档)。
选项
useSubscription 钩子接受以下选项:
ApolloClientApolloClient 实例。默认情况下,useSubscription / Subscription 使用通过上下文传递下来的客户端,但也可以传递不同的客户端。
DefaultContext你的组件与你的网络接口(Apollo Link)之间的共享上下文。
ErrorPolicy指定用于此操作的 ErrorPolicy。
Record你的组件与你的网络接口(Apollo Link)之间的共享上下文。
FetchPolicy你希望你的组件如何与 Apollo 缓存交互。对于详细信息,请参阅 设置获取策略。
boolean如果为 true,则钩子将不会使组件重新渲染。当你在 onData 和 onError 回调中用逻辑控制组件的渲染时,这很有用。
当钩子已经有 data 时将此设置为 true 会使 data 重置为 undefined。
() => void允许注册一个回调函数,该函数将在useSubscription钩子/Subscription组件完成订阅时被触发。
(options: OnDataOptions<TData>) => any允许注册一个回调函数,该函数将在useSubscription钩子/Subscription组件接收数据时被触发。回调函数的options对象参数包括当前的Apollo Client实例client以及接收到的订阅数据data。
(error: ApolloError) => void允许注册一个回调函数,该函数将在useSubscription钩子/Subscription组件接收错误时被触发。
boolean | ((options: BaseSubscriptionOptions<TData, TVariables>) => boolean)确定当钩子的输入(如subscription或variables)更改时,您的订阅是否应取消订阅并重新订阅。
boolean确定当前订阅是否应被跳过。如果有用,例如,当variables依赖于以前的查询且尚未准备好时。
TVariables一个对象,包含subscription执行所需的所有variables。
() => void⚠️ 已废弃
使用onComplete代替
允许注册一个回调函数,该函数将在useSubscription钩子/Subscription组件完成订阅时被触发。
(options: OnSubscriptionDataOptions<TData>) => any⚠️ 已废弃
使用onData代替
允许注册一个回调函数,该函数将在useSubscription钩子/Subscription组件接收数据时被触发。回调函数的options对象参数包括当前的Apollo Client实例client以及接收到的订阅数据subscriptionData。
结果
调用后,useSubscription钩子返回一个包含以下属性的结果对象:
TData一个对象,包含您的GraphQL订阅的结果。默认为一个空对象。
ApolloError一个运行时错误,具有graphQLErrors和networkError属性。
boolean一个布尔值,指示是否已返回任何初始数据
The older subscriptions-transport-ws library
如果你的服务器使用的是 subscriptions-transport-ws 而不是较新的 graphql-ws 库,你需要在设置连接的方式上进行一些修改:
不要执行
npm install graphql-ws命令:npm install subscriptions-transport-ws不要这样
import { createClient } from 'graphql-ws'导入:import { SubscriptionClient } from 'subscriptions-transport-ws'不要这样
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'导入:import { WebSocketLink } from '@apollo/client/link/ws'传递给
new SubscriptionClient的参数与传递给createClient的参数略有不同:- 传递给 SubscriptionClient 构造函数的第一个参数是订阅服务器的 URL。
- 选项对象
connectionParams被嵌套在一个名为options的选项对象中,而不是位于顶级。你也可以直接将new SubscriptionClient的参数传递给new WebSocketLink。) - 请参阅 subscriptions-transport-ws 的 README 文件获取完整的
SubscriptionClientAPI 文档。
在创建你的 wsLink 后,这篇文章中剩余的内容仍然适用: useSubscription, subscribeToMore 和分叉链接在这两种实现中工作方式完全相同。
以下是一个典型的 WebSocketLink 初始化示例:
import { WebSocketLink } from '@apollo/client/link/ws';import { SubscriptionClient } from 'subscriptions-transport-ws';const wsLink = new WebSocketLink(new SubscriptionClient('ws://:4000/subscriptions', {connectionParams: {authToken: user.authToken,},}),);
import { WebSocketLink } from '@apollo/client/link/ws';import { SubscriptionClient } from 'subscriptions-transport-ws';const wsLink = new WebSocketLink(new SubscriptionClient('ws://:4000/subscriptions', {connectionParams: {authToken: user.authToken,},}),);
有关 WebSocketLink 的更多详细信息,请参阅 其 API 文档。