Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?

posted at

Hotwire(Turbo)を試す その3: Trubo Streamsでのブロードキャスト

環境: Rails 6.1、Turbo 7.0.0-beta.5

の続きです。

Trubo Streamsには、HTML片を通常のHTTPレスポンスで送るほかに、Action Cableを使ってブラウザーに一斉送信(ブロードキャスト)する機能があります。

ブロードキャストをとりあえず試す

その1とその2で作ったサンプルにこの機能を導入して、記事を作成・更新・削除するたびに記事一覧が自動更新されるようにしてみます。各メソッドの詳しい説明は後回しにします。

Action Cable経由で更新したいページには、turbo_stream_fromメソッドでストリーム名を指定します。ここでは 'top_entries' としています。

app/views/entries/index.html.erb
<%= turbo_stream_from 'top_entries' %>

このメソッドは<turbo-cable-stream-source>要素を埋め込みます。TurboのJavaScriptはこれを見てAction Cableへの接続を開始するようです。

<turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-name="InRvcF9lbnRyaWVzIg==--0088422a300e71f502a839bd9f5940479f3fe7b18578309a3313619862ef4118"></turbo-cable-stream-source>

一覧中の記事が個別に更新されるように、記事一つ一つを<turbo-frame>で囲みます。フレーム名に文字列の代わりにモデルオブジェクトentryを指定していますが、これは 'entry_12' のように「モデル名_id」の文字列になります。entryの代わりにdom_id(entry)としても同じです。

その2で一覧を囲む<turbo-frame>に付けていたtarget: '_top'をここに移します。

app/views/entries/_entry.html.erb
<%= turbo_frame_tag entry, target: '_top' do %>
  <div class="entry">
    <%= link_to entry.title, entry %> |
    <%= entry.created_at.strftime('%m/%d %H:%M') %> |
    <%= link_to 'Edit', edit_entry_path(entry) %> |
    <%= link_to 'Destroy', entry, method: :delete, data: { confirm: 'Are you sure?' } %>
  </div>
<% end %>

モデルクラスでは、クラスメソッドbroadcasts_toを呼び出して、作成・更新・削除のコールバックで自動的にAction Cableによるブロードキャストが行われるようにします。ラムダ内の 'top_entries' は、ストリーム名です。inserts_by: 'prepend', target: 'entries'は作成のときだけ使われるもので、後ろではなく前に追加、追加先のフレーム名は 'entries' 、という指示です。

app/models/entry.rb
class Entry < ApplicationRecord
  validates :title, :body, presence: true

  broadcasts_to ->(e){ 'top_entries' }, inserts_by: 'prepend', target: 'entries'
end

ブラウザーのウィンドウを2つ並べ、記事の新規作成・更新・削除を行うと、もう片方の表示に反映されます。

image.png

ブロードキャストのためのメソッド

上記のサンプルでは、モデルのコールバックで一律にブロードキャストしていますが、実際のアプリケーションでは、ブロードキャストのためのメソッドを必要に応じて呼び出すことになるかと思います。

Turbo Streamsによるブロードキャストの機能は、Turbo::StreamsChannelクラスで「broadcast_アクション名_to」という名前のメソッドに実装されています。また、同名のメソッドがモデルクラスにも用意されています。

アクションには、append(後ろに追加)、prepend(前に追加)、replace(差し替え)、remove(削除)があります。

Turboのソースコードを見るのが手っ取り早いかもしれません。

追加

ある記事のHTML片を送信してリストの上に追加するには、broadcast_prepend_toメソッドを使います。第1引数はストリーム名です。targetオプションは対象になるフレーム名です。ストリーム名とフレーム名を混同しないようにしましょう。

partialとlocalsはそれぞれ、レンダリングに使う部分テンプレートと部分テンプレートで使うローカル変数です。

Turbo::StreamsChannel.broadcast_prepend_to 'top_entries', target: 'entries', partial: "entries/entry", locals: { entry: @entry }

これは、次のようなHTML片をストリーム 'top_entries' に送信します。

<turbo-stream action="prepend" target="entries">
  <template>
app/views/entries/_entry.html.erb のレンダリング結果
  </template>
</turbo-stream>

次のようにモデルのメソッドを呼び出しても同じことになります。

@entry.broadcast_prepend_to 'top_entries', target: 'entries', partial: "entries/entry", locals: { entry: @entry }

モデルのメソッドの場合、オプションは省略できます。targetを省略すると、モデル名の複数形(ここでは 'entries' )が使われます。partialとlocalsを省略すると、Railsのrenderメソッドでrender partial: 'モデル名の複数形/モデル名', locals: { モデル名: モデルオブジェクト }としたときと同じになります。

@entry.broadcast_prepend_to 'top_entries'

なお、HTML片を下に追加するときは、broadcast_prepend_toの代わりにbroadcast_append_toを使います。

差し替え

