Railsを使ってチャットアプリを作る

#はじめに
Railsを使って簡単なチャットアプリを作ったので、そのときの手順をまとめました。

![chat-app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371652/c0946340-a331-a933-80d5-9740a67605d1.gif)

#環境
```:
$ ruby -v
ruby 2.5.1p57 (2018-03-29 version 63029) [x86_64-darwin19]

$ bin/rails -v
Rails 5.2.3
```

#参考
チャットアプリを作る際、以下を参考にさせて頂きました。
この記事で掲載しているソースコードの大半は下記の2つのページの内容が基になっています。

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

#ソースコード
チャットアプリのソースコードは以下で確認できます。
[Neufo/rails-sample-app/ChatApp](https://github.com/Neufo/rails-sample-app/tree/master/ChatApp)

#手順
## 1. アプリケーションを作る
まずはチャット機能を実装するアプリケーションを作ります。

```:
$ rails new ChatApp
```

## 2. コントローラーを作る
ChatAppディレクトリに移動して以下のコマンドを実行します。

```:
$ bin/rails generate controller rooms show
```
これで```ChatApp/app/controllers/```に```rooms_controller.rb```が作られます。

## 3. モデルを作る
ユーザーが入力したメッセージを保存するモデルを作ります。

```:
$ bin/rails generate model message content:text
$ bin/rails db:migrate
```
## 4. メッセージの一覧を表示する
RoomsControllerのshowメソッドでメッセージの一覧を取得します。

```ruby:ChatApp/app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def show
    @messages = Message.all
  end
end
```
取得したメッセージを表示するためにViewを編集します。[^注1]

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

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

messagesの各要素をページに埋め込むために部分テンプレートを作ります。
(ChatApp/app/views/ に messagesフォルダを作り、その中にファイルを保存します)

```erb:ChatApp/app/views/messages/_message.html.erb
<div class="message">
  <p><%= message.content %></p>
</div>
```

## 5. チャンネルを作る
クライアント/サーバ間の通信処理のひな形を作ります。

```:
$ bin/rails generate channel room speak
```
コマンドを実行するとクライアント用、サーバ用のファイルが作られます。

- ChatApp/app/assets/javascripts/channels/room.coffee (クライアント)
- ChatApp/app/channels/room_channel.rb (サーバ)

サーバを起動して動作を確認してみます。

```:
$ bin/rails s
```

ブラウザで```/rooms/show```を開き、コンソールで```App.room.speak()```と入力します。trueが返ってくれば成功です。
<img width="398" alt="スクリーンショット 2019-11-24 22.53.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371652/ede06795-e0ca-6d51-5a28-ebb447467f29.png">

## 6. jQueryを有効にする
クライアントがメッセージを送受信するときにjQueryを使うので準備しておきます。

```text:ChatApp/Gemfile
gem 'jquery-rails'
```
上記をGemfileに追記したら```bundle update```を実行します。

jQueryのライブラリを読み込むようapplication.jsに追記します。

```javascript:ChatApp/app/assets/javascripts/application.js
(省略)
//= require jquery
//= require jquery_ujs
//= require_tree .
```

## 7. メッセージの送受信処理を作る
以下の機能を作ります。

- クライアント側
 - サーバからメッセージを受信する (received)
 - ユーザが入力したメッセージをサーバに送信する (speak)

```erb:ChatApp/app/views/rooms/show.html.erb
<form>
  <label>Say something:</label><br>
  <!-- room_speaker からサーバへメッセージを送信する -->
  <input type="text" data-behavior="room_speaker">
</form>
```

```coffeescript:ChatApp/app/assets/javascripts/channels/room.coffee
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
    # Enterキーが押されたらサーバへメッセージを送信する
    if event.keyCode is 13 # return = send
        App.room.speak event.target.value
        event.target.value = ''
        event.preventDefault()

App.room = App.cable.subscriptions.create "RoomChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # メッセージ一覧の末尾に受信したメッセージを追加する
    $('#messages').append data['message']

  speak: (message) ->
    # サーバのspeakメソッドを呼び出す
    @perform 'speak', message: message
```

- サーバ側
 - room_channelを購読する (subscribed)
 - クライアントから受信したメッセージを保存する (speak)
 - room_channelに参加しているクライアントにメッセージをブロードキャストする (perform)

```ruby:ChatApp/app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    # room_channelからデータを受信する
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    # データベースにクライアントから受信したメッセージを保存する
    Message.create! content: data['message']
  end
end
```
メッセージをブロードキャストするジョブを定義します。

```:
$ rails generate job MessageBroadcast
```

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

  def perform(message)
    # room_channelにメッセージをブロードキャストする
    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
```

データベースにメッセージを保存したら、ジョブを登録します。

```ruby:ChatApp/app/models/message.rb
class Message < ApplicationRecord
    # データベースにメッセージが保存されたらジョブを登録する
    after_create_commit { MessageBroadcastJob.perform_later self }
end
```
登録されたジョブは自動的に実行されます。
MessageBroadcastJobのperformメソッドが呼び出され、すべてのクライアントにメッセージが送信されます。
クライアントはreceivedでメッセージを受け取り、メッセージ一覧の末尾に受信したメッセージを追加します。
これでチャットアプリの作成は終了です。

[^注1]: ```<%= render @messages %>```は```<%= render partial: "messages/message", collection: @messages %>```を省略形です。参考：[コレクションをレンダリングする](https://railsguides.jp/layouts_and_rendering.html#%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B)