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

リアルタイムチャットは誰でもつくれる~Action CableでDM機能を作ろう~

はじめに

はじめまして!DMM WEBCAMPでメンターをしているものです。

Railsを学んでいる人は「Action Cableを一度は使ってみたい!」と思いがち。

思うだけじゃだめだ、作るんじゃぁぁぁ!!
と、いうことで。
Action Cableを使ったリアルタイムチャットアプリを作ってみましょう!
これを読めば、あなたもリアルタイムチャットを作れる!!!
初学者でもわかりやすいよう、後で記事へ補足説明を追加します。コードの詳しい説明は今回省いていますのでご了承ください。

そもそもAction Cableってなんじゃ

ざっくり説明すると、Action Cableとは、
RailsにおいてWebSocet(Webにおいて双方向通信を低コストで行うための仕組み)による双方向通信を実現する機能である。LINEとかSlackなんかで、ポンポンポンってメッセージが投稿されるあれです。
(Action Cableについて詳しく知りたい人は、Railsガイドを見てください)

開発環境

Ruby 2.5.7
Rails 5.2.4

本記事で作るアプリケーションのイメージ

特定の人物へメッセージを投稿できる「ダイレクトメッセージ」

ER図

スクリーンショット 2019-12-20 13.06.59.png

作ってみる

今回はChatAppDMというアプリケーション名で作ります。

$ rails new ChatAppDM
$ cd ChatApp

また、ここで、jQueryを使えるようにしておきましょう。

Gemfile
gem 'jquery-rails'
app/assets/javascripts/application.js
...
//
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require jquery ←追加
//= require jquery_ujs ←追加
//= require_tree .
...

$ bundle install

早速作ってみる

ログイン機能(devise)を準備する

まずはログイン機能を作っていこう。今回はgemのdeviseを使うぞ。
スペルミスなどに注意して進めよう。

Gemfile
gem 'devise'

gemを追加し、以下のコマンドでmodelやviewなどを生成しよう。

$ bundle install
$ rails g devise:install
$ rails g devise User name:string
$ rails db:migrate
$ rails g devise:views

nameカラムでログイン、サインアップできるようにしたい、またdeviseを加えたので、以下の記述を追加する。ここら辺はdeviseの知識。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
    before_action :configure_permitted_parameters, if: :devise_controller?

    protected

        def configure_permitted_parameters
            devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :email])
            devise_parameter_sanitizer.permit(:sign_in, keys: [:name])
        end

        def after_sign_in_path_for(resource)
            users_path
        end
end
app/views/devise/registrations/new.html.erb
...
<!-- 追加 -->
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true, autocomplete: "email" %>
  </div>
<!-- 追加 -->
...
app/views/devise/sessions/new.html.erb
...
<!-- 追加 -->
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true, autocomplete: "email" %>
  </div>
<!-- 追加 -->

<!-- 削除 -->
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>
<!-- 削除 -->
...
app/views/layouts/application.html.erb
...
 <body>
    <% if user_signed_in? %>
      <%= current_user.name %>
      <%= current_user.email %>
      <%= link_to "ログアウト", destroy_user_session_path, method: :delete %>
    <% else %>
      <%= link_to "新規登録", new_user_registration_path %>
      <%= link_to "ログイン", new_user_session_path %>
    <% end %>

    <h1>ChatAppDM</h1>
    <%= yield %>
  </body>
...
config/initializers/devise.rb
...
  config.authentication_keys = [:name] nameへ変更
...

ふう、やっとかdeviseの準備が整った。これでログインができるようになっているので、ログインができるか確認してみよう。http://localhost:3000/users/sign_up

スクリーンショット 2019-12-20 14.37.50.png

確認ができたら、次のステップに進むぞ。

モデルの作成

ここで、必要なモデルを作成していく。
roomモデル、entryモデル、direct_messageモデルを作成する。

$ rails g model room name:string
$ rails g model entry user:references room:references
$ rails g model DirectMessage user:references room:references name:string
$ rails db:migrate

モデルの編集

app/models/user.rb
...
has_many :entries
has_many :direct_messages
has_many :rooms, through: :entries
...
app/models/room.rb
...
has_many :entries
has_many :direct_messages
has_many :users, through: :entries
...

コントローラの作成

続いて、必要なコントローラを作成する。
今回作成するコントローラは2つ。アクション名も指定して作成するぞ。

$ rails g controller Users index show
$ rails g controller Rooms show

ルーティングの設定

config/routes.rb
root 'users#index'
devise_for :users
resources :users
resources :rooms

この状態でサーバーを立ち上げ、http://localhost:3000へアクセスしてみる。
うまく表示できたらOK。次へ進む。

Usersコントローラを編集

app/controllers/users_controller.rb
class UsersController < ApplicationController
    before_action :authenticate_user!
  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
    @currentUserEntry = Entry.where(user_id: current_user.id)
    @userEntry=Entry.where(user_id: @user.id)
    unless @user.id == current_user.id
      @currentUserEntry.each do |cu|
        @userEntry.each do |u|
          if cu.room_id == u.room_id then
            @isRoom = true
            @roomId = cu.room_id
          end
        end
      end

      unless @isRoom
        @room = Room.new
        @entry = Entry.new
      end
    end
  end
