Návod: ActionCable a real-time aktualizácia šablóny (Rails)

Väčšina článkov o ActionCable a WebSockets sa zameriava na aplikácie s četom a často sú obsiahle. Pozrime sa, naopak, na jednoduchý príklad: Ako „na diaľku“ aktualizovať stránku všetkým návštevníkom. Pomocou ActionCable a WebSockets.

Poďme na to

Toto chceme dosiahnúť:

  • Používateľ 1 v prvom okne pridá nový záznam (alebo upraví, odstráni)
  • Používateľ 2 v druhom okne túto zmenu uvidí bez nutnosti refrešnúť stránku 🙂

Príprava scaffold aplikácie

Majme najprv jednoduchú stránku s controllerom albums. Na to nám veľmi dobre poslúži scaffold.

rails new actiontest && cd $_
rails g scaffold album title:string
rails db:migrate

Do config/routes.rb pridajme root:

root to: 'albums#index'

A teraz naštartujme server a otvorme http://localhost:3000 v prehliadači:

rails s

Mali by sme vidieť toto. Môžeme pridať album.

Generujeme kanál

Vygenerujeme kanál, nazvime ho album:

rails g channel album

Dostaneme niekoľko súborov, jeden z nich je app/channels/album_channel.rb. Obsahuje skelet, upravme v ňom iba jeden riadok v metóde subscribed:

class AlbumChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'album_channel'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

Vysielame na kanál

Teraz môžeme na kanál začať vysielať. Jeden zo spôsobov je tento:

ActionCable.server.broadcast('album_channel', { foo: 'bar' })

To nám pošle dáta { foo: 'bar' } na kanál album_channel a všetci čo počúvajú (subscribers) môžu s týmito dátami okamžite narábať.

My si ale budeme posielať niečo užitočnejšie – html kód s partialom tabuľky vyrenderovaný na nových dátach (partial _table.html.erb vytvoríme v ďalšom kroku).

Do controlleru pridajme novú metódu na spodok pod časť private – broadcast_to_channel:

def broadcast_to_channel
  albums_html = ApplicationController.render(partial: 'albums/table', locals: { albums: Album.all })
  ActionCable.server.broadcast('album_channel', { albums_html: albums_html })
end

Chceme aby sa tabuľka prerenderovala nielen pri pridaní záznamu, ale aj pri aktualizácii a zmazaní. Preto použijeme after_action, ktorý zavolá metódu za každou jednou akciou v controlleri. My ju samozrejme obmedzíme iba na 3 akcie, ktoré potrebujeme.

Do scaffoldového controlleru app/controllers/albums_controller.rb pridáme navrch:

after_action :broadcast_to_channel, only: [:create, :update, :destroy]

Celý súbor (s vynechánými nezmenenými časťami bude vyzerať takto):

class AlbumsController < ApplicationController
  # ...
  after_action :broadcast_to_channel, only: [:create, :update, :destroy]

  # ...

  private

  # ...
  def broadcast_to_channel
    albums_html = ApplicationController.render(partial: 'albums/table', locals: { albums: Album.all })
    ActionCable.server.broadcast('album_channel', { albums_html: albums_html })
  end
end

Úprava scaffoldu

Teraz potrebujeme trochu upraviť vygenerovaný scaffold, konkrétne app/views/albums/index.html.erb. Vyberieme z nej tabuľku <table> do partialu (hneď v ďalšom kroku).

Namiesto tabuľky tam bude render partialu. Celé to obalíme do divu id=“albums“. Budeme tak vedieť v správnom momente div nájsť a cez websockety+javascript vymeniť obsah za nový (vyrenderovať partial nanovo dovnútra div-u).

Takto vyzerá upravená šablóna app/views/albums/index.html.erb:

<p id="notice"><%= notice %></p>

<h1>Albums</h1>

<div id="albums">
  <%= render 'table', albums: @albums %>
</div>

<br>

<%= link_to 'New Album', new_album_path %>

Pôvodnú tabuľku <table> presunieme do nového partialu – súboru _table.html.erb. Urobme tam jedinú zmenu – inštančnú premennú @albums prepíšme za lokálnu premennú albums.

Partial app/views/albums/_table.html.erb vyzerá takto:

<table>
  <thead>
  <tr>
    <th>Title</th>
    <th colspan="3"></th>
  </tr>
  </thead>

  <tbody>
  <% albums.each do |album| %>
    <tr>
      <td><%= album.title %></td>
      <td><%= link_to 'Show', album %></td>
      <td><%= link_to 'Edit', edit_album_path(album) %></td>
      <td><%= link_to 'Destroy', album, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
  </tbody>
</table>

Reagujeme na vysielanie

Teraz sa vráťme opäť ku ActionCable. Upravíme súbor app/javascript/channels/album_channel.js, ktorý sme tiež získali pri generovaní kanálu, cez rails g channel album.

Vygenerovaný album_channel.js je súčasťou implementácie websocketov na strane klienta. Obsahuje hotový skelet, hlavne časť received(data). Tá reaguje na prichádzajúce dáta. Stačí sem dať JS kód, ktorý chceme vykonať vždy keď niekto vysiela na kanál AlbumChannel.

Pridáme tieto 2 riadky javascriptu do vnútra received(data):

let table_div = document.getElementById('albums');
if (table_div) table_div.innerHTML = data['albums_html'];

Prvý riadok nájde div s id=“table“. Druhý riadok prepíše obsah div-u s novým obsahom – došlým html kódom v data['albums_html']. Ten si sem pošleme z controlleru hneď v ďalšom kroku.

Rails 6 už v základe nemá jQuery a preto som ho ani ja, pre jednoduchosť návodu, nepoužil. Použité getElementById namiestio $('') a innerHTML = '' namiesto html('').

Výsledný súbor app/javascript/channels/album_channel.js vyzerá takto:

import consumer from "./consumer"

consumer.subscriptions.create("AlbumChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    let table_div = document.getElementById('albums');
    if (table_div) table_div.innerHTML = data['albums_html'];
  }
});

HOTOVO? Skoro. Ešte nastavíme adapter.

Redis adapter

Nastavme ešte config/cable.yml. Pre development mód je adapter zadefinovaný ako async. Čo bude fungovať, ale ak by sme napríklad vysielali z rake tasku (volaný CRON-om), fungovať to nebude. Preto odporúčam použiť redis, tak ako pre production mód.

Upravený config/cable.yml môžeme prepísať na toto:

redis: &redis
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>

development:
  <<: &redis
  channel_prefix: news_development

test:
  adapter: test

production:
  <<: &redis
  channel_prefix: news_production

Inštalácia redis-server

Aby nám fungoval redis adapter, uistime sa, že redis server je nainštalovaný a spustený.

Linux:

sudo apt-get install redis-server 
sudo service redis-server start

MacOS:

brew install redis
brew services start redis

HOTOVO! 🎉 Tu je ešte raz výsledok:

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *