DHH氏のRails 5: Action Cable demoを見つつチャットルーム機能を付けようと思ったけどTurbolinksとのからみでさまよったのでメモ。
- Turbolinksはページ遷移するときにAjaxでページを取得してDOMの差分を書き換える
- ページを遷移してもDOMが書きかわるだけなのでActionCableのWebSocketの接続は生きたまま
- なので、チャットルームのように部屋ごとにActionCableのsubscriptionsを管理したい場合は、TurboLinksの独自イベントをハンドリングして下記の処理が必要
- ページを書き換える前に購読を解除
- ページを書き換えた後に購読を開始
- (もちろん、他のやり方(pub/subを分けるのでなく、1つにしてユーザ管理+ユニキャストっぽいやり方とかで)も良いと思います。)
app/assets/javascripts/channels/room.coffee
App.room = null
current_user_id = ->
$('#user').data('id')
current_room_id = ->
$('#room').data('id')
current_room_ch = ->
id = current_room_id()
if id?
return {channel: 'RoomChannel', room_id: id}
else
return null
# リンクのクリックによりAjaxリクエストが呼ばれたら購読を解除
document.addEventListener 'turbolinks:request-start', ->
if current_room_ch()?
App.room.unsubscribe()
# ページのロードが終わったら購読開始
document.addEventListener 'turbolinks:load', ->
if current_room_ch()?
App.room = App.cable.subscriptions.create current_room_ch() ,
received: (data) ->
$('#messages').append data['message']
speak: (user_id, room_id, content) ->
@perform 'speak', {
user_id: user_id
room_id: room_id
content: content
}
$(document).on 'keypress', '#text', (event) ->
if event.keyCode is 13
value = event.target.value
if value.length != 0
App.room.speak(current_user_id(), current_room_id(), value)
event.target.value = ''
event.preventDefault()
$(document).on 'click', '#button', (event) ->
value = $('#text').val()
if value.length != 0
$('#text').val('')
App.room.speak(current_user_id(), current_room_id(), value)
- チャットのルームIDごとにpub/subの名前を変える。
- クライアントのJavaScriptでApp.room.speakが呼ばれたらサーバのRoomChannel#speakをRPCされる。
- RPCされるとMessageを新規保存する。
app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params[:room_id]}"
end
def unsubscribed
end
def speak(data)
message = {
user_id: data['user_id'],
room_id: data['room_id'],
content: data['content']
}
Message.create! message
end
end
- モデルはDHHの例と同じ。roomとuserがくっついてるくらい。
- 保存できたらブロードキャストジョブに投げる。
app/models/message.rb
class Message < ApplicationRecord
belongs_to :room
belongs_to :user
after_create_commit { MessageBroadcastJob.perform_later self }
end
- メッセージと関連づいているチャットルームIDからさっきのpub/subの名前を指定してブロードキャスト。
- これでチャットルームにいるクライアントだけにメッセージがプッシュされる。
app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast "room_channel_#{message.room_id}", message: render_message(message)
end
private
def render_message(message)
ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
end
end