除了使用查詢獲取數據和使用突變修改數據之外,GraphQL 規範將很快制定第三種操作類型,稱為 subscription(訂閱)。你可以閱讀 GitHub 上的 RFC。雖然規範可能會在最終定稿之前發生細微變化,但你可以現在就使用 Apollo 實現訂閱功能。
GraphQL 訂閱是將數據從服務端推送到選擇從服務端監聽實時消息的客戶端的一種方法。訂閱與查詢類似,因為它們都指定一組傳遞給客戶端的欄位,而不是立即返回單個答案,每次在服務器上發生特定事件時都會發送結果。
訂閱常見的使用場景是向客戶端通知特定事件,例如創建新對象,更新欄位等。
概述
GraphQL 訂閱必須在 Schema 中定義,和查詢和突變一樣:
type Subscription {
commentAdded(repoFullName: String!): Comment
}
在客戶端上,訂閱的查詢語句與其他操作類似:
subscription onCommentAdded($repoFullName: String!){
commentAdded(repoFullName: $repoFullName){
id
content
}
}
客戶端收到的響應如下:
{
"data": {
"commentAdded": {
"id": "123",
"content": "Hello!"
}
}
}
在上面的例子中,每向 GitHunt 上指定代碼倉庫添加一條評論,服務端將發送一個新的響應結果。請注意,上述代碼僅在 Schema 中定義 GraphQL 訂閱。閱讀在客戶端上設置訂閱和設置服務端的 GraphQL 訂閱,瞭解如何為你的應用添加訂閱。
何時使用訂閱
在大多數情況下,保持客戶端更新的最佳方式實際上是使用輪詢或手動調用。那什麼時候訂閱會是更好的選擇呢?通常訂閱在以下情況下特別有用:
初始狀態數據很大,但增量變化集較小。可以使用查詢獲取初始狀態,然後通過訂閱進行更新。
在某些特定的情況下,你比較在意低延遲更新,比如用戶期望能在幾秒鐘內接收新消息的聊天應用。
Apollo 或 GraphQL 的未來版本可能包括對實時查詢的支持,這將是一種低延遲的替代輪詢的方案,但目前而言,GraphQL 中一般的實時查詢只支持一些偏實驗的設置。
客戶端設置
GraphQL 訂閱最流行的傳輸方式是 subscriptions-transport-ws。此包由 Apollo 社區開發維護,但可以與任何客戶端或服務端 GraphQL 實現一起使用。在本文中,我們將介紹如何在客戶端進行設置,但前提是你需要有一個服務端實現。你可以閱讀關於如何使用JavaScript 服務端訂閱,或者如果你使用了 GraphQL 後端服務,比如 Graphcool 或 Scaphold,即可享受開箱即用的訂閱。
我們來看看如何向 Apollo 客戶端添加對這個傳輸方式的支持。
首先,從 npm 安裝subscriptions-transport-ws:
1npm install --save subscriptions-transport-ws
然後,初始化一個 GraphQL 訂閱傳輸在客戶端的實例:
import { SubscriptionClient } from 'subscriptions-transport-ws';
const wsClient = new SubscriptionClient(`ws://localhost:5000/`, {
reconnect: true
});
然後,使用 addGraphQLSubscriptions 函數擴展現有的 Apollo 客戶端網路介面:
import { ApolloClient, createNetworkInterface } from 'react-apollo';
import { SubscriptionClient, addGraphQLSubscriptions } from 'subscriptions-transport-ws';
// 創建一個普通的網路介面:
const networkInterface = createNetworkInterface({
uri: 'http://localhost:3000/graphql'
});
// 使用 WebSocket 擴展網路介面
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient
);
// 最後,使用修改的網路介面創建你的 ApolloClient 實例
const client = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions
});
現在,查詢和突變將按照 HTTP 正常進行,但訂閱將通過 websocket 傳輸完成。
subscribeToMore
使用 GraphQL 訂閱,你的客戶端將從服務器獲取推送提醒,你應該選擇最適合你的應用程式的模式:
將其用作通知,並在其觸發時運行所需的任何邏輯,例如提醒用戶或重新獲取數據
使用與通知一起發送的數據,並將其直接合並到 store(既有查詢會自動通知)
用 subscribeToMore,你可以輕松地實現後者。
subscribeToMore 是一個可用於 react-apollo 的每個查詢結果的函數。它的工作原理就像 fetchMore,不同之處在於每次訂閱結果返回時都會調用更新函數,而不是只調用一次。
這是常規查詢:
import { CommentsPage } from './comments-page.js';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
const COMMENT_QUERY = gql`
query Comment($repoName: String!) {
entry(repoFullName: $repoName) {
comments {
id
content
}
}
}
`;
const withData = graphql(COMMENT_QUERY, {
name: 'comments',
options: ({ params }) => ({
variables: {
repoName: `${params.org}/${params.repoName}`
},
})
});
export const CommentsPageWithData = withData(CommentsPage);
現在,我們來為其添加訂閱。
添加一個名為 subscribeToNewComments 的函數,它將使用 subscribeToMore 訂閱,並使用 updateQuery 獲取的新數據更新查詢 store。
請注意,updateQuery 回調必須返回與初始查詢數據相同結構的對象,否則新數據將不會被合並。這里的新註釋被推入 entry 的 comments 列表中:
const COMMENTS_SUBSCRIPTION = gql`
subscription onCommentAdded($repoFullName: String!){
commentAdded(repoFullName: $repoFullName){
id
content
}
}
`;
const withData = graphql(COMMENT_QUERY, {
name: 'comments',
options: ({ params }) => ({
variables: {
repoName: `${params.org}/${params.repoName}`
},
}),
props: props => {
return {
subscribeToNewComments: params => {
return props.comments.subscribeToMore({
document: COMMENTS_SUBSCRIPTION,
variables: {
repoName: params.repoFullName,
},
updateQuery: (prev, {subscriptionData}) => {
if (!subscriptionData.data) {
return prev;
}
const newFeedItem = subscriptionData.data.commentAdded;
return Object.assign({}, prev, {
entry: {
comments: [newFeedItem, ...prev.entry.comments]
}
});
}
});
}
};
},
});
通過使用訂閱變量調用 subscribeToNewComments 函數來啟動實際訂閱:
export class CommentsPage extends Component {
static propTypes = {
repoFullName: PropTypes.string.isRequired,
subscribeToNewComments: PropTypes.func.isRequired,
}
componentWillMount() {
this.props.subscribeToNewComments({
repoFullName: this.props.repoFullName,
});
}
}
通過 WebSocket 認證
在許多情況下,必須先對客戶端進行認證,然後才能接收訂閱結果。為此,SubscriptionClient 構造函數接受一個 connectionParams 欄位,該欄位傳遞一個自定義對象,在設置任何訂閱之前可以被服務端用來驗證連接。
import {SubscriptionClient} from 'subscriptions-transport-ws';
const wsClient = new SubscriptionClient(`ws://localhost:5000/`, {
reconnect: true,
connectionParams: {
authToken: user.authToken,
},
});
除了身份認證,你還可以使用 connectionParams 設置你可能需要的其他配置選項,並使用 SubscriptionsServer 在服務端檢查其有效性。