はじめに
前回の記事で記載した、turbo streamを用いたコメント投稿にAction cableを導入しました。
あくまでturbo streamを導入している前提で記載しているので、Action cableのみ導入したいよ!という方はご注意ください。
開発環境
- ruby 3.2.0
- Rails 7.0.7.2
- MySQL
Action cableとは
railsでリアルタイム配信機能実装できるフレームワークのこと。
Action cableを用いることで、チャットやSNSのようにリアルタイムでメッセージのやり取りをすることができる。
用語
-
チャネル
チャネルとは、リアルタイム通信におけるサーバのようなもの。
サーバーとクライアントを円滑に繋ぐための部屋のようなものだとイメージするとよい。
1つのアプリケーションに対して、複数のチャネルを持たせることが可能。 -
サブスクリプション
チャネルを購読すること。 -
コンシューマー
チャネルからデータを受け取る側のこと。
コンシューマーはチャネルをサブスクリプション(購読)することで、メッセージ(データ)を受け取ることができる。 -
ブロードキャスト
サーバーからクライアント側へデータを送信すること。
実装手順
1.チャンネルの作成
以下のコマンドでチャンネルを作成する。
rails g channel comments
コマンド実行後はチャネルであるcomment_channel.rb
と関連するファイルが生成される。
comment_channel.rb
サーバーとクライアントを繋ぐためのチャネルを設定するファイル。
2.コントローラーにブロードキャストの記述を追記。
broadcast_prepend_later_to
を使用して、非同期的にHTMLを追加する。
先ほど作成した、comment_channelを経由してブロードキャストするように指定。
class CommentsController < ApplicationController
before_action :authenticate_user!
before_action :set_plan, only: [:index, :create, :edit, :update, :destroy]
before_action :set_comment, only: [:edit, :update, :destroy]
def create
@comment = Comment.new(comment_params)
respond_to do |format|
if @comment.save
reset_form
#comment_channelを経由して、@commentのデータをブロードキャストする。
@comment.broadcast_prepend_later_to("comment_channel")
format.html { redirect_to plan_comments_path }
format.turbo_stream
else
format.html { render :index, status: :unprocessable_entity }
end
end
end
3.ビューの修正
①create.turbo_stream.erb
ブロードキャストでコメントの内容を受け取るようにしたので、create.turbo_stream.erb
内でコメント内容の差し替えは不要になる。そのため入力フォームの更新のみ行えばよいので、以下の記述は削除する。
<%= turbo_stream.replace "new_comment" do %>
<%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>
<!-- 追記:以下の記述を削除-- >
<%#= turbo_stream.prepend "comments" do %>
<%#= render @comments %>
<%# end %>
②comments/index.html.erb
<div class="text-center mx-auto py-6 sm:py-8 lg:py-12 w-1/2">
<%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<div id="comments">
<%= render @comments %>
</div>
</div>
<!-- 追記:comments_channel経由でデータを受け取る記述を追加。-- >
<%= turbo_stream_from "comments_channel" %>
ここまでの実装でリアルタイムコメント追加ができました!
しかし、現在の実装だと1つ問題があります。それは別のplanに対してもコメントが非同期で更新されてしまうという点です。
動画の左はidが14、右はidが13の投稿ですが、plan14で投稿したコメント内容がplan13の投稿欄にも反映してしまっています。
これは、1つのチャネルを全ブラウザで共通して使用しているためです。
1つのチャネルを使用している場合、全部のコメントが同一チャネルを通じて画面に表示されてしまいます。
次の実装で、関連のないコメントは表示されないように各planごとにチャネルを作成していきます。
個別チャネルの実装
1.comments_chanel.rbで一意のidを割り振る。
comment_chanel.rb内でコメントを追記したいplanのid情報を取得し、channelの末尾に一意のidを割り振ることで、plan毎にチャネルを分けることができる。
class CommentsChannel < ApplicationCable::Channel
def subscribed
#以下の記述を追加
@plan = Plan.find(params[:plan_id])
stream_from "comments_channel_#{@plan.id}"
end
end
2.コントローラー内で一意のチャネルを指定する。
def create
@comment = Comment.new(comment_params)
respond_to do |format|
# binding.pry
if @comment.save
#こちらの記述に追記。末尾にチャネルの末尾に@planを渡して、一意のチャネルにブロードキャストするように指定。
@comment.broadcast_prepend_later_to("comments_channel_#{@plan}")
reset_form
format.html { redirect_to plan_comments_path }
format.turbo_stream
else
format.html { render :index, status: :unprocessable_entity }
end
end
end
3.ビュー内で一意のチャネル指定
ビューでも同じく、turbo_stream_from
で連動しているチャネルを変更する。
<div class="text-center mx-auto py-6 sm:py-8 lg:py-12 w-1/2">
<%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<div id="comments">
<%= render @comments %>
</div>
</div>
<!-- 以下の記述を編集。同じく@planの値を渡すことで一意のチャネルとの連動を作る -->
<%= turbo_stream_from "comments_channel_#{@plan.id}" %>
ここまでの実装で、関連のないコメントは反映されないように実装ができました!
編集・削除機能の実装
1. コントローラー内の編集
updateアクションと、destroyアクション内の記述を変更。
updateの場合は、編集した内容に更新をかけたいので、broadcast_update_later_to
を使用して、channel経由でデータを返す。
destroyの場合は、投稿を削除したいので、broadcast_update_later_to
を使用する。
~省略~
def update
respond_to do |format|
if @comment.update(comment_params)
@comment.broadcast_update_later_to("comments_channel_#{@plan.id}")
reset_form
format.html { redirect_to plan_comments_path }
format.turbo_stream
else
format.html { render :index, status: :unprocessable_entity }
end
end
end
def destroy
respond_to do |format|
if @comment.destroy
# 追記:broadcast_remove_toを使用して、削除した投稿を返す。
@comment.broadcast_remove_to("comments_channel_#{@plan.id}")
reset_form
format.html { redirect_to plan_comments_path }
format.turbo_stream
else
format.html { render :index, status: :unprocessable_entity }
end
end
end
2. ビューの編集
update.turbo_stream.erb
とdestroy.turbo_stream.erb
両方から、以下の記述を削除。
index.html.erb内でchannelを通してデータを受け取るようにする。
<%= turbo_stream.replace "new_comment" do %>
<%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>
<!-- 以下の記述を削除する-->
<%= turbo_stream.replace "comments" do %>
<%= render @comments %>
<% end %>
<%= turbo_stream.replace "new_comment" do %>
<%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>
<!-- 以下の記述を削除する-->
<%= turbo_stream.update "comments" do %>
<%= render @comments %>
<% end %>
ここまで実装ができたら、編集・削除の実装も完了です。