くるみ
- Railsで非同期なチャット機能を実装したい
- LINEのようなリアルタイムなチャット機能を簡単に作りたい
という方に向けて「RailsのAction Cableを使ってLINE風の非同期なチャット機能を実装する方法」をまとめてみました。
初学者の備忘録ゆえ至らない点もあると思いますが参考になれば幸いです⸝⸝- ̫ -⸝⸝
お品書き
チャット機能の完成イメージ
できたのはこんな感じ。
会話の内容はさておき、別のログインユーザーとリアルタイムでチャットをすることができます。
非同期なチャットの実装手順
ステップは大きく分けて3つ。今回非同期なチャットを実装するにあたって用いるのはAction Cableという機能。
「Action Cableってなんぞや」という方に向けて簡単に説明すると「フロント側とサーバ側が互いを監視し合って、リアルタイムで色々できるよ。やったね。」という機能です。
(詳しくはRailsガイドが参考になります。)
くるみ
RailsでAction Cableを使って非同期なチャット機能を実装する方法
作成するモデル
今回作成したモデルはUserモデルとChatモデルの2つ。
UserモデルとChatモデルは1対多の関係で、Chatモデルの「partner_id」はチャット相手のidのこと。
後述しますがUserモデルはdeviseの認証モデルとして作成します。
deviseでログイン認証機能を付ける
まずdeviseを使ってログイン認証機能を付け、Userモデルを作成します
ここで行ったことは以下の記事でまとめているのでそちらをご参照ください。
【Rails】deviseを使ってログイン認証を秒で実装する方法
チャット機能を実装
Chatモデルの作成
$ rails g model chat
resources :chats
モデル間の関連付け
class Chat < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :chats,dependent: :destroy
(以下略)
マイグレーション
class CreateChats < ActiveRecord::Migration[5.2]
def change
create_table :chats do |t|
t.references :users, null: false
t.integer :partner_id, null: false #チャット相手
t.string :sentence, null: false
t.timestamps
end
end
end
$ rails db:migrate
コントローラー
$ rails g controller chats index show
class ChatsController < ApplicationController
def index
@my_chats=current_user.chats
@chat_partners=User.where.not(id:current_user.id)#自分以外
end
def show
@partner=User.find(params[:id])
@chats_by_myself=Chat.where(user_id: current_user.id,partner_id: @partner.id)
@chats_by_other=Chat.where(user_id: @partner.id,partner_id: current_user.id)
@chats=@chats_by_myself.or(@chats_by_other)#リレーションオブジェクト達を結合する
@chats=@chats.order(:created_at)
end
end
indexでトークルーム一覧、showでチャットルームのやりとりを表示しています。
9行目以降は「送信したのが自分 or 送信されたのが自分」なチャットを取得し、それらのモデルオブジェクト達を結合させています。
ビュー
<table class="rooms">
<tbody>
<tr>
<td>
<%@chat_partners.each do |chat_partner|%>
<%=link_to "#{chat_partner.email}さんとのトークルーム","/chats/#{chat_partner.id}"%>
<%end%>
</td>
</tr>
</tbody>
</table>
<%=link_to("ログインページへ","/")%>
<%=link_to("ログアウトする","/users/sign_out",method: :delete)%>
これがindex.html.erb。トークルーム一覧です。
<h2 style="text-align:center"><%="#{@partner.email}さんとのチャット"%></h2>
<div id="chats">
<% @chats.each do |chat|%>
<%if chat.user_id==current_user.id%>
<div class="mycomment">
<p><%=chat.sentence%></p>
</div>
<%else%>
<div class="fukidasi">
<div class="faceicon">
<img src="/assets/profile.png" alt="相手">
</div>
<div class="chatting">
<div class="says">
<p><%=chat.sentence%></p>
</div>
</div>
</div>
<%end%>
<%end%>
</div>
<form id="send-form">
<input type="text" id="sentence" placeholder="入力してね" style="width:30%;">
<input type="submit" value="送信" id="send">
<input id="current_user_id" type="hidden" value= "<%=current_user.id%>">
<input id="partner_id" type="hidden" value= "<%=@partner.id%>">
</form>
<%=link_to("ログアウトする","/users/sign_out",method: :delete)%>
こっちがshow.html.erb。トークルームです。
参考 CSSで作る!吹き出しデザインのサンプル19選サルワカCSSでLINE風に
/************************************
** トークルーム一覧
************************************/
.rooms{
margin:0 auto;
background-color:#fdfdfd;
border: 2px solid #eee;
padding:1em 2em;
}
/************************************
** チャットの吹き出し
************************************/
#chats{
padding: 20px 10px;
max-width: 450px;
margin: 15px auto;
text-align: right;
font-size: 14px;
background: #7da4cd;
}
.fukidasi {
width: 100%;
margin: 10px 0;
overflow: hidden;
}
.fukidasi .faceicon {
float: left;
margin-right: -50px;
width: 40px;
}
.fukidasi .faceicon img{
width: 90%;
height: auto;
border-radius: 50%;
}
.fukidasi .chatting {
width: 100%;
text-align: left;
}
.says {
display: inline-block;
position: relative;
margin: 0 0 0 50px;
padding: 10px;
max-width: 250px;
border-radius: 12px;
background: #edf1ee;
}
.says:after {
content: "";
display: inline-block;
position: absolute;
top: 3px;
left: -19px;
border: 8px solid transparent;
border-right: 18px solid #edf1ee;
-webkit-transform: rotate(35deg);
transform: rotate(35deg);
}
.says p {
margin: 0;
padding: 0;
}
.mycomment {
margin: 10px 0;
}
.mycomment p {
display: inline-block;
position: relative;
margin: 0 10px 0 0;
padding: 8px;
max-width: 250px;
border-radius: 12px;
background: #30e852;
font-size: 15px;
}
.mycomment p:after {
content: "";
position: absolute;
top: 3px;
right: -19px;
border: 8px solid transparent;
border-left: 18px solid #30e852;
-webkit-transform: rotate(-35deg);
transform: rotate(-35deg);
}
/************************************
** 送信フォーム
************************************/
#send-form{
text-align:center;
margin-bottom:200px;
}
これでlocalhost:3000/users/showにアクセスし、フォームに入力してボタンを押してリロードしてあげればこんな感じに。
見た目だけはもう完成ですね。
くるみ
Action Cableでチャットをリアルタイムで行えるように
jQueryを使えるようにする
gem 'jquery-rails'
$ bundle install
まずgemをインストール。
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require jquery #追記
//= require jquery_ujs #追記
//= require_tree .
そして上のようにapplication.jsに記述してあげればjQueryが使えます。
Chatチャンネルを作成
$ rails g channel chat speak
これでchat.jsとchat_channel.rbが生成されます。
chat.jsにはフロント側の処理を、chat_channel.rbにはサーバー側の処理を書いていきます。
current_userを使えるようにする
module ApplicationCable
class Connection < ActionCable::Connection::Base
#channelでcurrent_userが使えるようにする
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
def session
cookies.encrypted[Rails.application.config.session_options[:key]]
end
end
end
channels/applicationcable/connection.rbに上のように記述。
これでChatChannelでcurrent_userが呼べるようになり、ログインしてるユーザーを取得することができます。
chat.js(フロント側の処理)
App.chat = App.cable.subscriptions.create("ChatChannel", {
connected: function() {
// Called when the subscription is ready for use on the server
},
disconnected: function() {
// Called when the subscription has been terminated by the server
},
received: function(data) {
//画面を開いているのがチャット送信者だった場合
if (data["isCurrent_user"]==true){
sentence=`<div class='mycomment'><p>${data["sentence"]}</p></div>`;
}
//画面を開いているのがチャット受信者だった場合
else{
sentence=`<div class='fukidasi'><div class='faceicon'>
<img src='/assets/profile.png' alt='管理人'></div>
<div class='chatting'><div class='says'><p>${data["sentence"]}</p>
</div></div></div>`;
}
$('#chats').append(sentence);
},
speak: function(sentence) {
current_user_id=$("#current_user_id").val();
partner_id=$("#partner_id").val();
return this.perform('speak',{sentence: sentence, current_user_id: current_user_id, partner_id: partner_id});
}
});
$(function(){
$("#send").on("click",function(e){
sentence=$("#sentence").val();
App.chat.speak(sentence);
$("#sentence").val(""); //フォームを空に
e.preventDefault();
});
});
やっていることを簡単にまとめると以下の通り。
- 送信ボタンがクリックされたらspeakメソッドを呼び出すように
- speakメソッドでは色々値を取得してサーバ側のspeakメソッドに渡している
- recieved〜内ではサーバから値を受けとり、要素を追加している(チャットの内容を表示している)
くるみ
今画面を開いているのがチャットを送った人なら右に、受け取った人なら左に吹き出しを追加する…ってな具合です。
次はこのフロント側と色々やり取りをするサーバ側に移ってきましょう。
chat_channel.rb(サーバー側の処理)
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_channel"
stream_for current_user.id
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
if data["sentence"]
Chat.create!(
user_id: data["current_user_id"].to_i,
partner_id: data["partner_id"].to_i ,
sentence: data["sentence"]
)
#画面を開いているのがチャット送信者だった場合
ChatChannel.broadcast_to data["current_user_id"].to_i,
sentence: data["sentence"],
partner_id: data["partner_id"],
isCurrent_user: true
#画面を開いているのがチャット受信者だった場合
ChatChannel.broadcast_to data["partner_id"].to_i,
sentence: data["sentence"],
partner_id: data["partner_id"],
isCurrent_user: false
end
end
end
subscribedメソッド内では接続が確立された時の処理を書いています。
stream_forでcurrent_user.idを指定することで今画面開いているユーザーに関連するストリームを作成。
くるみ
そしてフロント側から呼ばれるspeakメソッドの中で行っていることは以下の通り。
- フロント側から送られてきたデータを元にチャットのレコードを作成&保存
- broadcast_toで今画面開いているユーザーがチャット送信者なのか否かに応じて送るデータ(isCurrent_userの値)を変えている。
これでフロント側が、今画面を開いているのがチャットを送った人なのか否か判断できるわけですね。
以上です。これで完成!
くるみ
最後に:Action Cable難しくて草
作った後に「チャットルームごとに番号を振って配信する」という手もあったことに気が付いたという…
まあ不格好ながらカタチになったからいいか(›´ω`‹ )
くるみ
参考 【Rails6.0】ActionCableとDeviseの欲張りセットでリアルタイムチャット作成(改正版)Qiita