HTML片を送信してリスト中の項目を差し替えるには、broadcast_replace_toメソッドを使います。targetオプションにモデルオブジェクトを指定すると、「entry_123」のようなid付きのフレーム名が対象になります。

Turbo::StreamsChannel.broadcast_replace_to 'top_entries', target: @entry, partial: "entries/entry", locals: { entry: @entry }

モデルにも同名のメソッドが用意されています。

@entry.broadcast_replace_to 'top_entries', target: @entry, partial: "entries/entry", locals: { entry: @entry }

broadcast_prepend_toと同様にオプションを省略できます。デフォルトのtargetは、target: @entryとなります。

@entry.broadcast_replace_to 'top_entries'

削除

削除用のメソッドは、broadcast_remove_toです。差し替えと同じく、targetオプションにはモデルオブジェクトを指定できます。

Turbo::StreamsChannel.broadcast_remove_to 'top_entries', target: @entry

モデルのメソッドを使うと次のようになります。targetオプションは省略できます。

@entry.broadcast_remove_to 'top_entries'

ストリームにモデルオブジェクトを指定した場合

turbo_stream_fromメソッドに指定するストリーム名には、文字列のほかにモデルオブジェクトを指定できます。一覧ページではなく詳細ページで自動更新するための書き方でしょうか。

<%= turbo_stream_from @entry %>

この場合は、ブロードキャストの際にストリーム名にモデルオブジェクトを指定できます。

@entry.broadcast_replace_to @entry, target: @entry, partial: "entries/entry", locals: { entry: @entry }

「_to」を抜いたメソッドを使うと、ストリーム名はモデルオブジェクトと見なされます。オプションのtarget、partial、localsは省略可能です。

@entry.broadcast_replace

ところで、ストリーム名には複数のオブジェクトを配列で指定できます。特定のユーザーにだけ送信したい、という場合に使えそうです。

<%= turbo_stream_from [current_user, @entry] %>

ブロードキャスト用のメソッドでもストリーム名に配列を指定します。

@entry.broadcast_replace_to [current_user, @entry]

ジョブを使った非同期送信

ブロードキャストのメソッドには、「broadcast_アクション名_later_to」という名前のものも用意されています。「later」付きのメソッドを使うと、Active JobのクラスTurbo::Streams::ActionBroadcastJobのジョブが起動して、ジョブがAction Cableを使って送信する、という流れになります。

@entry.broadcast_prepend_later_to 'top_entries', target: 'entries'

「later」付きのメソッドで非同期のブロードキャストを実験するには、Active Jobを普通に設定すればOKです。アダプターのGemを加え(ここではSidekiq)、

Gemfile
gem 'sidekiq'

設定でアダプター名を指定し、

config/environments/development.rb
  config.active_job.queue_adapter = :sidekiq

Sidekiqを起動しておきます。

% bundle exec sidekiq

モデルのコールバック用のメソッド

上記の「ブロードキャストをとりあえず試す」では、broadcasts_toメソッドを使いました。

  broadcasts_to ->(e){ 'top_entries' }, inserts_by: 'prepend', target: 'entries'

これは、次のように指定するのと同じことです。モデルのコールバックを使ってブロードキャスト用のメソッドを呼び出します。

  after_create_commit { broadcast_prepend_later_to 'top_entries', target: 'entries' }
  after_update_commit { broadcast_replace_later_to 'top_entries' }
  after_destroy_commit { broadcast_remove_to 'top_entries' }

作成と更新のときは「later」付きのジョブを使ったブロードキャストで、削除のときは「later」なしです。なぜそうなのかは分かりません。

broadcasts_toの第1引数には、belongs_toなどで関連付けたモデルオブジェクトを指定することが想定されているようです。次のようにすると、送信先のストリーム名はsend(:blog)になります。上の例のようにストリーム名を文字列で指定したい場合は、ラムダを使うしかありません。

  belongs_to :blog
  broadcasts_to :blog, inserts_by: 'prepend', target: 'entries'

これは、次のように指定するのと同じです。

  after_create_commit { broadcast_prepend_later_to blog, target: 'entries' }
  after_update_commit { broadcast_replace_later_to blog }
  after_destroy_commit { broadcast_remove_to blog }

コールバック用のメソッドにはもう1つ、「_to」の付かないbroadcastsメソッドがあります。

  broadcasts inserts_by: 'prepend'

これは、次のように書くのと同じになります。送信先のストリームは、モデルオブジェクト(@entry.broadcast_replace_later_to @entryなどと同じ)となります。

  after_create_commit { broadcast_prepend_later }
  after_update_commit { broadcast_replace_later }
  after_destroy_commit { broadcast_remove }

ここまでの感想

  • Action Cableを使うときに、プログラムを自分であれこれ工夫しなくてもよいので楽になりそう。
  • 当面はその1とその3の機能だけ使って、その2は使わないかなあ。

おしまい

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?