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!

0 Comentário