Использование Turbo Streams с Kredis

При использовании Turbo Streams мы обычно хотим запускать обновления WebSockets из наших доменных моделей с помощью методов Turbo::Broadcastable, включенных в ActiveRecord.

Но что если мы хотим добавить функцию, которая использует только Redis для сохранения данных? Как мы можем транслировать потоки Turbo при изменении значения ключа Redis?

Давайте ответим на эти вопросы, создав простой живой счетчик с помощью Turbo Streams и Kredis.

Создание счетчика с помощью Turbo Streams и Kredis

Давайте создадим приложение Rails 7 без ActiveRecord (и с Tailwind только для того, чтобы наш счетчик выглядел хорошо):

$ rails new hotwire-counter --css tailwind --skip-active-record
$ cd hotwire-counter
Войдите в полноэкранный режим Выход из полноэкранного режима

Чтобы использовать Kredis, нам нужно откомментировать gem "kredis" в нашем Gemfile и установить его:

$ bundle
$ bin/rails kredis:install
Войти в полноэкранный режим Выход из полноэкранного режима

Давайте сгенерируем контроллер:

$ rails g controller counter show
Войти в полноэкранный режим Выйти из полноэкранного режима

И определим наши действия:

# app/controllers/counter_controller.rb
class CounterController < ApplicationController
  def show
    render :show, locals: {
      counter_count: counter_count.value
    }
  end

  def increment
    counter_count.increment

    respond_to do |format|
      format.turbo_stream { render_partial_update }

      format.html { redirect_to root_url }
    end
  end

  def decrement
    counter_count.decrement

    respond_to do |format|
      format.turbo_stream { render_partial_update }

      format.html { redirect_to root_url }
    end
  end

  private
    # We use Kredis to store and retrieve the counter's state
    def counter_count
      @counter_count ||= Kredis.counter "counter:count"
    end

    def render_partial_update
      render turbo_stream: turbo_stream.update("counter-count",
        counter_count.value
      )
    end
end
Войти в полноэкранный режим Выход из полноэкранного режима

Добавим наши маршруты:

Rails.application.routes.draw do
  root to: "counter#show"

  post "/increment", to: "counter#increment", as: :increment
  post "/decrement", to: "counter#decrement", as: :decrement
end
Войти в полноэкранный режим Выход из полноэкранного режима

Обновим app/views/counter/show.html.erb:

<div class="flex flex-col items-center mx-auto my-0 mb-4 text-lg gap-2">
  <div>
    <span id="counter-count">
      <%= counter_count %>
    </span>
  </div>

  <div class="flex gap-2">
    <%= button_to "+", increment_path, class: "px-8 py-4 text-white bg-black rounded" %>
    <%= button_to "-", decrement_path, class: "px-8 py-4 text-white bg-black rounded" %>
  </div>
</div>
Вход в полноэкранный режим Выход из полноэкранного режима

Запустим сервер:

$ bin/dev
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь мы должны иметь возможность использовать наш счетчик в браузере:

Трансляция турбо-потоков

Поскольку мы не используем ActiveRecord, мы не сможем использовать методы Turbo::Broadcastable для трансляции обновлений.

Но это не будет проблемой, потому что мы можем использовать Turbo::StreamsChannel напрямую с такими методами, как Turbo::StreamsChannel.broadcast_update_to (определен в модуле Turbo::Streams::Broadcasts).

Он работает с помощником turbo_stream_from, используемым в представлениях для подписки на Turbo::StreamsChannel и получения обновлений из определенных потоков.

Давайте попробуем использовать Turbo::StreamsChannel.broadcast_update_to в нашем контроллере для трансляции обновлений:

# app/controllers/counter_controller.rb
class CounterController < ApplicationController
  ...
  def increment
    ...
    respond_to do |format|
      format.turbo_stream do 
        render_partial_update

        broadcast_update
      end
      ...
    end
  end

  def decrement
    ...
    respond_to do |format|
      format.turbo_stream do 
        render_partial_update

        broadcast_update
      end
      ...
    end
  end

  private
    ...
    # This will broadcast the following Turbo stream:
    # <turbo-stream action="update" target="counter-count" >
    #   <template>counter_count.value</template>
    # </turbo-stream>
    def broadcast_update
      Turbo::StreamsChannel.broadcast_update_to "counter",
        target: "counter-count"
        content: counter_count.value
    end
end
Вход в полноэкранный режим Выход из полноэкранного режима

Нам также нужно использовать turbo_stream_from в app/views/counter/show.html.erb для получения обновлений:

<%= turbo_stream_from "counter" %>
...
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь давайте откроем нашу страницу в двух отдельных вкладках:

Отлично, сработало!

Но подождите! В нашем подходе есть проблема. Можете ли вы понять, в чем дело?

Наш контроллер не должен отвечать за трансляцию изменений страницы. Это потому, что они не могут быть переданы, если мы используем счетчик непосредственно из бэкэнда. Это значит, что мы не увидим результат на странице, если, например, увеличим счетчик из консоли.

Можете ли вы предложить лучшее решение?

Лучшим подходом было бы смоделировать наш счетчик, включив Kredis::Attributes и используя макрос kredis_counter с обратным вызовом after_change:

# app/models/counter.rb
class Counter
  include Kredis::Attributes

  kredis_counter :count,
    key: "counter:count",
    after_change: :broadcast_update

  class << self
    def count
      new.count
    end
  end

  def broadcast_update
    Turbo::StreamsChannel.broadcast_update_to "counter",
      target: "counter-count",
      content: count.value
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, когда мы перенесли логику вещания в модель Counter, мы можем транслировать обновления всякий раз, когда счетчик меняется. Это значит, что мы можем обновлять счетчик из консоли и видеть результат на странице.

Продолжим рефакторинг метода #counter_count в нашем контроллере:

# app/controllers/counter_controller.rb
class CounterController < ApplicationController
  ...
  private
    def counter_count
      @counter_count ||= Counter.count
    end
end
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь давайте откроем консоль и обновим счетчик:

Et voilà, вот оно!

Завершение

Мы можем использовать Turbo::StreamsChannel с Kredis::Attributes для трансляции потоков Turbo при изменении значения ключа Redis.

Оцените статью
devanswers.ru
Добавить комментарий