BLOG ブログ


2020.09.01 TECH

Amplify で開発してみる #4 / Subscriptions を使った API (GraphQL) のリアルタイムデータ連携

こんにちは、tacckです。

シリーズの第四回目です。

Comment Any は、認証機能を実装したことによって複数人で利用することができるようになりました。
しかし、他の人が追加したコメントを見るためにブラウザのリロードが必要な状態です。

そこで今回は、イベントの作成やコメントの情報をリアルタイムに反映させるようにします。

前回までのコードはこちらです。

GitHub 認証機能追加ブランチ

Amplify で開発してみるシリーズ

* Amplify で開発してみる #1 / API (GraphQL) その1 テーブル一つの実装

* Amplify で開発してみる #2 / API (GraphQL) その2 テーブル連携した実装

* Amplify で開発してみる #3 / Authentication と API (GraphQL) を連携した実装

* Amplify で開発してみる #4 / Subscription を使った API (GraphQL) のリアルタイムデータ連携 (今回)

開発

Subscription の導入

Subscription の機能自体は、特に何か設定をしなくても有効になっています。
そのため、実際にコードとして書くだけですぐに使うことができます。

イベントのリアルタイム通知

イベントのリアルタイム通知を実装してみましょう。

まずは、 import の追加です。

import { API, graphqlOperation } from 'aws-amplify'
import { listEvents } from '@/graphql/queries'
import { createEvent, updateEvent } from '@/graphql/mutations'
import { onCreateEvent, onUpdateEvent } from '@/graphql/subscriptions'

4行目で、 Subscription のクエリを import しています。(今回も、色の薄くなった行が変更の必要なところです。)

次に、 Subscription のクエリを実行した結果を保持するための変数を宣言します。

  data: function() {
    return {
      newEventName: '',
      newEventNameDialog: false,
      userName: '',
      events: [],
      onCreateEventSubscription: null,
      onUpdateEventSubscription: null,
    }
  },

そして、 Subscription を受け取った場合の処理の実装です。

  created: async function() {
    this.userName = this.$store.state.user.username

    const items = await API.graphql(graphqlOperation(listEvents)).catch(err =>
      console.error('listEvents', err),
    )
    this.events = items.data.listEvents.items

    this.onCreateEventSubscription = API.graphql(
      graphqlOperation(onCreateEvent),
    ).subscribe({
      next: data => {
        const savedEvent = data.value.data.onCreateEvent
        this.events.push(savedEvent)
      },
    })

    this.onUpdateEventSubscription = API.graphql(
      graphqlOperation(onUpdateEvent),
    ).subscribe({
      next: data => {
        const updatedEvent = data.value.data.onUpdateEvent
        const targetEventIndex = this.events.findIndex(
          item => item.id === updatedEvent.id,
        )
        this.events.splice(targetEventIndex, 1, updatedEvent)
      },
    })
  },

9〜16行目は、イベント作成時の Subscription です。
作成されたイベントの情報を13行目で取り出し、14行目でイベント一覧へ格納しています。

18〜28行目は、イベント更新時の Subscription です。
こちらも、更新したイベントの情報を22行目で取り出しています。
イベント一覧にあるイベントの中のもののうち取り出したイベントとIDが一致するものを、取り出したもので更新しています。

最後に、 Subscription が不要になった時にオブジェクトを破棄する処理を書いておきましょう。

  beforeDestroy: function() {
    if (this.onCreateEventSubscription) {
      this.onCreateEventSubscription.unsubscribe()
      this.onCreateEventSubscription = null
    }

    if (this.onUpdateEventSubscription) {
      this.onUpdateEventSubscription.unsubscribe()
      this.onUpdateEventSubscription = null
    }
  },

これは、ページの遷移などによって画面のコンポーネントが破棄されるときに呼ばれるものです。

それぞれ、イベント作成時・更新時の Subscription があれば、 unsubscribe() を実行して、イベントのリアルタイム通知をオフにしています。

こうすることで、無駄なリアルタイム通知の待ち受けを無くすことができ、ちょっとだけ動作に良い影響を与えることができます。

コメントのリアルタイム通知

次は、コメントのリアルタイム通知の実装です。

基本的な流れは、イベントの場合と同じです。

まずは、 import の追加から。今回は削除の Subscription も受け取るようにします。

