Posted at

Turbolinks利用時のActionCableのpub/sub管理

More than 3 years have passed since last update.

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