12
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

概要

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

環境

  • RailsとReactで作られた予約表のアプリが起点です。
  • 起点となるアプリについては、こちらのQiita記事参照

ソース(差分)

動作画像

予約作成の同期

一番左のブラウザで予約(バナナ)すると他のブラウザで同期されます。
https://gyazo.com/d4e67c99344a25b6aa36bdcfb281192c

予約削除の同期

一番左のブラウザで予約(バナナ)を削除すると他のブラウザでも削除されます。
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

予約作成と予約削除時に、上記で作成した発信するためのJobSyncItemJob.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を使えるようにする設定の追加が必要になります。
12
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?