訂閱 Subscription
即時更新是現代 web 與行動 app 不可或缺的功能。目前可在網路與行動 app 之間即時傳送資料的技術是 WebSocket。
你可以使用 WebSocket 協定在 TCP 通訊端開啟雙向通訊通道。這意味著網頁與 app 可以透過一個連結(connection)來傳送與接收資料。這項技術可讓你即時且直接從伺服器推送更新到客戶端網頁上。
到目前為止,我們都用 HTTP 協定來實作 GraphQL 查詢與 mutation。HTTP 提供了在用戶端與伺服器之間傳送與接收資料的手法,但它無法協助我們連接伺服器與監聽狀態的改變。在 WebSocket 出現前,監聽伺服器上的狀態改變唯一的方式就是不斷傳送 HTTP 請求給伺服器來確定是不是有所改變,也就是所謂的輪詢 (Long Polling)。
使用訂閱
在 GraphQL,我們要使用訂閱來監聽 API 以得知特定資料的改變。Apollo Server 已經支援訂閱了。它包裝了一些 npm 套件,可用來設定 GraphQL app 的 WebSocket: graphql-subscriptions 與 subscriptions-transport-ws。
graphql-subscriptions 套件是一種 npm 套件,它提供了一種發佈者/訂閱者 (publisher / subscriber) 設計模式的實作,PubSub。PubSub 是發佈資料變動好讓訂閱的用戶端接收的工具。subscriptions-transport-ws 是一種 WebSocket 伺服端與用戶端,可讓你在 WebSocket 上傳送訂閱。Apollo Server 在一開始就會自動加入這兩個套件來支援訂閱。
在預設情況下,Apollo Server 會在 ws://localhost:4000 設定 WebSocket,所以如果你使用簡單的 Apollo Server 組態,那它已經是個支援 WebSocket 組態了。
因為我們使用 apollo-server-express,所以必須執行一些步驟來讓訂閱開始運作。我們必須修改專案根目錄下的 index.js。 首先從 Node.js 原生的 http 模組匯入 createServer 函式:
1 | const { createServer } = require('http'); |
Apollo Server 會自動設定訂閱的支援,但為了執行這項工作,它需要一個 HTTP 伺服器。我們將使用 createServer 來建立一個。在最下端將程式碼改為:
1 | ... |
首先,我們用 Express app 實例來建立一個新的 httpServer。依照目前的 Express 組態,這個 httpServer 已經可以處裡收到的所有 HTTP 請求了。我們也有一個伺服器實例,可以在裡面加入 WebSocket 支援。
server.installSubscriptionHandlers(httpServer) 是讓 WebSocket 運作的程式。這是 Apollo Server 加入必要的處裡程式來支援使用 WebSocket 的所有訂閱的地方。除了 HTTP 伺服器之外,我們的後端現在已經可以接收 ws://localhost:4000/graphql 的請求了。
讓伺服器支援訂閱之後,接下來我們要實作訂閱。
實作訂閱
我們想要知道使用者何時張貼照片,這是一個很好的訂閱使用案例。如同 GraphQL 的其它型態,為了實作訂閱,我們必須從 schema 開始著手。我們要在 schema 子目錄下新增一個程式檔 subscription.js 專們來擺放訂閱,這裡要加入一個 subscription 型態:
1 | const { gql } = require("apollo-server-express"); |
有了新的 schema 定義,我們也需要修改 schema 目錄下的 index.js 將 subscription.js 匯入,並併接起來 :
1 | ... |
我們會在照片被加入時使用 newPhoto subscription 來將資料推送到客戶端。我們可以在客戶端用下列的 GraphQL 查詢語言來傳送訂閱操作:
1 | subscription { |
這個 subscription 會將新照片的資料推送到用戶端。如同 Query 與 Mutation,GraphQL 可讓我們用選擇組來請求關於特定欄位的資料。
當客戶端的 subscription 訂閱被送到伺服器時,連結會保持開啟,它會監聽資料的改變,每當照片被加入時,就會將資料傳給訂閱者。當你用 GraphQL Playground 設置訂閱時,會看到 Play 按鈕變成紅色的 Stop 按鈕。
Stop 按鈕代表訂閱目前是開啟的,並且正在監聽資料。當你按下 Stop 按鈕時,訂閱就會被取消,它會停止監聽資料的改變。
再來我們需要修改 postPhoto mutation,目前這個 mutation 是將新照片加入資料庫。我們想要在這同時將新照片的資料發佈給 subscription:
1 | ... |
這個解析函式第 3 行我們在傳入的 context 中多了一個 pubsub 實例,我們會在稍後來作這件事。pubsub 是一種可以發佈事件並傳送資料給訂閱解析函式的機制。它就像 Node.js EventEmitter。你可以用它來發佈事件,並將資料傳給每一個訂閱某個事件的處裡程式。
在這裡第 15 行,我們在將新照片加入資料庫之後發佈一個 photo-added 事件,並將 insertedPhoto 資料傳給訂閱 photo-added 事件的每一個處裡程式。
接下來加入 Subscription 解析函式,它的用途是訂閱 photo-added 事件。與 schema 一樣,我們在 resolvers 子目錄下新建立一個 subscription.js 程式檔:
1 | module.exports = { |
一樣要將它匯入 resolvers 目錄下的 index.js:
1 | ... |
Subscription 解析函式是根解析函式。你要將它直接加到 Query 與 Mutation 解析函式一樣的物件中。在 Subscription 解析函式中,我們要定義每個欄位的解析函式。因為我們在 schema 裡面定義了 newPhoto 欄位,所以必須確保解析函式裡面有 newPhoto。
與 Query 和 Mutation 不同的是,Subscription 解析函式有一個 subscribe 訂閱方法,與任何其它解析函式一樣,這個 subscribe 方法可接收 parent、args 與 context,我們在這個 subscribe 方法裡面訂閱特定的事件。在本例中,我們使用 pubsub.asyncIterator 來訂閱 photo-added 事件。每當 pubsub 發出 photo-added 事件時,它就會被傳給這個 newPhoto subscription。
postPhoto mutation 解析函式與 newPhoto subscription 解析函式都期望收到 context 中的 pubsub 實例,我們要修改 context 來加入 pubsub 實例。我們要修改專案根目錄下的 index.js,修改後會如下:
1 | const { createServer } = require('http'); |
首先在第 3 行從 apollo-server-express 套件匯入 PubSub 建構式,在第 35 行我們使用這個建構式建立一個 pubsub 實例,再來就要修改 context 函式將 pubsub 加入 context。
在第 13 行我們加入了一個 connection 引數,query 與 mutation 仍然使用 HTTP 協定,當我們傳送這些操作 (operation) 給 GraphQL 伺服器時,HTTP 請求會帶有兩個引數物件 req 與 res,這裡 req 會被送到 context 函式;但是當操作是 Subscription 時沒有 HTTP 請求,所以 req 引數會是 null,在此會改用 WebSocket connection 物件當成引數傳給 context 函式。所以當我們有訂閱時,必須透過 connection 的 context 來傳送授權 (token) 資料,而不是透過 HTTP 請求標頭 (HTTP headers) 的 Authorization。所以同時得修改第 17 行的 token 設定值。
現在可以使用 GraphQL Playground 來試一下訂閱功能了。
1 | subscription { |
開啟另外一個 GraphQL Playground 頁籤執行 postPhoto mutation:
1 | mutation newPhoto($input: PostPhotoInput!) { |
記得在 QUERY VARIABLES 加入 JSON 資料:
1 | { |
每當你執行這個 mutation 時,就會看到新的照片資料被送到訂閱。
接續下一篇 建立 GraphQL API (7)