Edited at

ActionCableでリアルタイムDMを実装する


初めに

Ajaxとか勉強していたらActionCableという自分以外の人にもリアルタイムで投稿を表示することができる機能があることを知ったので勉強してみた。

正直中身に関してはいまいちよくわかってないのでいろいろ間違っているかもしれませんがその時は教えてください…


とりあえず

とりあえずrails newしてmodelとcontrollerをつくる

rails new action_dm

rails g model user name:string
rails g model room name:string
rails g model comment body:text user_id:integer room_id:integer

rails db:migrate

rails g controller users index
rails g controller rooms show
rails g controller sessions
rails g controller comments

gem "jquery-rails'

bundle install

してそのあと

assets/javascripts/application.jsに

//= require jquery



//= require rails-ujs

の上に書く

これでOK

userは名前だけ持っていてログインも名前で行う。

roomはダイレクトメッセージを行うところですべてのユーザー間にroomが作られるようにする

コメントを表示する際に名前も表示したいのでcommentにuser_idをいれた


モデルにアソシエーションを書いとく


comment.rb

belongs_to :room



room.rb

has_many :comments



user.rb

has_many :comments


userとroomはたくさんのcommentを持つのでhas_manyした

commentはとりあえずルームに紐づけていればよい


User作成できるようにする

userを作成してログインできるようにcontrollerとviewsをいじっていく

まずusers_controller


users_controller.rb

class UsersController < ApplicationController

before_action :set_current_user

def index
@user = User.new
@rooms = Room.all
@users = User.all
end

def create
@user = User.create(user_params)
if @user.save
redirect_to users_path
else
render :new
end
end

private

def user_params
params.require(:user).permit(:name)
end
end


indexには@user,@rooms,@usersがあってuser作成やroomへのリンクuser一覧、ログインすべてindex.html.erbで行うため必要。

before_actionのset_current_userはapplication_controller.rbに書いてある

application_controller.rbはこんな感じ


application_controller.rb

class ApplicationController < ActionController::Base

def set_current_user
if session[:user_id]
@current_user ||= User.find session[:user_id]
end
end

helper_method :set_current_user
end


@current_userがあれば@current_userを代入してなければUser.find session[:user_id]で見つかったuserを@current_userに代入する。

次にviewをつくる

index.html.erbはこんな感じ


index.html.erb


<p>ユーザー一覧</p>

<% @users.each do |user| %>
<%= user.name %>
<% end %>

<p>現在のアカ</p>
<%= @current_user&.name %>

<p>アカ作成</p>
<%= form_with model: @user, local: true do |f| %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>

<p>ログイン</p>
<%= form_with url: sessions_new_path do |f| %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>

<p>ログアウト</p>
<%= link_to "ログアウト", "sessions/destroy", method: :delete %>

<p>DM</p>

<% @users.each do |user| %>
<% unless user.id == session[:user_id] %>
<%= link_to "#{user.name}", "/rooms/create/#{user.id}", method: :post %>
<% end %>
<% end %>


現在のアカに関しては@current_userがnilの時に@current_user.nameがエラーを出すので&.を使っています。これを使うと@current_userがnilでもエラーが出ない

cssに

p {

border-bottom: 1px solid gray;
}

と書いておく

とりあえずこれでユーザーを作成しログインすることができるようになった


roomを作る

このroomがDMを行う場所

まずrooms/show.html.erbを


show.html.erb

<h1>DM</h1>

<h2>comments</h2>

<div id="comments">
<%= render @room.comments %>
</div>

<%= render 'comments/new', room: @room %>


この<%= render @room.comments %>に関していまいちよくわかってないが


comment

<% @room.comments.each do |comment| %>

<%= render 'comments/comment', comment: comment %>
<% end %>

の省略した書き方?だと思う

comments/_comment.html.erbは


_comment.html.erb

<p><%= username(comment.user_id) %>:<%= comment.body %> -- <%= comment.created_at.to_s(:long) %></p>


コメントを作成するための _new.html.erbは


_new.html.erb

<%= form_for([ @room, Comment.new ], remote: true) do |form| %>

Your comment:<br>
<%= form.text_field :body, size: '50x20' %>
<%= form.submit %>
<% end %>

