订阅
从您的 GraphQL 服务器获取实时更新
除了查询和变更之外,GraphQL还支持第三种操作类型:订阅。
像查询一样,订阅允许您获取数据。不同之处在于,订阅是长期存在的操作,其结果会随时间变化。它们可以保持与您的GraphQL 服务器的活跃连接(通常是通过 WebSocket),允许服务器向订阅的结果推送更新。
订阅对于实时通知客户端后端数据的更改非常有用,例如新对象的创建或重要字段的更新。
何时使用订阅
在大多数情况下,您的客户端应不要使用订阅来保持与后端的同步。相反,您应通过查询间歇性地轮询,或者在用户执行相关操作(如点击按钮)时重新执行查询。
您应使用订阅进行以下操作:
对大型对象进行少量增量更改。重复轮询大型对象是非常昂贵的,尤其是当对象的大部分字段很少更改时。相反,您可以使用查询获取对象的初始状态,并且您的服务器可以主动推送到单个字段在发生时的更新。
低延迟、实时更新。例如,聊天应用程序的客户端希望在新消息可用时立即接收。
注意: 订阅不能用于监听本地客户端事件,如订阅缓存中的更改。订阅旨在用于订阅外部数据更改,并存储这些接收到的更改。然后您可以使用Apollo客户端的观察模型来监视缓存中的更改,使用client.watchQuery或useQuery来监视。
支持的订阅协议
The GraphQL规范没有定义用于发送订阅请求的特定协议。Apollo Client支持以下协议进行订阅:
- WebSocket使用以下子协议之一:
graphql-ws
subscriptions-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://127.0.0.1:4000/subscriptions',}),);
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';import { createClient } from 'graphql-ws';const wsLink = new GraphQLWsLink(createClient({url: 'ws://127.0.0.1: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://127.0.0.1:4000/graphql',});const wsLink = new GraphQLWsLink(createClient({url: 'ws://127.0.0.1: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://127.0.0.1:4000/graphql',});const wsLink = new GraphQLWsLink(createClient({url: 'ws://127.0.0.1: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://127.0.0.1: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://127.0.0.1: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
钩子接受以下选项:
ApolloClient
ApolloClient
实例。默认情况下,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 文件获取完整的
SubscriptionClient
API 文档。
在创建你的 wsLink
后,这篇文章中剩余的内容仍然适用: useSubscription
, subscribeToMore
和分叉链接在这两种实现中工作方式完全相同。
以下是一个典型的 WebSocketLink
初始化示例:
import { WebSocketLink } from '@apollo/client/link/ws';import { SubscriptionClient } from 'subscriptions-transport-ws';const wsLink = new WebSocketLink(new SubscriptionClient('ws://127.0.0.1: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://127.0.0.1:4000/subscriptions', {connectionParams: {authToken: user.authToken,},}),);
有关 WebSocketLink
的更多详细信息,请参阅 其 API 文档。