點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Apollo GraphQL 建立 Subscription

Sam Xiao's Avatar 2019-12-12

傳統若要實現 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 ApolloServergql 外,還需要 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 回傳目前所有資料,共兩筆。

pubsub000

Subscription

subscription {
  newBook {
    title
    price
  }
}

執行 newBook subscription,由於還沒被觸發,因此處在 listening 狀態。

pubsub001

Mutation

mutation {
  addBook(book: {
    title: "Speaking JavaScript"
    price: 300
  }) {
    title
    price
  }
}

執行 addBook mutation,由此 mutation 觸發 newBook subscription。

pubsub002

Subscription

subscription {
  newBook {
    title
    price
  }
}

原本 newBook subscription 由 listening 得到回傳資料。

pubsub003

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