次にrooms_controller.rb


rooms_controller.rb

class RoomsController < ApplicationController

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

def create
@r = find_room(params[:id])
if @r
redirect_to "/rooms/#{@r.id}}"
else
@room = Room.new(name: "room@#{params[:id]}@#{session[:user_id]}")
@room.save
redirect_to "/rooms/#{@room.id}"
end
end

private

def protect_room(room_id)
@room = Room.find room_id
id = @room.name.split("@")
unless id.find { |i| i.to_i == session[:user_id].to_i }
redirect_to users_path
end
end

def find_room(id)
@rooma = Room.find_by(name: "room@#{id}@#{session[:user_id]}")
@roomb = Room.find_by(name: "room@#{session[:user_id]}@#{id}")
@r = @rooma || @roomb
return @r
end

end


まずroomは最初から作成されているわけではなくメッセージを送るためroomに入ろうとしたときに作成されるようにした。

例えばAとBがいた場合にroomがない状態でAがBにメッセージを送るためroomに入ろうとするとroomが"room@(Bのid)@(Aのid)"という形でroomが作られる。

BがAにメッセージを送ろうとしたときにまたroomが作成されないようにするためにfind_roomという確認メソッドを作った

  def find_room(id)

@rooma = Room.find_by(name: "room@#{id}@#{session[:user_id]}")
@roomb = Room.find_by(name: "room@#{session[:user_id]}@#{id}")
@r = @rooma || @roomb
return @r
end

roomの名前

というのを作った

お互いの間にroomがない場合はnilを返す感じ

roomに他人を入れないようにするために

protect_room(room_id)メソッドを作った

  def protect_room(room_id)

@room = Room.find room_id
id = @room.name.split("@")
unless id.find { |i| i.to_i == session[:user_id].to_i }
redirect_to users_path
end
end

名前にお互いのidが記入されているのでそれを取得してsession[:user_id:と比較している

ここまででuser作成とroom作成はおわり


comment作成

action cableを使うのはこのcommentでcommentを作成したときに自動的にお互いに反映されるようにする

まずchannelを作る

rails g channel comments

このあと編集するのは二つのファイルで一つがサーバー用でもう一つがクライアントサイド用(多分..)

/assets/javascript/comments.coffeeがクライアントサイド用で

こんな感じにする


channels/comments.coffee

App.comments = App.cable.subscriptions.create "CommentsChannel",

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) ->
$('#comments').append data.comment
# Called when there's incoming data on the websocket for this channel


receivedでデータを受け取ったときに$('#comments').append data.commentが行われる。

サーバーサイドのファイルが/channels/comments_channel.rbにあり


comments_channel.rb

class CommentsChannel < ApplicationCable::Channel

def self.broadcast(comment)
broadcast_to comment.room, comment: CommentsController.render(partial: 'comments/comment', locals: { comment: comment })
end

def subscribed
Room.all.each do |room|
stream_for room
end
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end


subscribedでaction cableを使うものを指定している。

なので例えばstream_for Room.last とするそれだけaction cableを使える

stream_forとstream_fromの違いとしてはモデルに関連するストリームを作成する場合はstream_forでそうではない場合はstream_fromだそう

stream_forの場合CommentsChannel.broadcast_to(@room,@comment)と書けばブロードキャスト?できるそう

今回の場合はcomments#createでCommentsChannel.broadcast(comment)が行われている

さいごにrouteはこんな感じ


routes.rb

Rails.application.routes.draw do

resources :users
resources :rooms, only: [:show] do
resources :comments
end
post 'sessions/new'
delete 'sessions/destroy'

post "rooms/create/:id", to: "rooms#create"
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end


これで

room.png

room2.png

みたいな感じで使える。


おわりに

正直なんで動いでいるのか全く分らない..

なにか間違っているところがあればご指摘いただけるとありがたいです。

だれもいらないだろうけどGitHubも上げておきます。

https://github.com/Sibakeny/action_dm


参考文献

Rails Tour

https://www.youtube.com/watch?v=OaDhY_y8WTo&t=1089s

RailsGuides

https://railsguides.jp/action_cable_overview.html