既存のReact + Raisアプリに同期機能をAction Cableで追加した

# 概要
* 複数の端末間で予約表同期機能の追加をRailsのAction Cableで実現した
* 流れは、予約表作成/削除 →　サーバーからその予約表に接続している全端末にAcitionCableで情報を配信 → クライアントから予約情報のFetch機能を呼び出す
* Fetch機能が既にあるなら、30分で同期機能が追加できます。
* （websocketの特にSSL通信の本番環境は、それぞれの環境毎固有の設定が必要ですので、今のところdevelopmentまでの動作のみです）

# 環境
* RailsとReactで作られた予約表のアプリが起点です。
* 起点となるアプリについては、[こちらのQiita記事](https://qiita.com/junara/items/ddba402e277afd5d38b2)参照

# ソース（差分）
* https://github.com/junara/qrnote/commit/9ac977f71ed4a998525cfedc30365c1be759be74

# 動作画像

## 予約作成の同期
一番左のブラウザで予約（バナナ）すると他のブラウザで同期されます。
[![https://gyazo.com/d4e67c99344a25b6aa36bdcfb281192c](https://i.gyazo.com/d4e67c99344a25b6aa36bdcfb281192c.gif)](https://gyazo.com/d4e67c99344a25b6aa36bdcfb281192c)

## 予約削除の同期
一番左のブラウザで予約（バナナ）を削除すると他のブラウザでも削除されます。
[![https://gyazo.com/082cee67f4dec63ab0d6bc7e04cf25bc](https://i.gyazo.com/082cee67f4dec63ab0d6bc7e04cf25bc.gif)](https://gyazo.com/082cee67f4dec63ab0d6bc7e04cf25bc)

# 手順
1. （サーバー側）チャネルの追加機能の実装
2. （サーバー側）予約作成時の複数端末への配信機能
3. （クライアント側）チャネル受信機能と同期機能の実装

追加・編集するファイルは下記4つ。

* app/channels/note_channel.rb
* app/controllers/api/v1/reservations_controller.rb
* app/jobs/sync_item_job.rb
* app/javascript/containers/Item.jsx

## （サーバー側）チャネルの追加機能の実装
予約表毎に固有のsubscriptionができるようにチャネルを追加する。

subscribedはクライアントからのNoteChannel購読の依頼があったときに呼ばれるメソッド。

各予約表は**token**で区別されているので、購読するチャネル名に**token**を末尾につける事で固有の購読とした。

また、tokenの存在を`if Item.find_by(token: params[:token]).present?`を確認することで、不特定の購読作成ををはじくようにした。

```app/channels/note_channel.rb
class NoteChannel < ApplicationCable::Channel
  def subscribed
    stream_from "note_for_#{params[:token]}" if Item.find_by(token: params[:token]).present?
  end
end
```

次に、クライアント側に購読機能を実装します。

**{channel: 'NoteChannel', token: token}**で購読するチャネルを指定します。この値は、サーバー側で呼び出したいチャネルに対応します。今回は、サーバーの**NoteChannel**を呼び出したいので、channelに**NoteChannel**を指定し、固有のチャネル名とするために**token**を使うのでtokenに**token**を渡します。ここで渡したパラメータは、サーバー側では``params[パラメータ]``として参照することができます。（例`params[:token]`）


**deleteOldSubscription**購読するチャネルを開いている予約表のみに一つに制限するためのメソッドです。これがないと、開いた複数の予約表全てを購読する状態になってしまいますので不便です。

```app/javascript/containers/Item.jsx
  componentWillMount() {
    const token = this.props.token ? this.props.token : this.props.match.params.token
    this.props.attemptFetchItem(token)
    this.setupSubscription(token) // 予約表（Item）表示時にチャネルの購読を開始
  }

  componentWillUnmount () {
    this.deleteOldSubscription() // 予約表（Item）離脱時にチャネルの購読をやめる
  }

  // 購読チャネルのリセット
  deleteOldSubscription = () => {
    if (App.cable.subscriptions['subscriptions'].length > 0) {
      App.cable.subscriptions['subscriptions'].forEach((subscription) => {
        App.cable.subscriptions.remove(subscription)
      })
    }
  }

  // チャネルの購読
  setupSubscription = (token) => {
    if (!token) return
    this.deleteOldSubscription()
    App.note = App.cable.subscriptions.create({channel: 'NoteChannel', token: token}, {
      connected: () => {console.log('connected')},// チャネルの購読開始時のコールバック（今回は何もしない）
      disconnected: () => {console.log('disconnected')},// チャネルの購読終了時のコールバック（今回は何もしない）
    })
  }
```

## （サーバー側）予約作成時の複数端末への配信機能
予約作成および予約削除に呼応してチャネルへ情報を発信する

チャネルへ情報を発信するJobを追加する。配信先のチャネル名は`note_for_#{item_token}`。これは`app/channels/note_channel.rb`内で作成した`note_for_#{params[:token]}`と同じになるようにする。

`{token: item_token}`はクライアントに配信される情報です。今回使っていませんので、削除してもいいです。

```app/jobs/sync_item_job.rb
class SyncItemJob < ActiveJob::Base
  def perform(item_token)
    ActionCable.server.broadcast("note_for_#{item_token}", {token: item_token})
  end
end
```

予約作成と予約削除時に、上記で作成した発信するためのJob`SyncItemJob.perform_later(item.token)`を呼び出す。

```app/controllers/api/v1/reservations_controller.rb
      def create
        ## 省略
        @reservation.extend_item_expiation_dt
        SyncItemJob.perform_later(item.token) ## 追加
      end

      def destroy
        ## 省略
        reservation.destroy
        SyncItemJob.perform_later(@item_token) ## 追加
      end
```

## （クライアント側）チャネル受信機能と同期機能の実装

チャネル受信時に呼ばれるメソッドは`received`です。ここに予約表をFetchする機能を追加すればOKです。

QRnoteで予約表をFetchする機能は`this.props.attemptFetchItem(token)`で既に実装しています。これをreceived内で呼び出します。

```app/javascript/containers/Item.jsx
  setupSubscription = (token) => {
    // 省略
    App.note = App.cable.subscriptions.create({channel: 'NoteChannel', token: token}, {
      connected: () => {console.log('connected')},
      disconnected: () => {console.log('disconnected')},
      // ここから
      received: () => { // チャネル受信後に呼び出される
        console.log('received!')
        this.props.attemptFetchItem(token)  // 既に実装済の予約表を同期するための機能。各自のアプリでfetchに該当する機能をここに置き換えて下さい。
      },
      // ここまでを追加
    })
  }
```

これで終わりです。

# 所感
Action Cableというとチャットアプリの実装ばかりですよね。でも、チャットアプリって正直あまり作るモチベーションなかったので、あまり調べていませんでした。しかし、今回同期機能の実装のために、ActionCableを使ったら、思いのほか良かった。

むしろ、同期機能こそ、Action Cableつかわれるべきなきがする。チャット機能って、作りこみが面倒だけど、同期機能はactionに呼応したキューを配信するだけなので実装簡単だし。

# 備考
* Rails 5.0以上で作ったプロジェクトを前提としています。もし、4.2から5にアップグレードした場合は、action cableを使えるようにする設定の追加が必要になります。