[Rails]ActionCableでリアルタイム機能を実現してみた

# はじめに
Action Cableは、 WebSocketとRailsのその他の部分をシームレスに統合するためのものです。ちなみに、WebSocketとは、Webにおいての双方向通信を、従来のHTTP等よりも低コストで行うための仕組み。

# 事前知識
## コンシューマー
WebSocketコネクションのクライアント。複数のケーブルチャネルにサブスクライブできます。

## Pub/Sub
パブリッシャ-サブスクライバ（pub/sub）型モデルとも呼ばれる、メッセージキューのパラダイムです。`パブリッシャ側（Publisher）が、サブスクライバ側（Subscriber）の抽象クラスに情報を送信`します。 

## 購読(subscription)
クライアントがWebsocket通信を通じたチャネルと関連付け。購読は、一度に複数のチャネルに対して作成可能。

## app/channels/〇〇_channel.rb
Websocket処理におけるコントローラーのような役割を果たす。

```ruby
class RoomChannel < ApplicationCable::Channel
  # 購読後に呼ばれるメソッド
  def subscribed
  end

  # 購読解除後に呼ばれるメソッド
  def unsubscribed
  end

  # クライアントから呼び出された時に実行されるメソッド
  def speak
  end
end
```

## app/javascript/channels/〇〇.js
クライアントサイドを受け持つファイル。

```js
import consumer from "./consumer"

consumer.subscriptions.create("RoomChannel", {
  // 接続時の処理
  connected() {
  },

  // 切断時の処理
  disconnected() {
  },

  // サーバーからデータを受信した際の処理
  received(data) {
  },

  // 購読しているチェネルのspeakメソッドをWebsocket通信経由で呼び出し
  speak: function() {
    return this.perform('speak');
  }
});
```

## ブロードキャスト
接続している全購読者への送信。

# 利用方法
ActionCableを用いてチャットアプリを作成してみる。

1.チャットルーム画面のコントローラーを作成する。

```bash
# 今回は単一のチャットルームのためshowのみ作成
$ rails g controller rooms show
```

2.チャットメッセージを格納するモデルを作成する。

```bash
$ rails g model message content:text

$ rails db:migrate
```

3.ビュー側の実装をする。

```app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def show
    @messages = Message.all
  end
end
```

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

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

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

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

4.ActionCable用のファイルを生成する。

```bash
$ rails g channel room speak
```

5.サーバーサイド側の処理を実装する。

```app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    # ブロードキャスト用のストリーム名を設定
    stream_from "room_channel"
  end

  def unsubscribed
  end

  def speak
    # クライアントがメッセージを送信した時に実行される処理
    message = Message.create!(content: data["message"])
    ActionCable.server.broadcast(
      "room_channel", { message: render_message(message) }
    )
  end

  private

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

6.クライアント側の処理を実装する。

```js
import consumer from "./consumer"

consumer.subscriptions.create("RoomChannel", {
  connected() {
    document,
      querySelector('input[data-behavior="room_speaker"]'),
      addEventListener('keypress', (event) => { // メッセージが入力された時にイベント発火
        if (event.key === 'Enter') { // Enterキーを押下時、speakメソッドを呼び出す
          this.speak(event.target.value);
          event.target.value = '';
          return event.preventDefault();
        }
      });
  },

  disconnected() {
  },

  received(data) {
    // 受け取ったメッセージをHTML上に追加
    const element = document.querySelector('#messages')
    element.insertAdjacentHTML('beforeend', data['message'])
  },

  speak: function(message) {
    return this.perform('speak', { message: message });
  }
});
```

## Tips
### アダプタの設定
Websocketの接続管理を行うアダプターと、接続先を指定する。

```config/cable.yml
development:
  adapter: async

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: actioncable_app_production
```

### 接続可能なオリジンを設定する
オリジンは`URIスキーム・ホスト・ポート番号`を組み合わせたものです。development環境で別のドメインを利用する場合やproduction環境では明示的に設定する必要がある。

```ruby
...
config.action_cable.allowed_request_origins = [
  'http://example.com', /http:\/\/example.*/
]
...
```

### ワーカー数を指定
デフォルトでは1プロセスにつき最大4つのスレッドが使われる設定となっています。(環境ごとに設定してもOK)
※`config/database.yml`のpoolの値に、ActionCableのスレッド数とWeb用のスレッド数を足した数と同じもしくはそれ以上の値を設定しておく。

```config/application.rb
...
config.action_cable.worker_pool_size = 10
...
```

# 終わりに
少し自分的にはややこしいのですが、LINEやSlackなどのようなチャット機能を実装したい場合は、導入を検討してみても良いですね！

# 参考
[Action Cable の概要]
(https://railsguides.jp/action_cable_overview.html)

[【Rails】Action Cableについてまとめてみた]
(https://qiita.com/yokoto/items/fb1e66844b70f52682dd)