🦓

[Rails]turbo_streamでコメントをBroadcastする

2023/09/02に公開

はじめに

turbo_streamを使って、投稿に新しいコメントを追加されるとき、リアルタイムなDOMの更新を行ってコメントをページに追加します。
turbo_stream.prependでコメントしたユーザーのブラウザに更新を行いますが、Broadcastも効かせて全てのユーザーのブラウザに表示されるようにしたいです。

Turbo Stream

Turbo Streamは、サーバーサイドで生成されたデータの変更をクライアントに伝え、それに基づいてクライアント上で動的な更新を実現するための仕組みです。

Broadcast

BroadcastはイベントベースでTurbo Streamsを生成および送信する方法を提供します。
アプリ内で生成されたイベントをリッスンし、それに応じてTurbo Streamsを送信します。

Broadcastの基本的なフローは次のようになります:

  1. サーバーサイドでデータが変更される(例:新しいコメントが投稿される)と、Turbo Streamsを使用してその変更がストリームに含まれるHTMLフラグメントとして生成されます。
  2. サーバーはWebSocketを介してクライアントにTurbo Streamsメッセージをプッシュします。
  3. クライアントはTurbo Streamsメッセージを受信し、Turbo Streams内のHTMLフラグメントをクライアント上の指定されたTurbo Frame要素内更新を行います。

環境

Rails 7.0.7
ruby 3.2.1
Redis

流れ

1. コメントの更新用streamを作成する
2. コメントの更新用broadcastメソッドを作成する

コメント用turbo_stream_fromを作成する

コメントを表示させたいビュー(例:投稿の詳細ページ)にturbo_stream_fromを作成します。
turbo_stream_fromは、Turbo Streamsの更新を監視し、その変更に対して自動的にリアルタイムで再レンダリングを行うためのRailsヘルパーメソッドです。

名前が"comments"のTurbo Streamsを生成し、クライアントがそのストリームからのメッセージを受信することになります。

app/views/posts/show.html.erb
<%= turbo_stream_from "comments" %>

Redisを立ち上げて、Turbo Streamを追加されたことを確認します。

コメント一覧パーシャル
app/views/comments/_comments.html.erb
<%= turbo_frame_tag 'comments' do %>
  <% comments.each do |comment| %>
    <%= render 'comments/comment', comment: comment %>
  <% end %>
<% end %>
コメント詳細パーシャル
app/views/comments/_comment.html.erb
<%= turbo_frame_tag dom_id(comment) do %>
...
   <%= comment.user %>
   <%= comment.body %>
...
<% end %>

Broadcastメソッドを作成する

app/models/comment.rb
class Comment < ApplicationRecord
...
  after_create_commit -> { broadcast_prepend_to "comments", 
                           partial: "comments/comment", 
                           locals: { comment: self }, target: "comments" }
end
  1. after_create_commit: コメントがデータベースにcommitされた後に呼び出されるコールバックです。
  2. broadcast_prepend_to "comments": Turbo Streamsを指定のストリームにブロードキャストします。"comments" ストリームに対して、新しいコメントを追加するように指示しています。
  3. partial: "comments/comment": この部分は、新しいコメントが表示される際にどの部分テンプレートを使用するかを指定します。"comments/comment" 部分テンプレートが使用されます。
  4. locals: { comment: self }: 部分テンプレートに渡されるローカル変数です。
  5. target: "comments": 送信されるターゲットを指定します。"comments" ターゲットにTurbo Streamを内容を送信します。

WebSocketを介してTurbo Streamsはcommentsからストリーミングし、commentcommentsprependされることを確認します。

19:34:01 web.1  | Started GET "/cable" for ::1 at 2023-09-02 19:34:01 +0900
19:34:01 web.1  | Started GET "/cable" [WebSocket] for ::1 at 2023-09-02 19:34:01 +0900
19:34:01 web.1  | Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
19:34:01 web.1  | Turbo::StreamsChannel is transmitting the subscription confirmation
19:34:01 web.1  | Turbo::StreamsChannel is streaming from comments
19:34:02 web.1  | Started GET "/cable" for ::1 at 2023-09-02 19:34:02 +0900
19:34:02 web.1  | Started GET "/cable" [WebSocket] for ::1 at 2023-09-02 19:34:02 +0900
19:34:02 web.1  | Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
19:34:02 web.1  | Turbo::StreamsChannel is transmitting the subscription confirmation
19:34:02 web.1  | Turbo::StreamsChannel is streaming from comments
19:34:25 web.1  | Turbo::StreamsChannel transmitting "<turbo-stream action=\"replace\" target=\"comment_3\"><template><turbo-frame id=\"comment_3\">\n  <div class=\"my-5\" id=\"comment_3\">\n    <div class=\"bg-white py-5 px-3 rounded shadow\">\n      <div class=\"flex space-x-3 pb-2 border-b\">\n        <div class=\"flex-shrink-0\">\n          <di... (via streamed from comments)

Image from Gyazo

コメントの編集と削除

コメントの編集と削除も同じようにBroadcastしたいのでコールバックメソッドを作成します。
先に作成したメソッドでもよかったですが、Railsのネーミング規則に従って命名しているため、partiallocalstargetを指定しなくても大丈夫です。
なので、一行で書くこともできます。

app/models/comment.rb
class Comment < ApplicationRecord
...
  after_create_commit { create_broadcast }
  after_update_commit { update_broadcast }
  after_destroy_commit { destroy_broadcast }

  private

  def create_broadcast
    broadcast_prepend_to "comments"
    # 非同期 broadcast_prepend_later_to "comments"
  end

  def update_broadcast
    broadcast_replace_to "comments"
    # 非同期 broadcast_replace_later_to "comments"
  end

  def destroy_broadcast
    broadcast_remove_to "comments"
  end
end
irb(main):017:0> c = Comment.last
  Comment Load (0.9ms)  SELECT "comments".* FROM "comments" ORDER BY "comments"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 
#<Comment:0x000000010e308860
...
# パーシャル
irb(main):018:0> c.to_partial_path
=> "comments/comment"
# locals:model_name.element.to_sym => self
# モデルのelementをシンボルに変換し
irb(main):019:0> c.model_name.element
=> "comment"
irb(main):020:0> c.model_name.element.to_sym
=> :comment
# target:モデル名の複数形
irb(main):021:0> c.model_name.plural
=> "comments"

Broadcastの使えるメソッド:
https://rubydoc.info/github/hotwired/turbo-rails/Turbo/Streams/Broadcasts

終わりに

turbo_streamに対しての理解がまだ浅いですが少しずつ練習して理解を深めていきたいです。
参考した記事:
https://www.hotrails.dev/turbo-rails/turbo-streams

Discussion