LoginSignup
7
6

More than 5 years have passed since last update.

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

Posted at

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

7
6
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
7
6