Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

初めに

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

sibakenY
大学卒業後Ruby, Ruby on Railsを勉強しています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away