RubyonRails5.0.0.1

Rails 5 Action Cable メッセージとルームを紐付ける。

More than 1 year has passed since last update.

 概要

Rails5 Action Cableを使ってリアルタイムなチャットアプリを作るという記事が多いが、常にと言っていいほどメッセージモデルとルームモデルを紐付ける方法が質問されているので、その方法を書く。

Modelの構成

User, Message, Roomモデルがあり、MessageModelはUserModelとRoomModelに紐づいている。

app/models/user.rb
class User < ActiveRecord::Base
  has_many :messages
end
app/models/room.rb
class Room < ActiveRecord::Base
  has_many :messages
end
app/models/message.rb
class Message < ActiveRecord::Base
  # メッセージの保存に成功したらBroadCastJobに投げる
  after_create_commit { BroadCastMessageJob.perform_later self }
  # こうしとくと誰が送ったか識別しやすい
  belongs_to :sent_user, class_name: "User", foreign_key: 'user_id'
  belongs_to :room
end

Controller

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController

  def show
    @room = Room.find(params[:id])
    @messages = @room.messages
  end

end

view

app/views/rooms/show.html.erb
<div id="messages", data-room_id="<%= @room.id %>">
  <%= render @messages %>
</div>
<div class="input">
  <form>
    <label>post</label>
    <input type="text" data-behavior="room_speaker">
  </form>
</div>
app/views/messages/_message.html.erb
<div class="message">
  <p><%= "#{message.sent_user.name}: #{message.content}" %></p>
</div>

Room_idを受け取る

要するにあらかじめrooms_controllerからshow.html.erbにroomの情報(room_id)を渡しておいて、それをjs(room.coffee)で受け取り、その情報に基づいてチャンネル(room_channel.rb)の中でメッセージを作る。

app/assets/javascripts/channels/room.coffee
jQuery(document).on 'turbolinks:load', ->
  # viewの<div id="messages">から受け取る
  messages = $('#messages')
  if $('#messages').length > 0

  # ここやで
  App.room = App.cable.subscriptions.create { channel: "RoomChannel", room_id: messages.data('room_id') },
    received: (data) -> 
      $('#messages').append data['message']

    speak: (message) ->
      @perform 'speak', message:
message

  $(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
    if event.keyCode is 13
      App.room.speak event.target.value
      event.target.value = ''
      event.preventDefault()
app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    # このparams["room_id"]は上のroom.coffeeでsubscriptionを作る際に渡しているのがポイント。
    stream_from "room_channel_#{params['room_id']}"
  end

  def speak(data)
    # ここでRoomとMessageが紐付けられる。
    Message.create(content: data['message'], sent_user: current_user, room: Room.find(params['room_id']))
  end
end

ジョブに投げる

MessageModelの中のafter_create_commitで、メッセージの保存に成功したらjobに飛ばすように指定してる。

app/jobs/broadcast_message_job.rb
class BroadCastMessageJob < ApplicationJob
  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

これで完成。
投稿した内容が保存され、かつリアルタイムで画面に表示されれば完成。

結論

本当にビューの中にroom_id渡してJSで受け取ってゴニョゴニョするしかないんですかね、、
とりあえず私が調べた限りはこれくらいしかなさそうでしたが、、