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$('')
ainnerHTML = ''
namiestohtml('')
.
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:
