傳統若要實現 Browser 不用 Refresh 即時更新,會使用 WebSocket 實作,但 GraphQL Subscription 已經整合 WebSocket,只要使用類似 Query 語法,就能完成相同效果。
Version
macOS Catalina 10.15.2
WebStorm 2019.2.3
Node 13.2.0
Apollo GraphQL 2.9.6
Apollo GraphQL
src/index.js
import { ApolloServer, gql, PubSub } from 'apollo-server'
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
]
let pubsub = new PubSub()
let typeDefs = gql`
type Query {
books: [Book]
}
type Mutation {
addBook(book: BookInput!): Book
}
type Subscription {
newBook: Book
}
type Book {
title: String
price: Int
}
input BookInput {
title: String!
price: Int!
}
`
let books = () => data
let addBook = (_, { book }, { pubsub }) => {
data.push(book)
pubsub.publish('bookAdded', { newBook: book })
return book
}
let newBook = {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator('bookAdded')
}
let resolvers = {
Query: {
books
},
Mutation: {
addBook
},
Subscription: {
newBook
}
}
let context = () => ({ pubsub })
new ApolloServer({ typeDefs, resolvers, context })
.listen()
.then(({ url }) => `GraphQL Server ready at ${ url }`)
.then(console.log)
第 1 行
import { ApolloServer, gql, PubSub } from 'apollo-server'
若要使用 subscription,除了 import ApolloServer
與 gql
外,還需要 PubSub
。
PubSub
用來發布 event 與訂閱 event。
第 3 行
let data = [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 }
]
原始只有 2 筆資料,第 3 筆將由 mutation 新增並通知 subscription。
第 8 行
let pubsub = new PubSub()
PubSub
為 factory class,實作了 PubSubEngine
interface,稍後會以 publish()
發布 event。
11 行
type Query {
books: [Book]
}
type Mutation {
addBook(book: BookInput!): Book
}
type Subscription {
newBook: Book
}
type Book {
title: String
price: Int
}
input BookInput {
title: String!
price: Int!
}
在 schema 宣告 books
query、addBook
mutation 與 newBook
subscription,值得注意的是 subscription 宣告方式與 query 與 mutation 並無差異。
51 行
let resolvers = {
Query: {
books
},
Mutation: {
addBook
},
Subscription: {
newBook
}
}
在 resolvers
宣告 books
query、addBook
mutation 與 newBook
subscription,值得注意的是 subscription 宣告方式與 query 與 mutation 並無差異。
34 行
let books = () => data
實踐 books
query,目前只是回傳所有資料。
36 行
let addBook = (_, { book }, { pubsub }) => {
data.push(book)
pubsub.publish('bookAdded', { newBook: book })
return book
}
實踐 addBook
mutation,重點在 mutation 內使用 pubsub.publish()
發布 bookAdded
event。
pubsub
直接從 context
argument 解構,稍後會建立 context。
publish()
的第一個 argument 為 event 名稱。
第二個 argument 為要發到 event 的資料,newBook
為 subscription 名稱,book
為要傳給 subscription 的資料。
44 行
let newBook = {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator('bookAdded')
}
實踐 newBook
subscription,實作 subscribe()
時由 context
argument 直接解構出 pubsub
,再由 pubsub.asyncIterator()
訂閱 bookAdded
event。
60 行
let context = () => ({ pubsub })
new ApolloServer({ typeDefs, resolvers, context })
.listen()
.then(({ url }) => `GraphQL Server ready at ${ url }`)
.then(console.log)
由於各 resolver 都要使用 pubsub
,因此特別將 pubsub
放到 context 內。
在 context()
內回傳包含 pubsub
的 object,然後再 new ApolloServer
時傳入。
GraphQL Playground
Query
query {
books {
title
price
}
}
執行 books
query 回傳目前所有資料,共兩筆。
Subscription
subscription {
newBook {
title
price
}
}
執行 newBook
subscription,由於還沒被觸發,因此處在 listening 狀態。
Mutation
mutation {
addBook(book: {
title: "Speaking JavaScript"
price: 300
}) {
title
price
}
}
執行 addBook
mutation,由此 mutation 觸發 newBook
subscription。
Subscription
subscription {
newBook {
title
price
}
}
原本 newBook
subscription 由 listening 得到回傳資料。
Conclusion
- Client 端使用 subscription 後,會由其他 client 執行 mutation,由該 mutation 觸發 subscription 通知已訂閱的 client
- 實務上 mutation 會由其他 browser 執行, 因此在 GraphQL Playground 會另開一個 tab 執行 mutation
- Subscription 會在 resolver 內使用
pubsub
,因此特別適合將pubsub
放在 context 內讓多個 resolver 共用 - Subscription 底層使用了 WebSocket,但在使用上完全感覺不到 WebSocket 存在
Sample Code
完整範例可在我的 GitHub 上找到
Reference
Apollo Docs, Subscriptions
Apollo Docs, GraphQL Subscriptions Guide
Alex Banks & Eve Porcello, Learning GraphQL