跳至内容

订阅

除了使用查询获取数据和使用变异修改数据之外,GraphQL 规范还支持第三种操作类型,称为 subscription

GraphQL 订阅是一种将数据从服务器推送到选择监听服务器实时消息的客户端的方式。订阅类似于查询,因为它们指定要传递给客户端的一组字段,但它们不会立即返回单个答案,而是在服务器上发生特定事件时发送结果。

订阅的常见用例是通知客户端特定事件,例如创建新对象、更新字段等等。

概述

GraphQL 订阅必须在模式中定义,就像查询和变异一样

graphql
type Subscription {
  messageAdded(channelId: ID!): Message!
}

在客户端,订阅查询看起来与任何其他类型的操作一样

graphql
subscription onMessageAdded($channelId: ID!) {
  messageAdded(channelId: $channelId) {
    id
    text
  }
}

发送给客户端的响应如下所示

json
{
  "data": {
    "messageAdded": {
      "id": "123",
      "text": "Hello!"
    }
  }
}

在上面的示例中,服务器被写入以在每次为特定频道添加消息时发送新结果。请注意,上面的代码仅定义了模式中的 GraphQL 订阅。阅读 在客户端设置订阅为服务器设置 GraphQL 订阅 以了解如何将订阅添加到您的应用程序。

何时使用订阅

在大多数情况下,间歇性轮询或手动重新获取实际上是使客户端保持最新状态的最佳方式。那么,何时订阅是最佳选择?订阅在以下情况下特别有用

  1. 初始状态很大,但增量更改集很小。可以使用查询获取起始状态,然后通过订阅更新它。
  2. 您关心特定事件的低延迟更新,例如在聊天应用程序中,用户希望在几秒钟内收到新消息。

Apollo 或 GraphQL 的未来版本可能会包含对实时查询的支持,这将是替代轮询的低延迟方式,但在这一点上,GraphQL 中的通用实时查询在某些相对实验性设置之外尚不可用。

客户端设置

在本文中,我们将解释如何在客户端上进行设置,但您还需要服务器实现。您可以 阅读有关如何将订阅与 JavaScript 服务器一起使用,或者如果您使用的是像 Graphcool 这样的 GraphQL 后端即服务,则可以享受开箱即用的订阅设置。

GraphQL 规范没有定义用于发送订阅请求的特定协议。第一个实现 WebSocket 上订阅的流行 JavaScript 库称为 subscriptions-transport-ws。此库不再积极维护。它的继任者是一个名为 graphql-ws 的库。这两个库不使用相同的 WebSocket 子协议,因此您需要确保您的服务器和客户端都使用相同的库。

Apollo Client 支持 graphql-wssubscriptions-transport-ws。Apollo 文档 建议使用较新的库 graphql-ws,但如果您需要,这里解释了如何使用这两个库。

新库:graphql-ws

让我们看看如何使用为最新库 graphql-ws 设置的链接将对该传输的支持添加到 Apollo Client。首先,安装

bash
npm install graphql-ws

然后初始化 GraphQL WebSocket 链接

js
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";

const wsLink = new GraphQLWsLink(
  createClient({
    url: "ws://localhost:4000/graphql",
  })
);

我们需要根据操作类型使用 GraphQLWsLinkHttpLink

js
import { HttpLink, split } from "@apollo/client/core"
import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; // <-- This one uses graphql-ws
import { createClient } from "graphql-ws";
import { getMainDefinition } from "@apollo/client/utilities"

// Create an http link:
const httpLink = new HttpLink({
  uri: "http://localhost:3000/graphql"
})

// Create a GraphQLWsLink link:
const wsLink = new GraphQLWsLink(
  createClient({
    url: "ws://localhost:5000/",
  })
);

// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const link = split(
  // split based on operation type
  ({ query }) => {
    const definition = getMainDefinition(query)
    return (
      definition.kind === "OperationDefinition" &&
      definition.operation === "subscription"
    )
  },
  wsLink,
  httpLink
)

// Create the apollo client with cache implementation.
const apolloClient = new ApolloClient({
  link,
  cache: new InMemoryCache(),
});

apollo 客户端是将提供给 vue 应用程序的客户端,有关更多详细信息,请参阅 设置部分

现在,查询和变异将像往常一样通过 HTTP 进行,但订阅将通过 WebSocket 传输完成。

旧库:subscriptions-transport-ws

如果您需要使用 subscriptions-transport-ws 因为您的服务器仍然使用该协议,那么请安装以下内容,而不是安装 graphql-ws

bash
npm install subscriptions-transport-ws

然后初始化 GraphQL WebSocket 链接

js
import { WebSocketLink } from "@apollo/client/link/ws" // <-- This one uses subscriptions-transport-ws

