##概要
【ActionCable】+【jQuery】を用いたチャットアプリの作成について、説明します。
##目的
Javascriptやcoffeescriptを用いてチャット機能を実装する記事は多く見受けられたのですが、
jQueryしか分からない私は、jQueryを用いてチャット機能を実装する記事を見つけられず、
jQueryでの実装に苦労しました。。。
同じ境遇の方への技術共有と、自分の勉強のために、jQueryを用いたチャット機能の実装方法を説明します!
*一部jQueryではなく、javasctiptも使用していますが、
ActionCableを使う上で必要なものになりますので、なんとか理解して頂ければと思います。
*プログラミング学習を初めて3ヶ月ちょっとの人間が書いた記事です。
誤った理解や、もっとスマートに実装出来るであろう箇所があるかと思いますので、
もし気になる箇所が御座いましたら、ご指摘頂けますと幸いです。
##この記事のゴール
DM(ダイレクトメッセージ)が出来るチャットアプリの完成がゴールになります。
##目次
- この記事で説明する、DM機能とは?
- deviseの準備
- データベース設計
- チャットルーム作成機能の実装
- DMでのメッセージ投稿機能の実装
- 最後に
- 参考
##環境
rails (5.2.4.2)
jquery-rails (4.3.5)
devise (4.7.1)
##前提
この記事は、ActionCable + jQueryによるチャット機能の実装の続きになります。
ActionCableを実装して、オープンチャット機能の実装が出来ていることを前提として説明します。
##この記事で説明する、DM機能とは?
前回の記事では、全てのユーザが参加出来るオープンチャット機能の実装方法を説明しましたが、
例えば、自分とAさんだけでメッセージのやりとりをしたい場合には、
自分とAさんがチャットをするための「チャットルーム」を作成して、チャットメンバーを管理する必要があります。
この記事では、上記の機能をDM機能として、実装方法について説明します。
#deviseの準備
本記事では、セッションを使用するために
deviseのcurrent_userを用いますので、deviseを使用する準備をしておきます。
gem 'devise'
$ bundle install
$ rails g devise:install
$ rails g devise User
$ rails g model Room host_id:integer member_id:integer
今回は、ユーザの識別をするためにユーザの名前を使用しますので、
この時点で、ユーザのマイグレーションファイルにnameを追加して下さい。
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
# ここを追加する
t.string :name
# ----------以下、省略----------
追加したら、マイグレーションを実行します。
$ rails db:migrate
また、UserテーブルとRoomテーブルをMessageテーブルと関連づけるために
Messageモデルにforeign_keyを追加します。
$ rails g migration AddColumnToMessage user_id:integer room_id:integer
$ rails db:migrate
この時点でschemaファイルが以下のようになっているかと思います。
ActiveRecord::Schema.define(version: 2020_05_06_101819) do
create_table "messages", force: :cascade do |t|
t.string "content"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.integer "room_id"
end
create_table "rooms", force: :cascade do |t|
t.integer "host_id"
t.integer "member_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "name"
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
end
マイグレーションが出来たら、modelにアソシエーションとカスケードを追加します。
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
# ここを追加する
has_many :messages, dependent: :destroy
end
class Room < ApplicationRecord
# ここを追加する
has_many :messages, dependent: :destroy
end
class Message < ApplicationRecord
# ここを追加する
belongs_to :user
belongs_to :room
end
この時点で、データベース設計は下記のようになっています。
classname等を用いることで、もっとスマートな設計が出来るかもしれませんが、 今回は、このデータベース設計で実装を進めます。また今回は、チャットする相手を選択する機能を、usersのshow画面に表示しますので、
usersControllerも生成しておきます。
$ rails g controller users show index
ルーティングも設定しておきます。
Rails.application.routes.draw do
devise_for :users
# ここを修正
# get 'users/show'
# get 'users/index'
resources :users, only: [:show, :index]
# ここも修正
# root 'rooms#show'
resources :rooms, only: [:show]
mount ActionCable.server => '/cable'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
##チャットルーム作成機能の実装
modelとcontrollerが生成できたので、チャットルームの作成機能を実装します。
まずは、チャットをするメンバーを選択するためにユーザのshow画面を表示できるようにします。
users_controllerに下記のコードを記述します。
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
end
viewに下記のコードを記述します。
<h1><%= @user.name %>のプロフィール</h1>
<div><%= @user.name %>とチャットをする<div>
動作確認をするためにデータベースにユーザのデモデータを追加します。
$rails c
[1] pry(main)> User.create(name:"モブ太郎", email:"aa@aa", password:"password")
[2] pry(main)> User.create(name:"モブ子", email:"bb@bb", password:"password")
[3] pry(main)> User.create(name:"モブ次郎", email:"cc@cc", password:"password")
http://localhost:3000/users/1にアクセスすると、下記のように表示されるはずです。
ブラウザに表示されている「モブ太郎とチャットをする」をクリックすることで、
チャットルームを作成できるようにします。
ルーティングを下記のように修正して下さい。
Rails.application.routes.draw do
devise_for :users
# ここを編集する
resources :users, only: [:show, :index] do
resources :rooms, only: [:create]
end
resources :rooms, only: [:show]
mount ActionCable.server => '/cable'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
チャットする相手の情報(user_id)を取得するためにrooms#createをusersにネストさせています。
rooms_controllerに下記を追加して下さい。
class RoomsController < ApplicationController
# ここを追加する
def create
room = Room.new(host_id: current_user.id, member_id: params[:user_id])
room.save
redirect_to room_path(room.id)
end
def show
@messages = Message.all
end
end
viewファイルも修正します。
<h1><%= @user.name %>のプロフィール</h1>
# ここを編集する
<%= link_to user_rooms_path(@user), method: :post do %>
<div><%= @user.name %>とチャットをする<div>
<% end %>
動作確認をするために、アプリケーションにログインします。
http://localhost:3000/users/sign_inにアクセスして、下記の内容でログインして下さい。
・email: bb@bb
・password: password
ログインしたら、http://localhost:3000/users/1にアクセスします。
「モブ太郎とチャットをする」をクリックすると作成したチャットルームとしてrooms#showページが表示されるはずです。
コンソールからデータを確認するとRoomにレコードが追加されていることが、確認できます。
$ rails c
[1] pry(main)> Room.all
Room Load (1.0ms) SELECT "rooms".* FROM "rooms"
=> [#<Room:xxxxxxxxxxxxxxxx
id: 1,
host_id: 2,
member_id: 1,
created_at: Wed, 06 May 2020 13:01:31 UTC +00:00,
updated_at: Wed, 06 May 2020 13:01:31 UTC +00:00>,
ここまでで、チャットルームの作成し、チャットルームに入ることができました。
##DMでのメッセージ投稿機能の実装
チャットルームの作成ができましたので、
チャットルームに紐づいたユーザ間のメッセージのみ表示する機能を実装していきます。
まずは、Messageテーブルのカラムにuser_idとroom_idを追加していましたので、
投稿したメッセージのデータにuser_idとroom_idをつけてサーバにデータを送信する処理を実装します。
viewファイルを下記のように修正します。
<h1>Chat Room</h1>
<ul>
<% @messages.each do |message| %>
<li><%= message.content %></li>
<% end %>
</ul>
<ul id="add"></ul>
<input type="text", class="chat-input", autofocus>
<button class="button">送信</button>
# ここを追加します
<input type="hidden" class="room_id" value="<%= @room.id %>">
viewファイルからroom.jsにroom_idを渡せるように、viewファイル上にroom_idをセットしました。
inputタグのtypeをhiddenしてデータを記述することで、
ブラウザ上には表示されずに、view上にデータをセットすることができます。
*検証ツールを使えばブラウザ上でもhiddenタイプのデータを見ることができますので、
パスワードやメールアドレス等のデータはセキュリティ上この方法でviewに記述しない方が良いです。
現在入室しているroomの情報を@roomに代入できるように、rooms_controllerを修正します。
class RoomsController < ApplicationController
def create
room = Room.new(host_id: current_user.id, member_id: params[:user_id])
room.save
redirect_to room_path(room.id)
end
# ここを修正する
def show
@room = Room.find(params[:id])
@messages = @room.messages.all
end
end
ついでに、@messagesはroomに紐づいたデータのみを代入するように修正しました。
続いて、room.jsがview上の@room.idを受け取り、サーバにデータを渡せるように修正します。
App.room = App.cable.subscriptions.create("RoomChannel", {
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) {
$('<li>',{
class: "hoge",
text: data,
}).appendTo("#add");
// Called when there's incoming data on the websocket for this channel
},
// ここも修正します
speak: function(content, room_id) {
return this.perform('speak', {content: content, room_id: room_id});
}
});
$(function(){
$(".button").on("click",function(){
// ここを追加します
var room_id = $(".room_id").val();
var content = $(".chat-input").val();
// ここを修正します
App.room.speak(content, room_id);
$(".chat-input").val("")
});
});
サーバがデータを受け取って、メッセージを保存した後、
フロントにデータを渡せるようにroom_channelも修正します。
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
# ここを修正します
message = Message.new(content: data["content"], room_id: data["room_id"], user_id: current_user.id)
message.save
ActionCable.server.broadcast "room_channel", content: data["content"], room_id: data["room_id"]
end
end
サーバから受け取ったデータを、viewに表示できるようにroom.jsを修正します。
App.room = App.cable.subscriptions.create("RoomChannel", {
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) {
$('<li>',{
class: "hoge",
// ここを修正します
text: data["content"],
}).appendTo("#add");
};
// Called when there's incoming data on the websocket for this channel
},
speak: function(content, room_id) {
return this.perform('speak', {content: content, room_id: room_id});
}
});
$(function(){
$(".button").on("click",function(){
var room_id = $(".room_id").val();
var content = $(".chat-input").val();
App.room.speak(content, room_id);
$(".chat-input").val("")
});
});
dataの中身が増えましたので、表示したいデータを明示的に記述しました。
さらに、サーバから受け取ったデータが、どこのルーム宛てかを判断して、
viewに表示できるようにroom.jsを修正します。
App.room = App.cable.subscriptions.create("RoomChannel", {
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) {
// ここを修正します
var receiver_room_id = $(".room_id").val();
if(data["room_id"] == receiver_room_id){
$('<li>',{
class: "hoge",
text: data["content"],
}).appendTo("#add");
};
// Called when there's incoming data on the websocket for this channel
},
speak: function(content, room_id) {
return this.perform('speak', {content: content, room_id: room_id});
}
});
$(function(){
$(".button").on("click",function(){
var room_id = $(".room_id").val();
var content = $(".chat-input").val();
App.room.speak(content, room_id);
$(".chat-input").val("")
});
});
viewから@room.idの値を取得して、データを送信した際の@room.idを等しければ
メッセージを表示する処理をしました。
ここまでで実装は、ほぼ終わったのですが、
このままメッセージを投稿してもメッセージは表示されません...
room_channel.rbファイルでcurrent_userを使用しているのですが、
room_channel.rbファイルではセッションを使用することができないのです。
current_userの値を使用するためには、コネクションというファイルを下記のように修正する必要があります。
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
内容としては、セッションであるcurrent_userの値を、変数として使用できるように処理しています。
*詳細なコードの意味までは把握できていないため、知りたい方は公式ガイドを参照下さい。
ここまで出来たら動作確認をして見て下さい。
下記の要件が満たせていれば、実装完了です。
・同じチャットルームに入室しているユーザは、ルーム内のメッセージが非同期で表示される。
・投稿したメッセージが、他のチャットルームに表示されない。
##最後に
最後まで見て頂き、有り難う御座いました。
ちなみに、今回の説明したコードだけでは、チャットルームが重複して生成できますし、
実際にアプリケーションとして運用するためには、不足している点が多々あるかと思います。
本記事では、その辺の説明はしませんので、皆さんで工夫して頂ければと思います。
#参考