概要
- 複数の端末間で予約表同期機能の追加をRailsのAction Cableで実現した
- 流れは、予約表作成/削除 → サーバーからその予約表に接続している全端末にAcitionCableで情報を配信 → クライアントから予約情報のFetch機能を呼び出す
- Fetch機能が既にあるなら、30分で同期機能が追加できます。
- (websocketの特にSSL通信の本番環境は、それぞれの環境毎固有の設定が必要ですので、今のところdevelopmentまでの動作のみです)
環境
- RailsとReactで作られた予約表のアプリが起点です。
- 起点となるアプリについては、こちらのQiita記事参照
ソース(差分)
動作画像
予約作成の同期
一番左のブラウザで予約(バナナ)すると他のブラウザで同期されます。
予約削除の同期
一番左のブラウザで予約(バナナ)を削除すると他のブラウザでも削除されます。
手順
- (サーバー側)チャネルの追加機能の実装
- (サーバー側)予約作成時の複数端末への配信機能
- (クライアント側)チャネル受信機能と同期機能の実装
追加・編集するファイルは下記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?
を確認することで、不特定の購読作成ををはじくようにした。
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購読するチャネルを開いている予約表のみに一つに制限するためのメソッドです。これがないと、開いた複数の予約表全てを購読する状態になってしまいますので不便です。
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}
はクライアントに配信される情報です。今回使っていませんので、削除してもいいです。
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)
を呼び出す。
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内で呼び出します。
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を使えるようにする設定の追加が必要になります。