const wsLink = new WebSocketLink({
  uri: `ws://localhost:5000/`,
  options: {
    reconnect: true
  }
})

其余配置(创建 httpLink 和链接)与上面为 graphql-ws 描述的相同。

useSubscription

将实时数据添加到 UI 的最简单方法是使用 useSubscription 组合函数。这使您可以不断从服务器接收更新以更新 Ref 或反应式对象,从而重新渲染您的组件。需要注意的一点是,订阅只是监听器,它们在首次连接时不会请求任何数据:它们只打开连接以获取新数据。

首先在您的组件中导入 useSubscription

vue
<script>
import { useSubscription } from "@vue/apollo-composable"

export default {
  setup() {
    // Data & Logic here...
  }
}
</script>

然后,我们可以将 GraphQL 文档作为第一个参数传递并检索 result ref

vue
<script>
import { useSubscription } from "@vue/apollo-composable"

export default {
  setup() {
    const { result } = useSubscription(gql`
      subscription onMessageAdded {
        messageAdded {
          id
          text
        }
      }
    `)
  }
}
</script>

然后,我们可以 watch 结果,因为会收到新数据

vue
<script>
import { watch } from "vue"
import { useSubscription } from "@vue/apollo-composable"

export default {
  setup() {
    const { result } = useSubscription(gql`
      subscription onMessageAdded {
        messageAdded {
          id
          text
        }
      }
    `)

    watch(
      result,
      data => {
        console.log("New message received:", data.messageAdded)
      },
      {
        lazy: true // Don't immediately execute handler
      }
    )
  }
}
</script>

例如,我们可以像收到消息一样显示消息列表

vue
<script>
import { watch, ref } from "vue"
import { useSubscription } from "@vue/apollo-composable"

export default {
  setup() {
    const messages = ref([])

    const { result } = useSubscription(gql`
      subscription onMessageAdded {
        messageAdded {
          id
          text
        }
      }
    `)

    watch(
      result,
      data => {
        messages.value.push(data.messageAdded)
      },
      {
        lazy: true // Don't immediately execute handler
      }
    )

    return {
      messages
    }
  }
}
</script>

<template>
  <div>
    <ul>
      <li v-for="message of messages" :key="message.id">
        {{ message.text }}
      </li>
    </ul>
  </div>
</template>

变量

我们可以在第二个参数中传递变量。就像 useQuery 一样,它可以是对象、Ref、反应式对象或将变为反应式的函数。

使用 ref

js
const variables = ref({
  channelId: "abc"
})

const { result } = useSubscription(
  gql`
    subscription onMessageAdded($channelId: ID!) {
      messageAdded(channelId: $channelId) {
        id
        text
      }
    }
  `,
  variables
)

使用反应式对象

js
const variables = reactive({
  channelId: "abc"
})

const { result } = useSubscription(
  gql`
    subscription onMessageAdded($channelId: ID!) {
      messageAdded(channelId: $channelId) {
        id
        text
      }
    }
  `,
  variables
)

使用函数(它将自动变为反应式)

js
const channelId = ref("abc")

const { result } = useSubscription(
  gql`
    subscription onMessageAdded($channelId: ID!) {
      messageAdded(channelId: $channelId) {
        id
        text
      }
    }
  `,
  () => ({
    channelId: channelId.value
  })
)

选项

与变量类似,您可以将选项传递给 useSubscription 的第三个参数

js
const { result } = useSubscription(
  gql`
    subscription onMessageAdded($channelId: ID!) {
      messageAdded(channelId: $channelId) {
        id
        text
      }
    }
  `,
  null,
  {
    fetchPolicy: "no-cache"
  }
)

它也可以是反应式对象,或者一个将自动变为反应式的函数

js
const { result } = useSubscription(
  gql`
    subscription onMessageAdded($channelId: ID!) {
      messageAdded(channelId: $channelId) {
        id
        text
      }
    }
  `,
  null,
  () => ({
    fetchPolicy: "no-cache"
  })
)

有关所有可能的选项,请参阅 API 参考

禁用订阅

您可以使用 enabled 选项禁用和重新启用订阅

js
const enabled = ref(false)

const { result } = useSubscription(
  gql`
  ...
`,
  null,
  () => ({
    enabled: enabled.value
  })
)

function enableSub() {
  enabled.value = true
}

订阅状态

您可以从 useSubscription 中检索加载和错误统计信息

js
const { loading, error } = useSubscription(...)

事件钩子

onResult

当从服务器收到新结果时,会调用此方法

js
const { onResult } = useSubscription(...)

onResult((result, context) => {
  console.log(result.data)
})

onError

当发生错误时会触发此事件。

js
import { logErrorMessages } from '@vue/apollo-util'

const { onError } = useSubscription(...)

onError((error, context) => {
  logErrorMessages(error)
})

