【React】stateとイベント
一応、ここは学習のアウトプットの場と言うことを先にお伝えしておきます...
Reactの勉強をしようと思って色々検索すると、初心者向けの記事は色々出てくるけれどどれも書き方が違ったり、コード通りに書いても動かなかったりと中々初心者のハードルが高いような気がするReact
どうも調べると技術は日々新しくなっているから書き方も変わっていく、と。
何ならReactのチュートリアルですら書き方が古いものとか言う話も。
何を信じればいいのか分からない疑心暗鬼状態なのでとりあえずProgateを始めてみます
イベント
divやbuttonの要素をクリックしたりした時に発火するイベントはタグの中に書く
<button イベント名={ () => {処理}} > </button>
イベントはアロー関数を使う
「その要素がクリックされた時に発火」は onClick を使う
例えばクリックでコンソールにログを出す
<button onClick={()=>{console.log('Hello World')}}> </button>
コンソールに Hello World が表示される
state
stateはオブジェクトとして定義する
クラス定義からreturnの間に記述
this.state = {キー名: 値} の書き方で記述
// インポート import React, { Component } from 'react'; import logo from './logo.svg'; import './App.css'; // コンポーネントの内容 class App extends Component { render() { state = {text: "Hello World"} return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Welcome to React</h1> </header> <p className="App-intro"> To get started, edit <code>src/App.js</code> and save to reload. </p> </div> ); } } // エクスポート export default App;
こんな感じかな
stateの値はthis.setStateで変更できる
stateの値が変更されるとその情報をレンダリングしてビューの情報を書き換える
stateの値が変わるたびにHTMLが変化するように見える..と言う感じ
stateとイベント
ボタンを押して、見た目が変わる動きを作ろうとするときは例えば
- buttonにクリックイベントを持たせる
- イベントにはstateを書き換えるようにしておく
- 書き換わったstateでレンダリング
- 元の表示を書き換えるのでユーザーからは一瞬で変わったように見える
って言う感じですねー
コードで書いてみました
// インポート import React, { Component } from 'react'; import logo from './logo.svg'; import './App.css'; // ファイルの実質上の中身 class App extends Component { state = {text: 'World'} handleClick(text){ this.setState({text: text}) } render() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h1 className="App-title">Hello {this.state.text}</h1> <span onClick={()=>{this.handleClick("World")}}>world</span> <span onClick={()=>{this.handleClick("React")}}>react</span> </header> </div> ); } } // エクスポート export default App;
まず最初にstateが初期化されます
そして handleClick と言う名前のイベントが設定されます。引数はtextとします
このイベントが呼ばれた時、stateのtextは引数の値に変わりレンダリングされるようになっています
下のJSX(HTMLみたいなとこ)では Hello の後に初期化したstateのキー "World" が呼ばれているので最初の表示はHello World です
spanに onClick ,つまりspanがクリックされた時に、handleClickのイベントが呼ばれ引数textにクリックされた方の文字列が渡されます
渡された文字列でstateを書き換えレンダリングし、Helloの後の文字を書き換える...
理解するのに苦しみましたが、スクールで3ヶ月みっちりRailsを触っていたのでこの辺のやりとりはわかるようになってきました(汗
これでクリックすると書き換わるようになりました
すごいなぁ、何かもうすごいとしか言えない
React楽しい!楽しいReact!
また学習を進めて更新していきます
【Rails】プリコンパイル時のundefined method `[]' for nil:NilClass
デプロイしたアプリをローカルで更新し、AWSのEC2でプリコンパイルしようとした時にエラーが出ました
アセット以下をEC2のアプリ上でプリコンパイル
$ bundle exec rails assets:precompile RAILS_ENV=production
すると、成功できずメッセージがでます
[1/4] Resolving packages... success Already up-to-date. Done in 0.05s. rails aborted! NoMethodError: undefined method `[]' for nil:NilClass
??? つまりどういうことやねん!
日本語の記事でも外国の記事でも多いのは環境変数が渡せてなくてnilになっているのでは?とのこと
しかし前回からの変更で環境変数は特に触ってないからここではないのかな、と
思い当たるとろこで、前回から変更したview側のimgタグと、css側のbackground-imageを一旦削除してみました
ということはやはり画像表示で何かおかしくなってるな?
やってみたこと
- viewのimageタグとscssのbackground-imageを消した
プリコンパイルはできるようになりました
でも当然画面に画像は出てきません。。。次
- app/assets/imagesの画像をpublic/imagesに移動してみた
view側の
= image_tag asset_path('ika.png')
ここは本番環境でも表示されるように
だけどCSS側
background-image: image-url("/images/sangaku.jpg");
プリコンパイルに失敗します ちなみに
background-image: url("/images/sangaku.jpg");
これだとプリコンパイル成功
image-urlとすることでパスを読むことができなくなってninが出ている??
- viewにstyleを直書きしてみた
#room-group style="background-image: url( #{asset_path"sangaku.jpg"})"
image_tagのasset_pathが使えるならこれでできるのでは?という考え
結果としてはbackgroundが表示されました
ただこれだと別の画像を追加して行った時に結局プリコンパイルできないよね?って話になりますね...
とりあえず表示だけはできるようになりました
もっとこの辺は理解していかないとだなぁ
【Rails】ActionCable AWS/Nginx/Pumaでデプロイする
前回作ったチャットアプリをAWSでデプロイしました
ちょっと沼った部分もあったので書いていきます
環境
条件として
- ローカルではちゃんと動作する
- 本番環境ではActionCable以外はしっかり表示される
ActionCableの設定を行う
おそらく、そのまま素でデプロイしたままの状態だと、テキストを入力させても通信が行われずリアルタイムにviewに反映されないままだと思います
変更を加えるのは
- config/enviroments/producion.rb
- config/cable.yml
EC2側の
- /etc/nginx/conf.d/app-name.conf
この3つです
まずはテキストエディタでconfigの設定を
# Mount Action Cable outside main process or domain # config.action_cable.mount_path = nil # config.action_cable.url = nil config.action_cable.allowed_request_origins = [ 'http://Elastic IP/' ] ActionCable.server.config.disable_request_forgery_protection = true
コメントアウトを外して書き換えと、新しい記述を加えます
以下、Railsガイドより一部抜粋
Action Cableは、指定されていない送信元からのリクエストを受け付けません。送信元リストは、配列の形でサーバー設定に渡します。
送信元リストには文字列のインスタンスや正規表現を利用でき、これに対して一致するかどうかがチェックされます。
config.action_cable.allowed_request_origins = ['https://rubyonrails.com', %r{http://ruby.*}]
すべての送信元からのリクエストを許可または拒否するには、次を設定します。
config.action_cable.disable_request_forgery_protection = true
配信元のアドレスが同じかどうか確認して、リクエストを許可する?みたいになるのかな?
次にymlファイルを
production: # adapter: redis # url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> # channel_prefix: chat-app_production adapter: async
アダプタは、サブスクリションする時の設定ぽいです
production以下は、本番環境用なのでadapterにasyncを使うのは本来非推奨と、Railsガイドの方にも書いてあります
今回は運用とかではなく学習用で作っているのでとりあえずasyncでOKとしておきます
なお使用できるのは
- Redis
- PostgreSQL
の2つみたいです
EC2側にssh 接続して、カレントディレクトリからNginxのファイルを編集していきます
$ sudo vi /etc/nginx/conf.d/chat-app.conf
sudoを付けなくて変更できない!と延々言われました...sudoは管理者権限で変更できるようになるコマンドっぽいです
中身を書き換えます
server{ } の中に追記していきます
location /cable { proxy_pass http://puma/cable; proxy_http_version 1.1; proxy_set_header Upgrade websocket; proxy_set_header Connection Upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
自分の場合はこれでひとまず動くようになりました
途中で時間がかかったところ
- jsが全く効かない
これに関してはassets以下のjsをプリコンパイルしていませんでした
ec2側アプリのディレクトリで
$ bundle exec rails assets:precompile RAILS_ENV=production
これで解決
- 本番環境デベロッパーツールのconsoleで404エラー
- 本番環境デベロッパーツールのconsoleでfailed: WebSocket is closed before the connection is established
こいつらがエラーとして出る時は接続がうまくできていません
config/enviroments/producion.rbでIPアドレスが正しいか、もしくはドメイン名が正しいか
/etc/nginx/conf.d/chat-app.confの値が正しいか
ローカル環境を本番環境にpushしたか
など色々とやってみましょう
自分の場合は色々やってる内に動くようになりました笑
またボチボチと更新していきます
【Rails】ActionCableでチャットアプリ・グループや特定のユーザーとチャットする
前回、1からActionCableを用いたチャットアプリを作りました
現状ではチャットが全てのユーザーに対して送られているアプリです
そのため、特定のユーザーとチャットしたり、または複数人でチャットしたりと言う機能を作っていきたいと思います
前回作ったアプリを基に、コードを追加したりしていきます
前回作成したアプリはgithubからダウンロードできます
今回はフォロー機能も実装し、相互フォロー状態のユーザー同士でチャットできるルームを作るなどしていきます
ではいきましょう
フォロー機能を作る
フォローフォロワー機能についてはこちらがとても分かりやすいです
【初心者向け】丁寧すぎるRails『アソシエーション』チュートリアル【幾ら何でも】【完璧にわかる】
細かい説明は省きますので、コードをつらつらと作って書いていきます
$ rails g model Relationship following_id:integer follower_id:integer $ rails db:migrate $ rails g controller relationships create destroy
# ユーザー機能 resources :users, only: [:show] do resources :relationships, only: [:create, :destroy] get :follows, on: :member get :followers, on: :member end
class Relationship < ApplicationRecord # :class_name - 関連するモデルクラス名を指定。関連名と参照先のクラス名を分けたい場合に使う belongs_to :following, class_name: "User" belongs_to :follower, class_name: "User" end
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 has_many :entries has_many :rooms, through: :entries # フォロー・フォロワーの情報を集める # has_many :relatinshipだと2通り書かなくてはならず名前被りが起こるので中間テーブル名を再定義する # =========== 自分がフォローしているユーザーとの関連 ============== has_many :active_relationships, class_name: "Relationship", foreign_key: :following_id has_many :followings, through: :active_relationships, source: :follower # =========================================================== # ============ 自分がフォローされるユーザーとの関連 =============== has_many :passive_relationships, class_name: "Relationship", foreign_key: :follower_id has_many :followers, through: :passive_relationships, source: :following # =========================================================== # Userがfollow済みかどうか判定 def followed_by?(user) passive_relationships.find_by(following_id: user.id).present? end end
class RelationshipsController < ApplicationController def create follow = current_user.active_relationships.build(follower_id: params[:user_id]) follow.save redirect_back(fallback_location: rooms_path) end def destroy follow = current_user.active_relationships.find_by(follower_id: parmas[:user_id]) follow.destroy redirect_back(fallback_location: rooms_path) end end
class UsersController < ApplicationController def show end def follows user = User.find(params[:id]) @users = user.followings end def followers user = User.find(params[:id]) @users = user.followers end end
<% if user_signed_in? %> <%= link_to "ログアウト", destroy_user_session_path, method: :delete %> <p> YourName: <%= current_user.name %> </p> <% end %> <h2>All Chats</h2> <div class="message"> <%= render partial: 'rooms/messages', locals: { messages: @messages} %> </div> <div id="add-message"></div> <!-- 投稿フォーム --> <input type="text" data-behavior="room_speaker" data-user=<%= current_user.id %>> <div class="follow-wraper"> <div class="all-user"> <h4>ユーザー 一覧</h4> <% @users.each do |user| %> <ul> <li> <%= user.name %> <% if user.followed_by?(current_user) %> <span>フォロー済み</span> <% else %> <span><%= link_to "フォローする", user_relationships_path(user.id), method: :post %></span> <% end %> </li> </ul> <% end %> </div> <div class="following-user"> <h4>フォロー中ユーザー</h4> <ul> <% @followings.each do |f| %> <li> <%= f.name %> </li> <% end %> </ul> </div> <div class="followed-user"> <h4>あなたをフォローしているユーザー</h4> <ul> <% @followers.each do |f| %> <li> <%= f.name %> <span><%= link_to "フォローする", user_relationships_path(f.id), method: :post %></span> </li> <% end %> </ul> </div> </div>
class RoomsController < ApplicationController def index @messages = Message.all # 自分以外のユーザー一覧 @users = User.where.not(id: current_user.id) # フォロー中ユーザー @followings = current_user.followings # フォローされているユーザー @followers = current_user.followers end def show end end
.follow-wraper{ display: flex; justify-content: space-between; }
ここまででフォロー・フォロワー機能ができました
room.indexのviewはこんな感じになっています
※ ユーザーを何人分か登録してあります
それではユーザーとのルームを作っていきます
ActionCableに入る前に、コントローラーとviewを作ります
ルームを作る
自分と相手がフォローし合っている状態でリンクを持つようにします
もし、ルームがまだないのであれば新規作成
もうあるのであれば、そこに飛ぶようにします
index.htmlの、フォローしているユーザーの部分でリンクを作ります
<div class="following-user"> <h4>フォロー中ユーザー</h4> <ul> <% @followings.each do |f| %> <% if current_user.followed_by?(f) %> <li> <%= f.name %> <%= link_to "チャットルームへ", room_path(f) %> </li> <% else %> <li> <%= f.name %> </li> <% end %> <% end %> </ul> </div>
ルームのshowへ遷移するリンクを作りました
次はroom.controllerで、ルームがあるかどうかの判定と、その後の動きを記述します
ちょっと難しい書き方です
記述の内容は、こちらの記事を参考にしてありますので、どういうことをしているのか知りたい方はどうぞ
RailsでややこしいDM機能を1万字でくわしく解説してみた
def show @user = User.find_by(id: params[:id]) current_room = Entry.where(user_id: current_user.id) another_room = Entry.where(user_id: @user.id) unless @user.id == current_user.id current_room.each do |cr| another_room.each do |ar| # ルームが存在する時 かつ ルームのユーザーIDが2人(グループとの差別化) if cr.room_id == ar.room_id && Room.find_by(id: cr.room_id).users.ids.size == 2 @is_room = true @room = Room.find_by(id: cr.room_id) @messages = @room.messages end end end # なければ新規作成 unless @is_room @room = Room.create Entry.create(user_id: current_user.id, room_id: @room.id) Entry.create(user_id: params[:id].to_i, room_id: @room.id) end end @messages = @room.messages end
コントローラーもできたのでviewを記述していきます
<h2><%= @user.name %> さんとのチャットルーム</h2> <% @messages.each do |message| %> <%= message.message %> <% end %> <div id="add-user-chat"></div> <input type="text" data-behavior="room_speaker">
全体チャットのフォームとあまり変わらない記述ですね
ここから個別にチャットを送るためにはルームのidが必要となって来ます
ではActionCableに入っていきます
ActionCableで特定のユーザー・ルームにチャットを送る
まずはhtmlからjsにルームのidを渡します
showページでは@roomにそのルームの情報が入るので、@room.idとすることでルームのidを取得できます
この情報を一旦showのh2タグに持たせます
また、id名を指定しておくことで、js側で要素を取得できるようにします
<h2 id="user-chat-room" data-room="<%= @room.id %>"><%= @user.name %> さんとのチャットルーム</h2>
coffee側の記述にいきます
App.cable.subscriptions.create の後にパラメータをも出せることができます
coffeeは閉じダグをインデントで判定してるようなので気をつけましょう
$ -> App.room = App.cable.subscriptions.create { channel: "RoomChannel", room: $('#user-chat-room').data('room') }, connected: -> # 通信が確立された時の処理 disconnected: -> # 通信が切断された時の処理 received: (data) -> # データが送信されてきた時の処理 $('#add-message').append("<p>" + data["message"] + "</p>"); 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()
あっと!エラーが出た!
どうやらturbolinksが生きている様子
ということでapplication.htmlから削除します
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> ↓ <%= stylesheet_link_tag 'application', media: 'all' %> <%= javascript_include_tag 'application' %>
エラーが消えました
ではchannelの方にいきます
class RoomChannel < ApplicationCable::Channel def subscribed # 接続 stream_from "room_channel_#{params['room']}" end def unsubscribed # 切断 end # coffeeから送られて来たmessage(event.target.value)を受け取る # inputタグの文字がメッセージカラムに保存される def speak(message) Message.create(message: message['message'], user_id: message['user'].to_i, room_id: params['room']) end end
実際のparamsの値はこんな感じ
無事channelにパラメータを渡すことができました
このidの情報でどのメッセージがどのチャンネルに配信されるかを特定します
次は保存後の動きです
前回作ったアプリでは、inputに入力した情報をそのまま送っていました
今回はjobというものを通してDBに保存したメッセージを送るようにしていきます
modelに、メッセージの保存に成功した後jobにデータを渡すように記述します
class Message < ApplicationRecord # メッセージの保存ができた時、jobにデータを渡す after_create_commit { MessageBroadcastJob.perform_later self} belongs_to :user belongs_to :room, optional: true end
channelでmodelを通した時に、jobにデータを渡します
保存されたmessageはselfとしてjobに渡る...はず
jobはターミナルから作成します
$ rails g job MessageBroadcast
後からappendでタグを直書きしていくのも大変なので、データを基に部分テンプレートをレンダリングするようにします
jobから部分テンプレートのhtmlをjsに返します
まずはjob
class MessageBroadcastJob < ApplicationJob queue_as :default def perform(message) ActionCable.server.broadcast "room_channel_#{message.room_id}", message: render_message(message) end private def render_message(message) ApplicationController.renderer.render partial: 'rooms/user_message', locals: { message: message} end end
ApplicationController.renderer.render は、コントローラ以外の場所でレンダリングする時の記述らしいです
送られて来たmessageのデータを、同じルームidのチャンネルへ配信します
次にrooms/_user_message.html.erb を呼び出し
<p><%= message.message %></p>
とりあえず付け足すだけなのでこれだけにしておきます
js側に送られた部分テンプレートをviewに付け足す記述をします
coffee側へ
received: (data) -> # データが送信されてきた時の処理 $('#add-message').append data['message']
これでとりあえず完成です!
前回とはjob等の変更点もありますが、出来上がりました
実際の挙動はこんな感じ
ユーザー同士のチャットは全体には上がらず、全体のチャットはユーザー同士の方には上がりません
いい感じですね!
あとはhtmlやcssでいい感じにしていけばOKです
ただ今のままだと、リロードをかけると全体のチャットルームにユーザー同士のチャットが反映されてしまいますね
とりあえず、room_idのないメッセージは表示しない、みたいにしておきます
def index @messages = Message.where(room_id: nil) :
これでユーザー間のチャット機能はできるようになりましたね
これを応用すればグループ機能も作れるし、画像なんかのファイルを送ることもできます
デプロイ後の挙動も確認したいところです
次回はまたActionCable関係を更新していこうと思います!
【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を使用してリアルタイムチャットの実装
ありがとうございます!
【jQuery】要素を最上部に移動させる
今回はjsです
jsを知れば知るほどjQueryって便利なんだなぁとつくづく感じている所です
本題!
要素を最上部に移動させる
たった1行で実行できました
javascripts/channels/talk_room $('.talk__friends--list').prepend($('.talk-room-' + data["talk_room_id"]));
ざっくり書くと
$('入れ物').prepend('追加したい要素')
で、追加したい要素を入れ物の一番上に表示させる感じです
もし、すでに追加したい要素がある場合は、すでに要素は消えて上部に追加されます
(removeとか使ってしまって永遠に消えたりしてました...)
実際の動きはこんな感じです
名前がdddの人とのトークが一番上になりました
ついでに、一番新しいトーク内容を表示させて、時間も一緒に出します
# トークの追加があったトークルームを最上段に表示させる $('.talk__friends--list').prepend($('.talk-room-' + data["talk_room_id"])); # 内容を書き換える $('.talk-room-' + data["talk_room_id"]).find( $('#latest-talk').html('<span class="latest-talk-show__name-display--display">'+ data["talk"]+ '</span>') ) # 時間も書き換える $('#data-time').html('<span id="data-time">'+ data["created_at"]+ '</span>')
ActionCableでchannelのreceived: 部分に書いてあるので、受け取った相手も同じ動きをしてくれます
あとは未読メッセージでカウントを表示とかさせればよりわかりやすくなりますね
またボチボチ更新していきます