owned this note
owned this note
Published
Linked with GitHub
# Atualizando a página automaticamente com ActionCable
###### tags: `tutorial` `rails` `actioncable`
---
[ToC]
## Introdução
ActionCable é uma ferramenta que permite a utilização de WebSockets para transmissão de dados. WebSocket é uma conexão cliente-servidor entre o browser e o servidor.
A intensão desse tutorial é de quando o Sidekiq termina de processar, a gente atualizar a página automaticamente.
O fluxo de funcionamento do ActionCable é o seguinte:
```sequence
Cliente->Servidor: createConsumer() em consummer.js
Servidor->Cliente: Autentica conexão\n(ApplicationCable::Connection)
Cliente->Servidor: Inscreve no canal\n(*_channel.js)
Note right of Servidor: Identifica inscrição\n*_channel.rb
Servidor->Cliente: Estabeleçe inscrição
```
Depois que a inscrição é estabelecida, ambos as entidades podem enviar e receber informações diretamente.
## Criando a máquina do ActionCable
Novamente no nosso `docker-compose.yml`, vamos adicionar mais uma máquina:
```yaml=38
cable:
build:
context: .
dockerfile: Dockerfile
depends_on:
- web
- database
- redis
ports:
- '28080:28080'
volumes:
- .:/app
env_file: .env
entrypoint: ./entrypoints/cable_entrypoint.sh
```
e de novo vamos criar o entrypoint `entrypoints/cable_entrypoint.sh`, com
```shell=
#!/bin/sh
set -e
if [ -f tmp/pids/server.pid ]; then
rm tmp/pids/server.pid
fi
bundle exec puma -p 28080 cable/config.ru
```
:::warning
:exclamation: **Nota:** Por ser um executável que vai rodar no docker, devemos habilitar a permissão de execução, então devemos executar **`chmod +x entrypoints/cable_entrypoint.sh`**.
:::
Como podemos perceber na linha de execução, temos uma chamada para um `cable/config.ru` e esse arquivo não existe. Então vamos criá-lo e adicionar esse código de inicialização:
```ruby=
require_relative "../config/environment"
Rails.application.eager_load!
run ActionCable.server
```
Agora devemos configurar o ActionCable no nosso app. E fazemos isso no `config/cable.yml`. O que faremos é trocar
```yaml=
development:
adapter: async
```
por
```yaml=
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: pasta_do_projeto_dev
```
O que mudamos foi por onde iremos executar nosso ActionCable. O `async` é na própria instância do nosso `rails s`, enquanto o `redis` usa um banco de dados em memória para armazenar todos os envios.
Agora que temos uma máquina para o ActionCable, podemos alterar a aplicação para usá-lo.
## Configurações do ActionCable
Vamos configurar o jeito que a conexão do ActionCable é autenticada. Isso acontece no `app/channels/application_cable/connection.rb`. Essa classe determina as regras para permitir a conexão. Como temos um usuário logado, podemos usá-lo como autenticação. Então vamos adicionar esse código à classe:
```ruby=
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user
user_id = cookies.encrypted[:user_id]
User.find_by(id: user_id) || reject_unauthorized_connection
end
```
Aqui usamos um cookie chamado `user_id` para buscar o usuário logado e, se existir, retornamos ele. Senão dizemos que a conexão não está autorizada.
Agora vamos configurar esse cookie no arquivo `config/initializers/warden_hooks.rb`, que deve ser criado com esse código:
```ruby=
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.encrypted["#{scope}_id"] = user.id
end
Warden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.encrypted["#{scope}_id"] = nil
end
```
O **Warden** é uma gema que o **Devise** se baseia, então usamos os método `after_set_user` e `before_logout` para gerarmos e limparmos os cookies, respectivamente. Esse método `cookies.encrypted` é um utilitário para deixar criptografado o conteúdo e o nome do cookie.
Agora devemos criar o canal que o ActionCable irá usar para se comunicar. Podem existir diversos canais, mas para este tutorial iremos precisar de apenas um, o `app/channels/image_channel.rb`. Cada canal tem um método chamado `subscribed` que é para quando algum cliente tenta se conectar com ele, alguma *stream* seja gerada. Nós queremos atualziar as imagens da tela, então iremos gerar uma *stream* de dados da imagem caso seja uma imagem do nosso usuário, com esse código:
```ruby=
class ImageChannel < ApplicationCable::Channel
def subscribed
image = Image.find(params['image_id'])
stream_for(image) if image.user == current_user
end
end
```
Esse `params['image_id']` vai vir do nosso cliente do Front. Que é o que vamos criar agora, em `app/javascript/channels/image_channel.js`. Nele nós vamos criar o consumidor e podemos definir uma lógica para ser executada quando recebemos alguns dados. O código do nosso consumidor é:
```javascript=
import consumer from "./consumer"
function applyImageData(data) {
const duration = 500
$('#card').animate({ backgroundColor: data.background_color }, duration)
$('#card-title').animate({ color: data.detail_color }, duration)
$('#first-text').animate({ color: data.primary_color }, duration)
$('#second-text').animate({ color: data.secondary_color }, duration)
}
document.addEventListener('turbolinks:load', () => {
let image_id = $('#image-id').data('image-id');
if (window.cable.image_channel) {
window.cable.image_channel.unsubscribe();
delete window.cable.image_channel;
}
if (image_id) {
window.cable.image_channel = consumer.subscriptions.create({channel: "ImageChannel", image_id: image_id}, {
received(data) {
applyImageData(data)
}
})
}
})
```
O que fazemos nesse arquivo é definir a lógica do que fazer no método `applyImageData` (usamos o método `animate` do jQuery-UI), criamos um **EventListener** no `turbolinks:load`, buscarmos o ID da imagem atual no elemento de id `image-id` (ainda não criamos ele), resetamos a conexão caso já tenhamos uma e por fim, se tivermos uma imagem, criamos a inscrição com o canal. Para criar a inscrição precisamos de duas coisas, os dados do canal e um *payload*. Os dados se referem á conexão, ou seja, qual canal (tem que ser a mesma classe que temos no Rails) e parametros extras, no caso enviamos o `image_id`. O *payload* pode possuir diversos métodos, mas o que usamos é o `received`, que é executado quando o servidor nos manda dados.
Agora precisamos de um pequeno ajuste no `app/javascript/packs/application.js`, que é adicionar o seguinte código no final:
```javascript=
if (!window.cable) {
window.cable = {}
}
```
Agora só nos resta duas coisas, atualizar o front para aceitar as atualizações e criar o elemento com o id da imagem, e fazer o `broadcast` no canal para envio dos dados
Para atualizar o front, vamos abrir o `app/views/images/show.html.erb` e adicionar alguns IDs:
No elemento `class="card mx-auto"`, vamos colocar `id="card"`
No elemento `class="card-title"`, vamos colocar `id="card-title"`
No elemento `<p>` que tem "Primary color", vamos colocar `id="first-text"`
No elemento `<p>` que tem "Secondary color", vamos colocar `id="second-text"`
e antes de `<div class="card-body">` vamos adicionar essa linha:
```htmlembedded=
<div id='image-id' data-image-id="<%= @image.id %>" hidden></div>
```
No final, o arquivo deve ficar assim:
```htmlmixed=
<div class="card mx-auto" id="card" style="width: 50%; min-width: 200px; margin-top: 1em; background-color: <%= @image.background_color %>">
<%= image_tag @image.file, class: 'card-img-top' %>
<div id='image-id' data-image-id="<%= @image.id %>" hidden></div>
<div class="card-body">
<h2 class="card-title" id="card-title" style="color: <%= @image.detail_color %>"><%= @image.title %>!</h2>
<p class="card-text" id="first-text" style="color: <%= @image.primary_color %>">Primary color</p>
<p class="card-text" id="second-text" style="color: <%= @image.secondary_color %>">Secondary color</p>
<div class="btn-group d-flex">
<%= link_to 'Editar', edit_image_path(@image), class: 'btn btn-warning' %>
<%= link_to 'Voltar', images_path, class: 'btn btn-secondary' %>
</div>
</div>
</div>
```
Tudo que nos resta é atualizar o nosso **Worker** para fazer o broadcast quando atualizamos os dados, então no final do `def perform` vamos adicionar
```ruby=16
attributes = image.attributes.slice('title', 'primary_color', 'secondary_color', 'detail_color', 'background_color')
ImageChannel.broadcast_to(image, attributes)
```
A primeira linha busca os atributos da imagem (o `slice` faz buscar apenas os especificados) e a segunda faz o *broadcast* para todos os usuários conectados na pagina da `imagem` enviando os `attributes`.
:::warning
:exclamation: **Desafio:** O desafio desse tutorial é fazer com que na tela que aparecem todas as imagens (`images/index.html.erb`) fique explícito quais elementos estão sendo processados ou já terminaram. Pode ser com texto ou um esquema de cores
:::
:::success
:rocket: **Próximo passo:**
- Extra ➜ [Atualizando a lista de Imagens](https://hackmd.io/HS_MRsOfQqiK8YIfKpNlWA)
- Siga o link ➜ [Notificações na Página](https://hackmd.io/Qv45cPAoQ1mqxhXdcsMJaA)
:::