始めに
Websocketとは
ウェブブラウザとサーバー間で双方向のリアルタイム通信を可能にする技術。通常のHTTPリクエストと異なり、一度コネクション(※)を確立すると、そのコネクションを維持したままデータをやり取りできる
※コネクションとは、クライアント(通常はウェブブラウザ)とサーバー間で確立された通信路やセッションを指す。
通常のHTTPリクエスト
クライアントがサーバーにリクエストを送り、サーバーがレスポンスを返すという単発のやりとり。
Action Cableとは
RubyonRailsフレームワークに統合された、WebSocketベースの双方向通信ライブラリ。これにより、Railsアプリケーション内でのリアルタイムな双方向通信が簡単に実現できる。
コマンド実行後作成されるファイル
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
実装
投稿の内容ごとにチャンネルを作成してみました。
一部を抜粋したものです。細かい設定は参考URLにわかりやすく記述されています。
投稿内容からチャンネルへ遷移
# <省略>
<%= 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 %>
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>
posts_controller.rb
# <省略>
def channel
@room = Post.find(params[:id])
@messages = @room.messages
end
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();
}
});
});