【Rails】ActionCableでチャットアプリを作る
LINEを作る過程でActionCableでかなり悩まされたので、アウトプットかつおさらいということで書いていきます
今回は簡単なチャットアプリケーションを作ろうと思います
初めてActionCableを触る!くらいの人にもなるべく分かるようにしていきたいと思います
ActionCableってなんぞや?
めちゃくちゃ詳しく調べ上げた訳でない上に言葉にするのが難しいですが、ざっくり言うと「ユーザー側とサーバー側が常に通信状態だよ」って感じです
ブラウザのページは基本的にリクエストを送りサーバー側と通信しデーターを取得します
そのリクエストによってページが切り替わり、サーバーのデーターによって表示が変わります
この状態のままだと更新をかけるまではページは最新のものになりません
ActionCableはほぼ常にサーバーの情報を取得します
サーバーにデータ送り、保存された後そのデータを、ActionCableを通して接続されているユーザーのviewに送り返します
ここで関わってくるのは
- view (html)
- javascript (coffeeがデフォルト)
- ActionCable (railsの機能)
になります
自分は文字で書くと頭に入らないので図で表してみます
JSに渡ったデータがActionCableに送られ、保存するなどの処理を行った後にJSにデータを返します
返されたデータはJSによってviewを書き換える処理を行います
この一連の流れを作り上げていきます
では作っていきましょう
環境
開発環境 - Virtualbox - Vagrant
railsのバージョンが5と6では書き方が変わるようです
今回5で行っています
ER図
今回はとりあえずActionCableの導入とチャットができるようになるところまでは作ります
後々フォロフォロワー機能、チャットルーム機能を作り、特定の相手にチャットが届くようにしていこうと思います
アプリを作っていく
$ rails new chat-app
chat-appという名前で作っていきます
ユーザー認証を作りたいので、gem 'devise' を導入します
gem 'devise'
$ bundle install $ rails g devise:install $ rails g devise User name:string $ rails db:migrate $ rails g devise:views users $ rails g devise:controllers users
ルート画面を新規登録画面にします
devise_for :users, controllers:{ sessions: 'users/sessions', registrations: 'users/registrations' } devise_scope :user do root :to => 'users/registrations#new', as: :unauthenticated_root end
<h2>Sign up</h2> <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> <%= render "users/shared/error_messages", resource: resource %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email %> </div> <div class="field"> <%= f.label :password %> <% if @minimum_password_length %> <em>(<%= @minimum_password_length %> characters minimum)</em> <% end %><br /> <%= f.password_field :password %> </div> <div class="field"> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation %> </div> <div class="actions"> <%= f.submit "Sign up" %> </div> <% end %> <%= render "users/shared/links" %>
<h2>Log in</h2> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <div class="field"> <%= f.label :email %><br /> <%= f.email_field :email %> </div> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password %> </div> <div class="actions"> <%= f.submit "Log in" %> </div> <% end %> <%= render "users/shared/links" %>
before_action :configure_permitted_parameters, if: :devise_controller? private def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up,keys:[:name]) end
ここまででユーザー認証の機能はできました
次はER図にのっとりモデルとコントローラーを追加します
モデル・コントローラーの作成
実際にチャットを行うルームモデル
メッセージを持つメッセージモデル
どのユーザーがどのルームに登録してあるかを持つエントリーモデル
この3つを作ります
最終的にはLINEのように個人個人とチャットできるようにしますが、一旦全ての人が一つのチャットルームでチャットできるようにします
$ rails g model Room name:string $ rails g model Entry user:references room:references $ rails g model Message message:text user:references room:references $ rails db:migrate
外部キーにはreferencesを使用しています
ER図に沿ってモデル同士の関連付けをしていきます
has_many :messages has_many :entries has_many :rooms, through: :entries
has_many :messages has_many :entries has_many :users, through: :entries
belongs_to :user belongs_to :room
belongs_to :user belongs_to :room
throughを使うことで、中間モデルを介してその先のモデルにアクセスできます
コントローラーに関しては、ルームのindexとshow、ユーザのshowを作ります
$ rails g controller Room index show $ rails g controller User show
ルームとユーザーのコントローラーができたのでルーティングを設定します
resources :rooms, only: [:index, :show] resources :users, only: [:show]
viewも作ってくれるので、サインアップ後、ログイン後はルームのindexに飛ぶようにします
def after_sign_in_path_for(resource) rooms_path end def after_sign_up_path_for(resource) rooms_path end def after_sign_out_path_for(resource) unauthenticated_root_path end
サインアップ後にルームインデックスへ遷移したでしょうか
次はviewを書き換えていきます
viewを作成していく
room.indexのviewを作成していきます
ログアウトボタンを作ります
また、自身の名前を表示しておきます
とりあえずチャットルームの概念は置いておいて、このページでチャットができるようにしていきます
<% if user_signed_in? %> <%= link_to "ログアウト", destroy_user_session_path, method: :delete %> <p> YourName: <%= current_user.name %> </p> <% end %> <h2>Chat Room</h2> <div class="message"> <%= render partial: 'rooms/messages', locals: { messages: @messages} %> </div>
メッセージ一覧を表示させる部分は、部分テンプレートを使います
そのため、views/_messages.html.erbを作ります
@meesagesには今までのメッセージを全て持たせます
<div class="message"> <% messages.each do |message| %> <p><%= message.message %></p> <% end %> </div>
def index @messages = Message.all end
これでメッセージを表示させれるようになりました
次に投稿フォームを作ります
通常、フォームと言えばform_forやform_withが常ですが、ここではhtmlのinput要素を使って作ります
...追記 <div id="add-message"></div> <!-- 投稿フォーム --> <input type="text" data-behavior="room_speaker">
input部分の data-behavior は、厳密には違うようですが、クラス名やID名のようなものだと考えてください
ここまででユーザー登録、メッセージの表示と投稿までをできるようになりました
ただ、今のままだとinputはただ文字を書けるだけの箱にすぎません
保存などはActionCableを使用して行います
ではいよいよActionCableを導入します
ActionCableを導入
まず最初にchannelを作成します
$ rails g channel room speak
以下のように、channelとchannelのcoffeeScriptが作成されます
[vagrant@localhost chat-app]$ rails g channel room speak Running via Spring preloader in process 7636 create app/channels/room_channel.rb identical app/assets/javascripts/cable.js create app/assets/javascripts/channels/room.coffee
データはJSを通ってchannelに送られます
channelは送られてきたデータを処理するコントローラやモデルのようなものです
まずcoffeeの方から作り上げていきます
jQueryの方が書きやすいかと思うので、jQueryの書き方を使います
そのため、railsでjQueryを使えるようにgemを導入します
gem 'jquery-rails'
$ bundle install
//= require rails-ujs //= require activestorage //= require jquery //= require_tree .
turbolinksのコメントアウトがありますが、色々と不便なこともあるので消しました
下準備はこれで完了です
assets/javascripts/channels/room.coffeeに記述します
App.room = App.cable.subscriptions.create "RoomChannel", connected: -> # 通信が確立された時の処理 disconnected: -> # 通信が切断された時の処理 received: (data) -> # データが送信されてきた時の処理 speak: (message) -> # channelのspeakアクションにmessageパラメータを渡す @perform 'speak', message: message # チャットを送る $(document).on 'keypress', '[data-behavior~=room_speaker]', (event) -> # return(Enter)が押された時 if event.keyCode is 13 #channel speakへ、event.target.valueを引数に App.room.speak event.target.value # inputの中身を空に event.target.value = '' alert() event.preventDefault()
この時点で、inputにフォーカスしてエンターを押してみましょう
このようにアラートが表示されれば、javascriptがしっかりと動いています
ではchannelの方にいきましょう
class RoomChannel < ApplicationCable::Channel def subscribed # 接続 stream_from "room_channel" end def unsubscribed # 切断 end # coffeeから送られて来たmessage(event.target.value)を受け取る # inputタグの文字がメッセージカラムに保存される def speak(message) Message.create(message: message['message']) end end
coffeeからspeakにデータが送られます
そのデータを基にmessageを保存します
しかしメッセージにはFKがあります
今はroom.indexで全て表示するのでroom_idはいいとしても、user_idは保存したいところです
ここで、htmlのinputにdataとしてユーザーidを持たせ、js側に渡し、パラメータとしてchannelまで通します
実際に作ってみましょう
<!-- 投稿フォーム --> <input type="text" data-behavior="room_speaker" data-user=<%= current_user.id %>>
data-〇〇は値をjs側に送ることができます
この場合だと、inputのdata['user']とすることで、自分のユーザーidを使うことができます
次にcoffeeです
App.room = App.cable.subscriptions.create "RoomChannel", connected: -> # 通信が確立された時の処理 disconnected: -> # 通信が切断された時の処理 received: (data) -> # データが送信されてきた時の処理 speak: (message, user) -> # channelのspeakアクションにmessageパラメータを渡す @perform 'speak', {message: message, user: user} # チャットを送る $(document).on 'keypress', '[data-behavior~=room_speaker]', (event) -> # return(Enter)が押された時 if event.keyCode is 13 #channel speakへ、event.target.valueを引数に App.room.speak event.target.value, $('[data-user]').attr('data-user') # inputの中身を空に event.target.value = '' event.preventDefault()
エンターキーを押された時に、speakアクションの引数を文字とユーザーidの2つを指定します
speak: の後に、messageに文字、userにユーザーidを渡すように指定します
ではchannelにいきます
# coffeeから送られて来たmessage(event.target.value)を受け取る # inputタグの文字がメッセージカラムに保存される def speak(message) Message.create(message: message['message'], user_id: message['user'].to_i) # room_channelに紐づくjsにメッセージの内容を配信する ActionCable.server.broadcast 'room_channel', message: message['message'] end
実際にターミナルで値がどのようになっているか見てみましょう
(message)に、{"message"=>"こんにちわ", "user"=>"1", "action"=>"speak"}と、これだけの情報が入っています
メッセージは文字列なのでそのままで大丈夫です
ユーザーidは、integerにしたいので、渡って来たデータのままだとstring状態です
.to_iメソッドで数字に変換してから保存するようにします
これでメッセージを保存するところまでできました
ActionCable.server.broadcastで、inputの文字列をjsに返します
jobと言う機能を使うと、DBに保存したデータを配信できるのですが、それは次回にします
ここでは一旦送られてきた生のデータを配信します
ではcoffee側にいきます
received: と言う値が送られて来た時の処理を記述します
received: (data) -> # データが送信されてきた時の処理 $('#add-message').append("<p>" + data["message"] + "</p>");
(data)には["message"]として、inputに入力された文字列が入っています
その文字列をpタグで囲んだ要素として、div id="add-message" に追加します
それでは、フォームに文字を入力してエンターを押してみましょう
このように内容が即座に追加されると思います
ここで問題発生
room_idを入力していないのでDBに保存されません、そのため一旦room_idがnilでも保存できるようにします
class Message < ApplicationRecord belongs_to :user belongs_to :room, optional: true end
optional: trueは、紐づくデータがnilでも保存できるようにするよ、と言うメソッドです
railsの仕様で、FKがない場合は保存ができなくなっていたようです
これで簡単ではありますがチャット機能ができました
チャット機能の完成
ブラウザを2つ立ち上げて、実際チャットが即座に反映されるか確かめてみます
実際にデプロイ後にちゃんと動くかどうかはこれからですが、ActionCableはこんな感じで動きます!
もしかしたら抜け等あるかもしれません...ご了承をば...
次回は全体ではなく、特定のユーザーやグループでのチャット機能を作っていきます
今回参考にしたサイト・記事等
WebSocketはなぜ生まれたのか?Action Cableを使いたい
ActionCableを使って簡易チャット機能を作ろう
ActionCableを用いてリアルタイムチャットの実装
Rails 5 + ActionCableで作る!シンプルなチャットアプリ(DHH氏のデモ動画より)
【Rails6.0】ActionCableとDeviseの欲張りセットでリアルタイムチャット作成(改正版)
[Rails]ActionCableを使用してリアルタイムチャットの実装
ありがとうございます!