ActionCableでリアルタイム通信の実装

## 始めに

### Websocketとは
ウェブブラウザとサーバー間で双方向のリアルタイム通信を可能にする技術。通常のHTTPリクエストと異なり、一度コネクション(※)を確立すると、そのコネクションを維持したままデータをやり取りできる

※**コネクション**とは、クライアント（通常はウェブブラウザ）とサーバー間で確立された通信路やセッションを指す。

**通常のHTTPリクエスト**
クライアントがサーバーにリクエストを送り、サーバーがレスポンスを返すという単発のやりとり。




### Action Cableとは
RubyonRailsフレームワークに統合された、WebSocketベースの双方向通信ライブラリ。これにより、Railsアプリケーション内でのリアルタイムな双方向通信が簡単に実現できる。

**イメージ図**
[![Image from Gyazo](https://i.gyazo.com/c03bf190f3475eecef700e2e8aa328b1.png)](https://gyazo.com/c03bf190f3475eecef700e2e8aa328b1)
```ruby:コマンド実行後作成されるファイル
rails g channel room #チャネル名
# 実行結果
Running via Spring preloader in process *****
      invoke  test_unit
      create  test/channels/room_channel_test.rb
      create  app/channels/room_channel.rb    #サーバー側の処理
   identical  app/javascript/channels/index.js
   identical  app/javascript/channels/consumer.js
      create  app/javascript/channels/room_channel.js  #クライアント側の処理
```


設定の仕方はいろいろ記事がありました
ありがとうございます。
### 参考
https://techtechmedia.com/action-cable-rails6/
https://qiita.com/rhiroe/items/4c4e983e34a44c5ace27
***
### 実装
投稿の内容ごとにチャンネルを作成してみました。

[![Image from Gyazo](https://i.gyazo.com/cefcb0a92d978e819b150dc2c449a099.gif)](https://gyazo.com/cefcb0a92d978e819b150dc2c449a099)
[![Image from Gyazo](https://i.gyazo.com/4db194b3b78de932460ffede0aeaf7e9.gif)](https://gyazo.com/4db194b3b78de932460ffede0aeaf7e9)


一部を抜粋したものです。細かい設定は参考URLにわかりやすく記述されています。

```ruby:投稿内容からチャンネルへ遷移
# <省略>
<%= link_to channel_post_path(post), data: { turbo: false }, class: "block h-full p-6 flex items-center gap-x-3" do %>
  <span class="font-semibold text-brack dark:text-gray-200">
      <%= post.content %>    
  </span>       
 <% end %>
 ```

 ```ruby:posts/cahnnel.html.erb
<div class="channel bg-green-50" style="background-image: url(<%= asset_path('channel_04.png') %>)">
    <div class="flex justify-center">
        <form class="mt-6">
            <%= label_tag :content, '', class: 'block text-gray-700' %>
            <input type="text" data-behavior="room_speaker" class="py-2 px-3 border border-gray-300 rounded-lg shadow-sm focus:ring focus:ring-blue-200 focus:outline-none" placeholder="メッセージ入力">
        </form>
    </div>
    <div class="flex-1 p:2 sm:p-6 justify-between flex flex-col h-screen">
        <div id='messages' data-room_id="<%= @room.id %>" class="flex flex-col space-y-4 p-3 overflow-y-auto scrollbar-thumb-blue scrollbar-thumb-rounded scrollbar-track-blue-lighter scrollbar-w-2 scrolling-touch">
            <%= render @messages %>
        </div>
    </div> 
</div>
```
```ruby: posts_controller.rb
# <省略>
def channel
    @room = Post.find(params[:id])
    @messages = @room.messages
  end
```

```ruby: channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_#{params['room_id']}" #引数として特定のチャンネル名を指定.これにより、クライアントがこのチャンネルにサブスクライブ（購読）すると、そのクライアントはこのチャンネルに関連付けられたストリームを受け取ることができる
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    Message.create!(content: data['message'], user_id: current_user.id, post_id: data['room_id'], created_at: Time.now)
  end

end
```

```javascript/room_channel.js
import consumer from "./consumer"
import $ from 'jquery';

$(function(){
	const chatChannel = consumer.subscriptions.create({ channel: 'RoomChannel', room_id: $('#messages').data('room_id') }, {
		connected() {
		},

		disconnected() {
			// Called when the subscription has been terminated by the server
		},

		received(data) {
			const messagesContainer = $('#messages');
			messagesContainer.prepend(data['message']);
		},

		speak: function(message) {
			return this.perform('speak', {message: message, room_id: $('#messages').data('room_id')});
		}
	});

	$(document).on('keypress', '[data-behavior~=room_speaker]', function(event) {
		if (event.key === 'Enter') {
			chatChannel.speak(event.target.value);
			event.target.value = '';
			return event.preventDefault();
		}
	});
});
```