end

users/index.html.erbの作成

app/views/users/index.html.erb
<h3>ユーザ一覧</h3>
<% @users.each do |user| %>
    <%= link_to "ユーザ名:#{user.name}, user_path(user.id) %>
<% end %>

users/index.html.erbはuserの一覧ページ。
userの詳細画面へ遷移できるようにする。

users/show.html.erbの作成

app/views/users/show.html.erb
<h3>ユーザー詳細</h3>
<h2><%= @user.name %></h2>
<h2><%= @user.email %></h2>

<% unless @user.id == current_user.id %>
  <% if @isRoom == true %>
    <%= link_to "チャットへGo!", room_path(@roomId) %>
  <% else %>
    <%= form_for @room do |f| %>
      <%= fields_for @entry do |e| %>
        <%= e.hidden_field :user_id, :value=> @user.id %>
      <% end %>
      <%= f.submit "チャットを始める" %>
    <% end %>
  <% end %>
<% end %>

Roomsコントローラを編集

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
    before_action :authenticate_user!
  def show
    @room = Room.find(params[:id])
    #present?の戻り値は真偽値。よって、trueの場合、
    if Entry.where(:user_id => current_user.id, :room_id => @room.id).present?
      @direct_messages = @room.direct_messages
      @entries = @room.entries
    else
      redirect_back(fallback_location: root_path)
    end
  end

  def create
    @room = Room.create(:name => "DM")
    @entry1 = Entry.create(:room_id => @room.id, :user_id => current_user.id)
    @entry2 = Entry.create(params.require(:entry).permit(:user_id, :room_id).merge(:room_id => @room.id))
    redirect_to room_path(@room.id)
  end
end

rooms/show.html.erbの作成

app/views/rooms/show.html.erb
<h1><%= @room.name %></h1>

<h4>参加者</h4>
<% @entries.each do |e| %>
  <%= link_to e.user.name, user_path(e.user_id) %>
<% end %>

<h1>Chat room</h1>
<form>
  <label>Say something:</label><br>
  <input type="text" id="chat-input" data-behavior="room_speaker">
</form>

<div id="direct_messages" data-room_id="<%= @room.id %>">
  <%= render @direct_messages %>
</div>

<%= render @direct_messages %>とは
<% render partial: 'direct_messages/direct_message', collection: @direct_messages %>
の略。
この記述をすると、部分テンプレートで繰り返し処理が可能になります。

direct_messageを表示する

部分テンプレートで繰り返し処理したいことを書きます。

app/views/direct_messages/_direct_message.html.erb
<%= direct_message.content %><br>

roomチャンネルの作成

さて、ここからActionCable特有のチャンネルを作成するぞ。
チャンネルの詳しい説明はまた今度にする。
今回はspeakメソッドを持っているroomチャンネルを作成する。

$ rails g channel room speak

上記を実行すると、以下のファイルが生成される。

app/channels/room_channel.rb

app/assets/javascripts/channels/room.coffee

・app/channels/room_channel.rbとは......
サーバー側の記述

・app/assets/javascripts/channels/room.coffeeとは......
クライアント側の記述

app/assets/javascripts/channels/room.coffee
document.addEventListener 'turbolinks:load', ->
  App.room = App.cable.subscriptions.create { channel: "RoomChannel", room: $('#direct_messages').data('room_id') },
    connected: ->

    disconnected: ->

    received: (data) ->
      $('#direct_messages').append data['direct_message']

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

  $('#chat-input').on 'keypress', (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
    # stream_from "some_channel"
    stream_from "room_channel_#{params['room']}"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    DirectMessage.create! message: data['direct_message'], user_id: current_user.id, room_id: params['room']
  end
end

jobの作成

$ rails g iob DirectMessageBroadcast 

jobを編集する

app/jobs/direct_message_broadcast_job.rb
class DirectMessageBroadcastJob < ApplicationJob
  queue_as :default

 def perform(direct_message)
    ActionCable.server.broadcast "room_channel_#{direct_message.room_id}", direct_message: render_direct_message(direct_message)
  end

  private

    def render_direct_message(direct_message)
      ApplicationController.renderer.render partial: 'direct_messages/direct_message', locals: { direct_message: direct_message }
    end
end
app/models/direct_message.rb
...
after_create_commit { DirectMessageBroadcastJob.perform_later self }
...

current_userの情報を取得する

app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected
    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

動作確認

動作確認をしてみましょう。

終わりに

うまく作れたでしょうか?
僕が初めて実装した時は、おぉぉぉぉ!!!!!
と感動しました。
なお、初学者でもわかりやすいよう、後で補足説明を追記します。

誰もがActionCableを使いこなせるように記事を更新していきたいと思います。

参考記事

RailsでDM(ダイレクトメッセージ)を送れるようにしよう
Rails 5 + ActionCableで作る!シンプルなチャットアプリ(DHH氏のデモ動画より)
【Rails6.0】ActionCableとDeviseの欲張りセットでリアルタイムチャット作成(改正版)

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
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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
ユーザーは見つかりませんでした