import { API, graphqlOperation } from 'aws-amplify'
import { getEvent } from '@/graphql/queries'
import {
  createComment,
  updateComment,
  deleteComment,
} from '@/graphql/mutations'
import {
  onCreateComment,
  onUpdateComment,
  onDeleteComment,
} from '@/graphql/subscriptions'

こちらも同じく、 Subscription のクエリを実行した結果を保持するための変数を宣言します。

  data: function() {
    return {
      event: {},
      userName: '',
      inputComment: '',
      linkUrl: '',
      updatedIds: [],
      comments: [],
      onCreateCommentSubscription: null,
      onUpdateCommentSubscription: null,
      onDeleteCommentSubscription: null,
    }
  },

そして、 Subscription を受け取った場合の処理の実装です。

  created: async function() {
    this.linkUrl = location.href
    this.userName = this.$store.state.user.username

    const item = await API.graphql(
      graphqlOperation(getEvent, { id: this.eventId }),
    ).catch(err => console.error('getEvent', err))
    this.event = item.data.getEvent
    this.comments = this.event.comments.items

    this.onCreateCommentSubscription = API.graphql(
      graphqlOperation(onCreateComment),
    ).subscribe({
      next: data => {
        const savedComment = data.value.data.onCreateComment
        if (!this.isTargetEvent(savedComment.eventId)) {
          return
        }

        const comment = this.getComment(savedComment.id)
        if (comment === null) {
          this.comments.push(savedComment)
        }
      },
    })

    this.onUpdateCommentSubscription = API.graphql(
      graphqlOperation(onUpdateComment),
    ).subscribe({
      next: data => {
        const updatedComment = data.value.data.onUpdateComment
        if (!this.isTargetEvent(updatedComment.eventId)) {
          return
        }

        const comment = this.getComment(updatedComment.id)
        if (comment === null) {
          return
        }
        comment.likes = updatedComment.likes
        comment.updatedAt = updatedComment.updatedAt
      },
    })

    this.onDeleteCommentSubscription = API.graphql(
      graphqlOperation(onDeleteComment),
    ).subscribe({
      next: data => {
        const deletedComment = data.value.data.onDeleteComment
        if (!this.isTargetEvent(deletedComment.eventId)) {
          return
        }

        const deletedCommentIndex = this.getCommentIndex(deletedComment.id)
        if (deletedCommentIndex >= 0) {
          this.comments.splice(deletedCommentIndex, 1)
        }
      },
    })
  },

11〜25行目は、コメント作成時の Subscription です。
作成されたコメントの情報を15行目で取り出し、22行目でコメント一覧へ格納しています。

が、その間に少し処理が入っていますね。

まず、16〜18行目です。
こちらは、現在開いているイベントのコメントかどうか、という判定をしています。
これは、現在のコメント用 Subscription のクエリで、受け取りたいイベントを絞ることができないからです。

もちろん、イベントを限定したコメント用 Subscription のクエリを作ることはできます。ただ、そのためには Schema からの対応が必要なため、今回のシリーズでは扱わないことにします。

次は、20〜21行目です。
ここは受け取ったコメントがすでにコメント一覧に存在するかのチェックを行ない、存在しなければ22行目で一覧へ追加する、という処理です。

コメント機能では、自分の追加したコメント、自分が「イイね」を押して表示順が変わったコメント、の背景色を一時的に変更するアニメーションを Vue.js で実装しています。
そのため、自分の追加したコメントと、Subscription で受け取ったコメントの区別をつけて処理を行なう必要があるのです。

上記の理由で、「コメント一覧に無い = 自分の追加したコメントでは無い」コメントだけを、 Subscription 経由でコメント一覧に追加するようにしています。

27〜43行目は、コメント更新時の Subscription です。
更新されたコメントの情報を31行目で取り出し、40〜41行目で必要な情報だけ更新を行なっています。

こちらも、イベントを限定する処理が入っています。(32〜34行目)
コメント作成時と理由は同じですね。

次の36〜39行目は、すでにコメント一覧に存在するコメントだけを更新対象とする処理です。
そして、存在していたら40〜41行目でコメント一覧に存在していたコメントの likesupdatedAt を取り出したコメントの情報で更新しています。

