ActionCable para comunicação de socket com Ruby on Rails + ReactJS
O ActionCable é uma ferramenta adicionada ao framework Ruby on Rails desde a versão 5.0. Ele fornece uma maneira eficiente e fácil de criar recursos de tempo real em aplicações web, como notificações em tempo real, atualizações de dados e chats.
Introdução ao ActionCable
O que é ActionCable?
O ActionCable é uma ferramenta adicionada ao framework Ruby on Rails desde a versão 5.0. Ele fornece uma maneira eficiente e fácil de criar recursos de tempo real em aplicações web, como notificações em tempo real, atualizações de dados e chats.
O ActionCable se baseia no protocolo WebSocket, onde permite comunicações bidirecionais em tempo real, entre cliente e o servidor.
Vantagens de usar o ActionCable em aplicações Rails
Como qualquer WebSocket, o ActionCable diminui a latência ao permitir troca instantânea de dados. É escalável para suportar um grande número de conexões simultâneas. Sendo uma ferramenta que já está integrada diretamente ao Rails, contando com as medidas de segurança.
Papel do protocolo WebSocket
O protocolo WebSocket fornece uma comunicação bidirecional. O que significa que ambas as partes trocam dados em tempo real, numa única conexão TCP, sem fechar a conexão, diferente do protocolo HTTP. Por ser uma conexão bidirecional, o servidor pode ter a iniciativa de enviar dados para o cliente sem que haja um pedido.
Arquitetura Pub/Sub
A arquitetura Publish/Subscribe é um modelo de comunicação que tem o objetivo de facilitar a troca de mensagens entre diferentes componentes ou sistemas. Nessa arquitetura, praticamente, temos duas divisões, publicadores e assinantes.
Publicadores são os componentes responsáveis por gerar e publicar mensagens para o sistema. Esses componentes não precisam saber para quem ou quantos assinantes estão ouvindo suas mensagens.
Assinantes são os componentes que se inscrevem nos canais para receber as mensagens. Elas não sabem quem é o publicador ou quantos outros assinantes estão recebendo a mesma mensagem. Apenas indicam seu interesse em receber mensagens de um canal específico.
Implementando um chat com ActionCable
Backend
Primeiro vamos criar a API Rails.
rails new chat-action-cable-api --api
Depois vamos criar um model de User, para direcionar os envios de mensagem.
rails generate model User name:string
Precisamos criar também um model para as mensagens, que vai guardar qual a mensagem e qual o destinatário.
rails generate model Notification message:string user:references
Após criar os dois únicos models que vamos precisar, podemos executar a migração.
rails db:migrate
Depois da migração criada, vamos criar dois usuários para testes.
User.create!(name: "Maria")
User.create!(name: "João")
Agora, vamos criar um controller básico com uma action de criar mensagens: app/controllers/notifications_controller.rb.
class NotificationsController < ApplicationController
def create
return head :bad_request unless all_params?
Notification.create!(
user_id: params[:user_id],
message: params[:message]
)
head :ok
end
private
def all_params?
params[:message].present? && params[:user_id].present?
end
end
Após criar o controller, precisamos configurar as rotas para expor o notifications_controller, e o endpoint do ActionCable.
config/routes.rb.
Rails.application.routes.draw do
resources :notifications, only: [:create]
mount ActionCable.server => '/cable'
end
Agora vamos criar um canal para o qual a mensagem será transmitida. No método subscribed podemos configurar o canal para a transmissão de dados. E no método unsubscribe podemos configurar as ações que serão executadas quando você se desconectar do canal.
app/channels/notifications_channel.rb.
class NotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications_#{params[:user_id]}"
end
def unsubscribed
stop_all_streams
end
end
Com o canal criado, nós iremos configurar o model para transmitir as novas mensagens criadas para o canal.
app/models/notification.rb.
class Notification < ApplicationRecord
after_create :stream_notification
belongs_to :user
validates :message, presence: true
private
def stream_notification
message_data = { user_name: user.name, message: message }
ActionCable.server.broadcast("notifications_#{user_id}", message_data)
end
end
Com esse método no model de User, observe que ao transmitir uma mensagem, estaremos enviando os dados para o canal nomeado pelo id do usuário. Ou seja, se a mensagem for criada com o User com o id 2, a mensagem será enviada para o canal “notifications_2”.
Frontend
Com o backend configurado, agora iremos criar o projeto do frontend, com React usando o template typescript.
npx create-react-app action-cable-frontend --template typescript
Com o projeto criado, vamos instalar a library actioncable.
yarn add actioncable @types/actionclabe
Agora vamos criar dois componentes de chat, um para cada usuário. Com o objetivo de simular dois destinatários diferentes.
import actionCable from 'actioncable';
import { useCallback, useEffect, useState } from 'react';
import { NotificationTypes } from "../interfaces/NotificationTypes";
const Chat1 = () => {
const cableApp = actionCable.createConsumer('ws://localhost:3000/cable');
const [channel, setChannel] = useState<null | actionCable.Channel>(null);
const [messagesChat, setMessagesChat] = useState<any[]>([]);
useEffect(() => {
const subscription = cableApp.subscriptions.create(
{
channel: 'NotificationsChannel',
user_id: 1,
},
{
received: (message: NotificationTypes) => handleReceivedMessage(message)
},
);
setChannel(subscription)
return () => channel?.unsubscribe()
}, []);
const handleReceivedMessage = useCallback(
(message: NotificationTypes) => setMessagesChat([...messagesChat, message])
, [messagesChat]);
return (
<>
<h1>Chat 1</h1>
{messagesChat.map((message: NotificationTypes, index: number) => {
return(
<div key={index}>
<h3> user: {message['user_name']} </h3>
<h3> message: {message['message']} </h3>
</div>
)
})}
</>
);
}
export default Chat1;
import actionCable from 'actioncable';
import { useCallback, useEffect, useState } from 'react';
import { NotificationTypes } from "../interfaces/NotificationTypes";
const Chat2 = () => {
const cableApp = actionCable.createConsumer('ws://localhost:3000/cable');
const [channel, setChannel] = useState<null | actionCable.Channel>(null);
const [messagesChat, setMessagesChat] = useState<any[]>([]);
useEffect(() => {
const subscription = cableApp.subscriptions.create(
{
channel: 'NotificationsChannel',
user_id: 2,
},
{
received: (message: NotificationTypes) => handleReceivedMessage(message)
},
);
setChannel(subscription)
return () => channel?.unsubscribe()
}, []);
const handleReceivedMessage = useCallback(
(message: NotificationTypes) => setMessagesChat([...messagesChat, message])
, [messagesChat]);
return (
<>
<h1>Chat 2</h1>
{messagesChat.map((message: NotificationTypes, index: number) => {
return(
<div key={index}>
<h3> user: {message['user_name']} </h3>
<h3> message: {message['message']} </h3>
</div>
)
})}
</>
);
}
export default Chat2;
Se observarmos os dois códigos, a única mudança entre os componentes é quando os mesmos se inscrevem no canal, para receber as mensagens. Cada componente tem um ID diferente, para receber somente as mensagens de cada usuário.
Não vamos esquecer de importar esses componentes no App.tsx.
import Chat1 from './components/Chat1';
import Chat2 from './components/Chat2';
const App = () => {
return (
<>
<Chat1/>
<Chat2/>
</>
);
}
export default App;
Testando o projeto
Vamos rodar os projetos.
rails s
yarn start
Após executar os dois projetos, abra um browser e entre no localhost:3001. E o nosso projetinho tem essa cara feia hehe.
Para testar a aplicação, vamos utilizar o postman, configurando um request, com esses valores.
Observe que como parâmetro, vamos enviar o id do usuário para quem queremos enviar a mensagem, e o conteúdo da mesma. Quando enviarmos esse request, o browser vai mostrar a mensagem no chat correto.
Conseguimos ver, que a mensagem foi enviada para o componente do chat2, que está inscrito no canal correto, enquanto o chat1, não recebe nada. Mas se enviarmos um request para o usuário com id 1. O componente chat1, vai receber uma mensagem.
Após enviar o request.
=)
Conclusão
Com esse passo a passo, você terá um chat em tempo real básico, usando o ActionCable em uma aplicação Ruby on Rails e um cliente em ReactJS. O servidor está configurado para enviar mensagens em tempo real para o cliente através do WebSocket.
O ActionCable é uma poderosa ferramenta para adicionar recursos em tempo real em aplicações Rails. Você pode explorar mais recursos do ActionCable, para criar notificações em tempo real, atualizar dados em tempo real, e muito mais. As opções vão muito além de um simples chat!
That’s all folks!