更新缓存

使用 onResult,您可以使用新数据更新 Apollo 缓存。

js
const { onResult } = useSubscription(...)

onResult((result, { client }) => {
  const query = {
    query: gql`query getMessages ($channelId: ID!) {
      messages(channelId: $channelId) {
        id
        text
      }
    }`,
    variables: {
      channelId: '123',
    },
  }

  // Read the query
  let data = client.readQuery(query)

  // Update cached data
  data = {
    ...data,
    messages: [...data.messages, result.data.messageAdded],
  }

  // Write back the new result for the query
  client.writeQuery({
    ...query,
    data,
  })
})

subscribeToMore

使用 GraphQL 订阅,您的客户端将在服务器推送时收到通知,您应该选择最适合您的应用程序的模式。

  • 将其用作通知,并在触发时运行您想要的任何逻辑,例如提醒用户或重新获取数据。
  • 使用与通知一起发送的数据,并将其直接合并到存储中(现有查询会自动收到通知)。

使用 subscribeToMore,您可以轻松地做到后者。

subscribeToMore 是一个函数,可用于使用 useQuery 创建的每个查询。它的工作原理与 fetchMore 相似,只是更新函数在每次订阅返回时都会被调用,而不是只调用一次。

让我们从关于 mutations 的部分(稍作修改以包含一个变量)中获取我们之前示例组件中的查询。

vue
<script>
const MESSAGES = gql`
  query getMessages($channelId: ID!) {
    messages(channelId: $channelId) {
      id
      text
    }
  }
`

export default {
  props: ["channelId"],

  setup(props) {
    // Messages list
    const { result } = useQuery(MESSAGES, () => ({
      channelId: props.channelId
    }))
    const messages = computed(() => result.value?.messages ?? [])

    return {
      messages
    }
  }
}
</script>

现在让我们将订阅添加到此查询中。

useQuery 中检索 subscribeToMore 函数。

vue
<script>
const MESSAGES = gql`
  query getMessages($channelId: ID!) {
    messages(channelId: $channelId) {
      id
      text
    }
  }
`

export default {
  props: ["channelId"],

  setup(props) {
    // Messages list
    const { result, subscribeToMore } = useQuery(MESSAGES, () => ({
      channelId: props.channelId
    }))
    const messages = computed(() => result.value?.messages ?? [])

    subscribeToMore()

    return {
      messages
    }
  }
}
</script>

它期望一个对象或一个函数,该函数将自动具有反应性。

js
subscribeToMore({
  // options...
})
js
subscribeToMore(() => ({
  // options...
}))

在后一种情况下,如果选项发生更改,订阅将自动重新启动。

您现在可以放置一个包含相关订阅的 GraphQL 文档,如果需要,还可以包含变量。

vue
<script>
const MESSAGES = gql`
  query getMessages($channelId: ID!) {
    messages(channelId: $channelId) {
      id
      text
    }
  }
`

export default {
  props: ["channelId"],

  setup(props) {
    // Messages list
    const { result, subscribeToMore } = useQuery(MESSAGES, () => ({
      channelId: props.channelId
    }))
    const messages = computed(() => result.value?.messages ?? [])

    subscribeToMore(() => ({
      document: gql`
        subscription onMessageAdded($channelId: ID!) {
          messageAdded(channelId: $channelId) {
            id
            text
          }
        }
      `,
      variables: {
        channelId: props.channelId
      }
    }))

    return {
      messages
    }
  }
}
</script>

现在订阅已添加到查询中,我们需要使用 updateQuery 选项告诉 Apollo Client 如何更新查询结果。

js
subscribeToMore(() => ({
  document: gql`
    subscription onMessageAdded($channelId: ID!) {
      messageAdded(channelId: $channelId) {
        id
        text
      }
    }
  `,
  variables: {
    channelId: props.channelId
  },
  updateQuery: (previousResult, { subscriptionData }) => {
    const tmp = [...previousResult] 
    tmp.messages.push(subscriptionData.data.messageAdded)
    return tmp
  }
}))

WebSocket 上的身份验证

在许多情况下,需要在允许客户端接收订阅结果之前对其进行身份验证。为此,SubscriptionClient 构造函数接受一个 connectionParams 字段,该字段传递一个自定义对象,服务器可以使用该对象在设置任何订阅之前验证连接。

js
import { WebSocketLink } from "@apollo/client/link/ws"

const wsLink = new WebSocketLink({
  uri: `ws://localhost:5000/`,
  options: {
    reconnect: true,
    connectionParams: {
        authToken: user.authToken,
    },
  }
})

提示

您可以将 connectionParams 用于您可能需要的任何其他用途,而不仅仅是身份验证,并在服务器端使用 SubscriptionsServer 检查其有效负载。