はじめに
ポートフォリオでActionCableでチームチャット機能を作ったので、アウトプットする。
参考記事
以下の記事を参考にしました。ありがとうございました。
・ Rails 5 + ActionCableで作る!シンプルなチャットアプリ(DHH氏のデモ動画より)
・ 【Rails6.0】ActionCableとDeviseの欲張りセットでリアルタイムチャット作成(改正版)
コード
参考記事にあるコードに手を加えて、チーム毎にチャットを送るようにする。
今回は、teamコントローラーにchatアクションを作ってチャットを送れるようにする。
Action Cable以外の部分をMVC構造に従って、作成する。
model
モデルは、チームモデル、ユーザーモデルの多対多の関係を記述する。
また、チャットの中身であるメッセージモデルは、ユーザーとチームの中間テーブルのモデルとして作成する。
class Team < ApplicationRecord #チームモデル
belongs_to :owner, class_name: 'User', foreign_key: :owner_id
has_many :team_assigns, dependent: :destroy
has_many :members, through: :team_assigns, source: :user
has_many :messages, dependent: :destroy
end
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
has_one_attached :icon #Active Storageでユーザーアイコンをつける
has_many :team_assigns, dependent: :destroy
has_many :teams, through: :team_assigns, source: :team
has_one :owner_team, class_name: 'Team', foreign_key: :owner_id, dependent: :destroy
has_many :messages, dependent: :destroy
end
class TeamAssign < ApplicationRecord
belongs_to :team
belongs_to :user
end
class Message < ApplicationRecord
belongs_to :user
belongs_to :team
end
controller
コントローラーは、チーム内にチャットアクションを作成した。
リンクからチームIDを受け取って、Teamクラスから検索、そのチームが持つチャットを取得してviewに渡す。
def chat
@team = Team.find(params[:id])
messages = @team.messages
end
view
bootstrapを適用したので、container、alertなどを使っている。基本的には受け取ったチャットをメッセージパーシャルをレンダリングすることで画面に表示している。また、チャットを送るためのフォームを設置している。
<h1 id="chat" data-team_id="<%= @team.id %>">
<%= @team.name %>チャットルーム
</h1>
<div class="container">
<% if @team.members.count == 1 %>
<div class="alert alert-danger" role="alert">
チャットするチームメンバーがいません。チーム画面からメンバーを追加して下さい
</div>
<% end %>
<form class="form-group">
<label>チャット:</label>
<input type="text" data-behavior="team_speaker", placeholder="内容", class="form-control">
</form>
<div id="messages_<%= @team.id %>">
<ul class="list-group">
<%= render @messages %>
</ul>
</div>
</div>
<div class="message">
<li class="list-group-item">
<div class="row">
<div class="col-md-2">
<% if message.user.icon.attached? %>
<%= image_tag message.user.icon.variant(resize:'50x50').processed %>
<% end %>
<p><%= message.user.name %></p>
</div>
<div class="col-md-10">
<small class="text-muted"><%= l message.created_at, format: :short %></small><br>
<%= message.content %>
</div>
</div>
</li>
</div>
ここから、Action Cableの実装をする。
まず、チームチャンネルを作成する。
チャンネル用にディレクトリとファイルが作成される。
$ rails g channel team
coffeescript
まず、ブラウザからサーバーを監視する設定を記述していく。
jQueryを活用して、viewからチームIDを取得して、購読するチャンネルを作成する。
App.team = App.cable.subscriptions.create {
channel: "TeamChannel",
team_id: $("#chat").data("team_id")},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
# Channel側のspeakメソッドで受け取ったチャットを受け取る
$('#messages_'+data['team_id']).prepend data['message']
speak: (message) ->
team_id = $('#chat').data('team_id')
@perform 'speak', message: message, team_id: team_id
$(document).on 'keypress', '[data-behavior~=team_speaker]', (event) ->
if event.keyCode is 13 # return = send
App.team.speak event.target.value
event.target.value = ''
event.preventDefault()
channel.rb
次に、サーバーからブラウザを監視する設定を記述していく。
ブラウザの@perform speakによって、サーバー側のspeakアクションが呼び出される。
speakアクションは、ブラウザから受け取った情報を元に、メッセージクラスのインスタンスを作成する。
class TeamChannel < ApplicationCable::Channel
def subscribed
stream_from "team_channel_#{params['team_id']}" #どのチームを監視しているのかを示す
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
message = Message.create!(
content: data['message'],
team_id: data['team_id'],
user_id: current_user.id
)
end
end
チャンネル内でcurrent_userを使っているが、そのためには次のコードを追記する必要がある。
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
verified_user = User.find_by(id: env['warden'].user.id)
return reject_unauthorized_connection unless verified_user
verified_user
end
end
end
Messageモデル
Messageモデルにインスタンスが作成された際(コミット後)にjobが走るようにします。
class Message < ApplicationRecord
belongs_to :user
belongs_to :team
after_create_commit { MessageBroadcastJob.perform_later self } #追記
end
job
jobから、作成されたメッセージをブラウザへ送る設定を記述します。
まず、railsコマンドからMessageBroadcastJobを作成します。
$ rails g job MessageBroadcast
ActiveStorageを使っている場合、ApplicationController.rendererのhttp_hostを指定しないと、画像の参照先がエラーになる。http://ではなく、https://から始まるURLを本番で使う場合には、https: trueを指定する必要がある。
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast(
"team_channel_#{message.team_id}",
message: render_message(message),
team_id: message.team_id
)
end
private
def render_message(message)
renderer = ApplicationController.renderer.new(
http_host: 'http_host番号', #ローカルだと、locallhost:3000。本番環境によって変更
#https: true https://から始まるURLの場合、このコードを追記する必要あり
)
renderer.render(partial: 'messages/message', locals: { message: message })
end
end
このjobによって、coffeescriptのreceiveにメッセージのパーシャルが渡されてリアルタイムで反映されるという仕組みになっている。
まとめ
ActionCableは複雑だと思ったが、参考文献によってデータの受け渡しを追うことができ、理解することができた。この経験から、複雑に見えても、データの流れを1つずつ追うことが大事で、サーバー側だったらbinding.pry、ブラウザ側だったらdebuggerを活用するのが一番の近道だと改めて感じた。