#はじめに
はじめまして、とあるプログラミングスクールでメンターをしている者です。
今回、その受講生からグループチャットを作りたいというお願いを聞き自分で勉強をしてみました。せっかくなので、リアルタイムでチャットができるよう調べて見たところ、RailsのActioncableという機能にたどり着きました。今回、その備忘録となっております。まだまだRailsの細かい機能については理解できていないので、間違い等あれば指摘していただければと思います。
本記事を1つにまとめるとかなりのボリュームとなってしまうため、2部構成となっております。本記事は第1部目です。第2部目は以下のリンクからご参照ください。
【Rails6】ActionCableを用いたリアルタイムグループチャット機能② ~グループ機能導入→完成まで~
#参考記事
【Rails6.0】ActionCableとDeviseの欲張りセットでリアルタイムチャット作成(改正版)
【Rails6.0】ActionCableを使用したライブチャットアプリを実装する手順を解説
DOM Keyboardイベントで押されたキーを判別するにはkeyプロパティを使う
#本題
今回作成していくために使っている機能は以下の通りです。
- ユーザー認証・・・Devise
- リアルタイムチャットの実現・・・Actioncable
また、基本的なCRUD機能のついたアプリの作成済みが前提となっております。
##作成順序
以下の手法で作成していきます。
- チャット機能の作成
- グループに参加するメンバーのみでチャットができるよう改良
第一部ではここまで行います。
第二部では、グループ作成→完成まで行います。
##開発環境
macOS Big Sur 11.4
Ruby2.7.4
Rails6.1.4.1
DB:sqlite3
##gemのインストール
Gemfileに必要なgemを追加し、インストールします。
gem 'devise'
$ bundle install
##ユーザー認証
インストールしたDeviseを用いて、ユーザー認証を実現します。
$ rails generate devise:install
$ rails generate devise user
$ rails db:migrate
詳しいdeviseの仕様等は、検索して頂くと参考になる記事がたくさん出てくると思います。
サーバーを起動して、http://localhost:3000/users/sign_inにアクセスし、ログイン機能を確認してください。
##チャット機能の作成
###Messageモデルの作成
# string型のcontentカラムを持つMessageモデルを作成
$ rails g model message content:string
$ rails db:migrate
###roomsコントローラーの作成
# showアクションを持つroomsコントローラーを作成
$ rails g controller rooms show
###ルーティングの設定
Rails.application.routes.draw do
#deviseインストール時に自動的に追加される
devise_for :users
#roomsコントローラーのshowアクションに飛ぶリンクの作成
get 'rooms/show' => 'rooms#show'
end
###showアクションの中身を追加
class RoomsController < ApplicationController
before_action :authenticate_user! # Deviseのログイン確認
def show
# メッセージ一覧を取得
@messages = Message.all
end
end
###roomsコントローラーのshowアクションに対するビューの作成
<h1>Chat room</h1>
<div id='messages'>
<%= render @messages %>
</div>
render @messages
とはrender partial: 'messages/message', collection: @messages
の省略形です。曖昧な方は以下の記事を参考にしてください。
【Rails基礎】ややこしい部分テンプレートの省略形について簡単にまとめてみた
message1つ1つを表示するパーシャルを追加
<div class='message'>
<p><%= message.content %></p>
</div>
この中のmessage
変数の中に@messages
のそれぞれのレコードが代入されている。
投稿データの登録
$ rails c
Message.create! content: 'Hello world!!'
http://localhost:3000/rooms/showにアクセスし、投稿したデータが表示されるか確認。
###Roomチャネルの作成
Actioncableの機能を使うためにチャネルを作成します。
そのためにjQueryを使用するための準備を行います。
$ yarn add jquery
const { environment } = require('@rails/webpacker');
const webpack = require('webpack');
environment.plugins.append('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery'
})
);
module.exports = environment;
// 追加
require('jquery');
デベロッパーツールのコンソールでバージョンが表示されたら準備OK。
console.log($.fn.jquery);
// 3.6.0
Actioncableの設定、speak
メソッドを持つroom
チャネルを作成します。
$ rails g channel room speak
Actioncableの有効化、routes.rb
に以下の事項を追記する。
※バージョンによっては記述しなくても大丈夫。
# 追加
mount ActionCable.server => '/cable'
room_channel.rb
を編集する。
- subscribedメソッドのコメントアウトを外し、"room_channel"をstreamする。
- speakメソッドに引数を用意し、中身を追加する。
class RoomChannel < ApplicationCable::Channel
def subscribed
# room_channel.rbとroom_channel.jsでデータの送受信ができるようになる。
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
# jsで実行されたspeakのmessageを受け取り、room_channelのreceivedにブロードキャストする
ActionCable.server.broadcast 'room_channel', message: data['message']
end
end
channelでspeakを実行させるためにroom_channel.js
でspeak関数を定義する。
import consumer from "./consumer"
// 「const chatChannel =」を追記
const chatChannel = consumer.subscriptions.create("RoomChannel", {
// 省略
// room_channel.rbでブロードキャストされたデータがreceivedに届き、アラート表示を実行。
// アラート表示する内容は「data([‘message’])」
// 「event.target.value」で取得したデータと同じ
received(data) {
return alert(data['message']);
},
// 仮引数 function(message)のmessage
// 実引数 event.target.value
// room_channel.rbのspeakアクションを動かすために、speak関数を定義
speak: function(message) {
return this.perform('speak', {message: message});
}
});
// フォーム内でEnterキーが押された時の動作を記述
// event.KeyCode === 13は非推奨となっているため、event.key === 'Enter'と変更
$(document).on('keypress', '[data-behavior~=room_speaker]', function(event) {
if (event.key === 'Enter') {
chatChannel.speak(event.target.value);
event.target.value = '';
event.preventDefault();
}
})
サーバーを起動し、テキストボックスに文字を入力、Enterキーを押してアラートが出ればOK.
###入力したテキストがデータベースに保存されるように改善
room_channel.rb
のspeak
アクションの記述を以下のように変更.
class RoomChannel < ApplicationCable::Channel
#省略
def speak(data)
#ActionCable.server.broadcast 'room_channel', message: data['message']
#上記の文を変更
Message.create! content: data['message']
end
end
Broadcastのjobを作成
$ rails g job MessageBroadcast
作成したjobを編集し、ブロードキャスト処理を追加
class MessageBroadcastJob < ApplicationJob
#省略
# ブロードキャスト(一つのネットワークの中にあるすべてのホストに対してデータを送る。)
def perform(message)
ActionCable.server.broadcast 'room_channel', message: render_message(message)
end
# app/views/message/_message.html.erbを呼び出す。
private
def render_message(message)
ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
end
end
Messageモデルを編集。データ保存後の処理を指定。
class Message < ApplicationRecord
validates :content, presence: true
# データ保存後にMessageBroadcastJobのperformメソッドを実行,引数はself
after_create_commit { MessageBroadcastJob.perform_later self }
end
サーバー起動、文字入力後エンターを押し、入力した文字入りHTMLがアラートされればOK。
###非同期でアラートの代わりに画面に文字を表示させる。
room_channel.js
の変更。
received: function(data) {
return $('#messages').append(data['message']);
}
実際にメッセージを送信して確認。これでチャット機能の実装は終了です。
##複数のルームとメッセージの紐付け
ここから改良して、複数ルームのそれぞれのルームに所属している人のみにチャットが見れるようにします。
$ rails g model room
$ rails db:migrate
messagesテーブルに紐付けのためのカラムを追加します。
$ rails g migration AddReferencesToMessages user:references room:references
NotNull制約でうまくいかなかったので以下のようにMigrationファイルを手直し
この解決策をどなたかにご教示して頂きたいです。
class AddReferencesToMessages < ActiveRecord::Migration[6.1]
def change
add_reference :messages, :user, foreign_key: true
add_reference :messages, :room, foreign_key: true
end
end
$ rails db:migrate
UserモデルとRoomモデルに紐付けのためのhas_many
とbelongs_to
を追加
class User < ApplicationRecord
# ...
has_many :messages
end
class Room < ApplicationRecord
has_many :messages
end
class Message < ApplicationRecord
# ...
belongs_to :user
belongs_to :room
end
参考:アソシエーションはuserとmessageが1:N、roomとmessageが1:N
##接続ルームのメッセージのみが表示されるようRoutingとControllerとViewを変更
###Routing
Rails.application.routes.draw do
mount ActionCable.server => '/cable'
devise_for :users
# ...
resources :rooms
end
###Controller
class RoomsController < ApplicationController
# ついでにRoom一覧を表示させるアクションも追加しておく
def index
@room_lists = Room.all.order(:id)
end
def show
@room = Room.find(params[:id])
@messages = @room.messages
end
end
###View
接続ルームに紐づいたメッセージを全て表示させるためのview
<%#ここの data-room_id を使ってjs側で部屋を見分ける %>
<div id='messages' data-room_id="<%= @room.id %>">
<%= render @messages %>
</div>
<form>
<%= label_tag :content, 'Say something:' %>
<input type="text" data-behavior="room_speaker">
</form>
接続ルームに紐づいたメッセージを一つ一つ表示するパーシャル
<div class='message'>
<p><%= "#{message.user.username}: #{message.content}" %></p>
</div>
Room一覧画面を追加
<h1>Real part</h1>
<div>
<ul>
<% @room_lists.each do |room| %>
<li><%= link_to "ROOM#{room.id}", room_path(room) %></li>
<% end %>
</ul>
</div>
##Room.idを受け取り、監視する場所を分ける
import consumer from "./consumer"
// $(function() { ... }); で囲むことでレンダリング後に実行される
// レンダリング前に実行されると $('#messages').data('room_id') が取得できない
// turbolinks を使っている場合は $(document).on('turbolinks:load', function() { ... }); で囲うorturbolinksの設定を無効にする。
$(function(){
const chatChannel = consumer.subscriptions.create({ channel: 'RoomChannel', room: $('#messages').data('room_id') }, {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received: function(data) {
return $('#messages').append(data['message']);
},
speak: function(message) {
return this.perform('speak', {message: message});
}
});
$(document).on('keypress', '[data-behavior~=room_speaker]', function(event) {
if (event.key === 'Enter') {
chatChannel.speak(event.target.value);
event.target.value = '';
return event.preventDefault();
}
});
});
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params['room']}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
Message.create! content: data['message'], user_id: current_user.id, room_id: params['room']
end
end
ブロードキャストする場所もルームごとに分ける。
def perform(message)
ActionCable.server.broadcast "room_channel_#{message.room_id}", message: render_message(message)
end
##current_user情報を取得する
Deviseを使っている場合、ログインユーザーのインスタンスには以下でアクセスできる。
env['warden'].user
これを使ってcurrent_userをWebsocket側で使うためのコードを書く
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
reject_unauthorized_connection unless find_verified_user
end
private
def find_verified_user
self.current_user = env['warden'].user
end
end
end
#完成
以上で第一部の実装が完了となります。第二部ではグループチャットの部分を仕上げて最終完成まで持っていきます。↓
【Rails6】ActionCableを用いたリアルタイムグループチャット機能② ~グループ機能導入→完成まで~