ここは、 Array に対する splice() を頻発させるのを避けたかったので、こういう実装にしています。 他のところと同じように splice() を使った実装もできるので、気になる方は試してみてください。

45〜59行目は、コメント削除時の Subscription です。
削除されたコメントの情報を49行目で取り出し、56行目でコメント一覧から削除しています。

こちらも、イベントを限定する処理が入っています。(50〜52行目)

54〜55行目でコメント一覧に受け取ったコメントが存在するかをチェックし、存在する場合にだけ56行目で一覧から削除する、となります。

引き続き、 Subscription が不要になった時にオブジェクトを破棄する処理を書いておきましょう。

  beforeDestroy: function() {
    if (this.onCreateCommentSubscription) {
      this.onCreateCommentSubscription.unsubscribe()
      this.onCreateCommentSubscription = null
    }

    if (this.onUpdateCommentSubscription) {
      this.onUpdateCommentSubscription.unsubscribe()
      this.onUpdateCommentSubscription = null
    }

    if (this.onDeleteCommentSubscription) {
      this.onDeleteCommentSubscription.unsubscribe()
      this.onDeleteCommentSubscription = null
    }
  },

書き方自体はイベントの時と同じなので、詳しい説明は割愛します。

    isTargetEvent: function(eventId) {
      return this.event.id === eventId
    },

今回の対象となるイベントのチェックメソッドを追加しているので、こちらを methods: {} の中に追加してください。

さて、これでリアルタイム通知の実現に必要な Subscription の実装はできたのですが、これにあわせて EventDetail.vue の方でメソッド名の抽出や修正といった、簡単なリファクタリングも行なっています。

その辺りの対応については、下記の実装差分を確認してみてください。

GitHub リアルタイム通知機能 実装差分

最終的には、こちらのブランチの状態になっているはずです。うまくできない箇所があれば、下記のファイルとも見比べてみてください。

GitHub リアルタイム通知機能追加ブランチ

うまくできたでしょうか?

うまくいけば、下記の動画のように複数のブラウザ・複数のユーザー間でイベントの操作やコメントの操作がリアルタイムに共有されます。

まとめ

これで、最初に要件としてあげた機能を満たす Comment Any を作成することができました!

社内で使うなど、利用者が限定されているシーンであればこれで十分な機能であるといえるでしょう。

このように、モックをベースとして基本的な機能を実装したプロダクト(いわゆるMVP)を短期間で作成するためには、AWS Amplify というサービスはとても適しています。

一方で、途中で「もう少し掘り下げて対応した方が良い」箇所も出てきました。

シリーズ#3 の "Schema 設定" 節では、コメント操作できるユーザーの制限をつけない状態で実装を進めました。
正しくユーザーの制限を行なうためには、今回取り上げていない Amplify Functions を利用して、悪意のあるユーザーに手の届かないところにロジックを持っていく必要があります。

Amplify で開発してみる #3 / Authentication と API (GraphQL) を連携した実装

また、今回の記事の "コメントのリアルタイム通知" 節では、受け取るコメントのイベントを絞る処理を JavaScript で実装しています。
これをより良くするためには、 Schema の方で新たな Subscription を定義する必要があります。

このように、本物のプロダクトへ育てるためには、セキュリティ面や利用効率面も考慮して Amplify を使いこなしていく必要があります。

とはいえ、そういった面はどういったサービス・フレームワークを使う上でも出てくるものです。

AWS Amplify は、「まず一通り作ってみたい」を最速で叶えることのできるサービスの一つだと思います。
このシリーズを通して、少しでも多くの人に実際に手を動かしてもらえれば、と思います。


tacck
ライター名:tacck
元 技術推進Group Group Leader

現在は株式会社ノースディテールを離れて、
エバンジェリストとして技術啓蒙や勉強会の開催、各種プロジェクトに参画しています。
機会があれば、ノースディテールでのプロジェクトに参加できればと思っています。

言語は問わずに対応しますが、心はPHPer。
フロントエンド・バックエンド・インフラ・スマホアプリなどを、
「垣根を超えて」どう作るか、を考えるのが好きです。

好きなフィギュアスケートの技はスプレッド・イーグル。

主な記事一覧へ

一覧に戻る


LATEST ARTICLE 最新の記事

CATEGORY カテゴリー