はしばみあきら blog

プログラミングアウトプットするブログ。202010スタート

【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 です

f:id:hashibamiakira:20201123004704p:plain

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を一旦削除してみました

するととりあえず追加したcssとjsのプリコンパイルは成功

ということはやはり画像表示で何かおかしくなってるな?

やってみたこと

  • 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としておきます

なお使用できるのは

の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からダウンロードできます

github.com

今回はフォロー機能も実装し、相互フォロー状態のユーザー同士でチャットできるルームを作るなどしていきます

ではいきましょう

フォロー機能を作る

フォローフォロワー機能についてはこちらがとても分かりやすいです

【初心者向け】丁寧すぎる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はこんな感じになっています
※ ユーザーを何人分か登録してあります

f:id:hashibamiakira:20201112160159p:plain

それではユーザーとのルームを作っていきます

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の値はこんな感じ

f:id:hashibamiakira:20201112174036p:plain

無事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ってなんぞや?

めちゃくちゃ詳しく調べ上げた訳でない上に言葉にするのが難しいですが、ざっくり言うと「ユーザー側とサーバー側が常に通信状態だよ」って感じです

ブラウザのページは基本的にリクエストを送りサーバー側と通信しデーターを取得します

そのリクエストによってページが切り替わり、サーバーのデーターによって表示が変わります

f:id:hashibamiakira:20201111145224p:plain

この状態のままだと更新をかけるまではページは最新のものになりません

ActionCableはほぼ常にサーバーの情報を取得します

サーバーにデータ送り、保存された後そのデータを、ActionCableを通して接続されているユーザーのviewに送り返します

ここで関わってくるのは

  • view (html)
  • javascript (coffeeがデフォルト)
  • ActionCable (railsの機能)
    になります

自分は文字で書くと頭に入らないので図で表してみます

f:id:hashibamiakira:20201111153512p:plain

JSに渡ったデータがActionCableに送られ、保存するなどの処理を行った後にJSにデータを返します
返されたデータはJSによってviewを書き換える処理を行います

この一連の流れを作り上げていきます

では作っていきましょう

環境

開発環境 - Virtualbox - Vagrant

railsのバージョンが5と6では書き方が変わるようです
今回5で行っています

ER図

f:id:hashibamiakira:20201111154144p:plain

今回はとりあえず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の書き方を使います

そのため、railsjQueryを使えるように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にフォーカスしてエンターを押してみましょう

f:id:hashibamiakira:20201112115709p:plain

このようにアラートが表示されれば、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があります

f:id:hashibamiakira:20201112120740p:plain

今は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

実際にターミナルで値がどのようになっているか見てみましょう

f:id:hashibamiakira:20201112123500p:plain

(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を使用してリアルタイムチャットの実装

ありがとうございます!

計画頓挫。。。

RailsでLINEを作ってたら突然今まで動いていた挙動が全く動かなくなりました...

githubさかのぼって動かしてみても動く気配無し...

とりあえず切り替えて、一旦デプロイするとこまでしてみようと思います

ではでは

【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: 部分に書いてあるので、受け取った相手も同じ動きをしてくれます

あとは未読メッセージでカウントを表示とかさせればよりわかりやすくなりますね

またボチボチ更新していきます