Rails 5とActionCableでチャットアプリ 初心者制作

##環境
ruby '2.6.3'
'rails', '~> 5.2.3'

##参考
[Rails 5 + ActionCableで作る！シンプルなチャットアプリ（DHH氏のデモ動画より）](http://https://qiita.com/jnchito/items/aec75fab42804287d71b)
[Rails 5+ ActionCableで作る！ シンプルなチャットアプリ　ハマった所](http://https://qiita.com/tacyan/items/be4f3e43c9914ff73c61)
[Action Cableでリアルタイムチャットアプリの作成方法 （Rails 5.1.4にて）（その１）　herokuで動かす！](http://https://qiita.com/Hijiri-K/items/c3774c72a2cb68e1a720)

#アプリケーション作成手順
##1.アプリ作成
chat_roomという名前のアプリを作成。

```
$ rails new chat_room
```
```
$ cd chat_room
```
Gemfile に追加。

```
gem 'jquery-rails'
gem 'jquery-turbolinks'
```
```
$ bundle install
```
application.jsファイルに追加

```app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
```

この順になれば良い

```
//
//= require rails-ujs
//= require activestorage
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .
```
##2.roomsコントローラー作成
showアクションをもつroomsコントローラーを作成。

```
$ rails g controller rooms show
```

`get 'rooms/show'`を`root to: 'rooms#show'`に変更します。

```config/routes.rb
Rails.application.routes.draw do
  root to: 'rooms#show'

  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html

  # Serve websocket cable requests in-process
  # mount ActionCable.server => '/cable'
end
```
サーバーを起動

```
$ rails s
```
[localhost:3000](http://localhost:3000)でアクセスできるようになった。

##3.Messageモデル作成
contentという属性を持つ、Messageクラスを作成。

```
$ rails g model message content:text
```
```
$ rails db:migrate
```

##4.controllerとview作成・編集
roomsのコントローラーのshowアクションを編集し、@messages変数に全ての投稿を代入する。

`@messages = Message.all`を追加。

```app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def show
    @messages = Message.all
  end
end
```
`app/views/messages`viewsにmessagesというフォルダーを作成。
`app/views/messages/_message.html.erb`そのmessagesに_message.html.erbというファイルを作成。

controllerで代入した@messagesを表示。全て書き換える。

```app/views/rooms/show.html.erb
<h1>Chat room</h1>

<div id="messages">
  <%= render @messages %>
</div>
```
投稿を1件だけ出力

```app/views/messages/_message.html.erb
<div class="message">
  <p><%= message.content %></p>
</div>
```
Action Cableを有効にするため、`mount ActionCable.server => '/cable'`を追加。

```config/routes.rb
Rails.application.routes.draw do
  root to: 'rooms#show'
  mount ActionCable.server => '/cable'
end
```

##5.Roomチャンネル作成
speakアクションを持つRoomチャンネルを作成。
`app/channels/room_channel.rb`（サーバーサイド用）
`app/assets/javascripts/channels/room.coffee`
（クライアントサイド用）
という2つのファイルが作られる。

```
$ rails g channel room speak
```
サーバーを起動

```
$ rails s
```
ブラウザーのコンソール（デベロッパーツールからconsoleを開く）に`App.room.speak()`を入力し、trueが返ってくれば成功。
![aa.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469488/bc547088-aa49-d815-d8e9-7247579d5539.png)

##6.speakアクション
クライアントサイドのspeakアクションを定義する。サーバーサイドのspeakアクションを呼びだし、messageをパラメータとして渡す。
`speak: ->
    @perform 'speak'`を
`speak: (message) ->
    @perform 'speak', message: message`
と書き換える。

```app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
 connected: ->

 disconnected: ->

 received: (data) ->


  speak: (message) ->
    @perform 'speak', message: message
```
サーバーサイドのspeakアクションを定義します。


`def subscribed
    # stream_from "some_channel"
  end`から`#`を削除し、`some_channel`を`room_channel`に書き換える。

`def speak
  end`を
` def speak(data)
    ActionCable.server.broadcast 'room_channel', message: data['message']
  end`に書き換える。



```app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
  end

  def speak(data)
    ActionCable.server.broadcast 'room_channel', message: data['message']
  end
end
```

サーバーからデータを受け取ったときの動きを定義します。


receivedのところに`alert data['message']`を追加

```app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  connected: ->
    
  disconnected: ->
    
  received: (data) ->
    alert data['message']

speak: (message) ->
    @perform 'speak', message: message
```

サーバーを再起動します。

```
$ rails s
```

ブラウザのコンソールから `App.room.speak('Hello world')` を入力し、trueが返ってくれば成功。
![bb.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469488/2b710a2b-3116-4f7d-a467-cf4bd1623937.png)

ブラウザ内のフォームからデータを送信できるようにする。

フォーム`<form>
  <label>Say something:</label><br>
  <input type="text" data-behavior="room_speaker">
</form>`を追加。

```app/views/rooms/show.html.erb
<h1>Chat room</h1>

<div id="messages">
  <%= render @messages %>
</div>

<form>
  <label>Say something:</label><br>
  <input type="text" data-behavior="room_speaker">
</form>
```

テキストボックスのkeypressイベントを定義します。
リターンキーが押されたときに、Roomチャンネルのspeakアクションを実行。

```app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  # (省略)

$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  if event.keyCode is 13 # return = send
    App.room.speak event.target.value
    event.target.value = ''
    event.preventDefault()
```

ブラウザをリロードすると、テキストボックスが現れる。
![cc.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469488/98b9aa23-89b1-0285-1b2b-35f609f8f73e.png)


##データの保存とブロードキャスト処理の改善
このままではmessageが保存されないので、データベースに保存。
`ActionCable.server.broadcast 'room_channel', message: data['message']`を`Message.create! content: data['message']`に書き換える。

```app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "some_channel"
  end

  def unsubscribed
    
  end

  def speak(data)
    Message.create! content: data['message']
  end
end
```

Messageモデルのコールバックを定義し、データが作成されたら非同期でブロードキャスト処理を実行する。

トランザクションをコミットしたあとでブロードキャストしないと、他のクライアントからデータが見えない恐れがある。

```app/models/message.rb
class Message < ApplicationRecord
  after_create_commit { MessageBroadcastJob.perform_later self }
end
```

非同期でブロードキャストするためのMessageBroadcastジョブを作成

```
$ rails g job MessageBroadcast
```
`def perform(*args)
    # Do something later
  end`を
`def perform(message)
    ActionCable.server.broadcast 'room_channel', message: render_message(message)
  end
  private
    def render_message(message)
      ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
    end`に書き換える。

```app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast 'room_channel', message: render_message(message)
  end

  private
    def render_message(message)
      ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
    end
end
```

##サーバーからデータを受け取ったらブラウザ内の表示を書き換える

サーバーからデータを受け取ったら、ブラウザ内の表示を更新する。
入力したものが更新なしで画面に反映される。同時にデータベースに登録される。

`received: (data) ->
alert data['message']`を
`received: (data) ->
 $('#messages').append data['message']`に書き換える。

```app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  connected: ->
    
  disconnected: ->
    
  received: (data) ->
     $('#messages').append data['message']
    
  speak: (message) ->
    @perform 'speak', message: message

$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  if event.keyCode is 13 # return = send
    App.room.speak event.target.value
    event.target.value = ''
    event.preventDefault()

```
ブラウザをリロードし、文字列を入力。


リターンキーを押すと、ブラウザの表示が更新。


これでチャットアプリの完成。