Artigo original: How to create a realtime app using Socket.io, React, Node & MongoDB

Nota da tradução: o artigo a seguir foi escrito quando o React estava em sua versão 16.7.0. No momento da tradução, ele se encontra em sua versão 18.2.0 e houve muitas mudanças no comportamento dos componentes – em especial, com a questão de roteamento. Caso deseje realizar o processo tal e qual ele está neste artigo, sugerimos a instalação da versão 16.7.0 do React para seguir o passo a passo. Para isso, use o comando npm install react@16.7.0.

Você já se perguntou como as aplicações em tempo real são criadas? Já percebeu a importância e os casos de uso de aplicações em tempo real?

Se você está curioso sobre as perguntas acima e precisa de respostas, então este artigo é para você.

Primeiro, vamos identificar alguns casos de uso que precisam de aplicações em tempo real:

  1. Obter atualizações de localização do seu táxi em um mapa de uma aplicação de reserva de táxi.
  2. Receba novas mensagens instantaneamente em sua aplicação de bate-papo favorito.
  3. Atualização de informações sobre pedidos de comida para a cozinha de seu restaurante favorito.

Todos esses são cenários comuns do nosso dia a dia, em que não podemos tolerar um atraso na atualização das informações e, portanto, precisamos de comunicação em tempo real.

Tecnologias que podem ser usadas para comunicação em tempo real são:

  1. Short Polling: AJAX, cria tráfego intenso.
  2. Long Polling: Igual ao AJAX, mas o servidor retém a resposta até que tenha uma atualização. Depois de recebê-la, o client envia outra solicitação e precisa que o cabeçalho adicional seja percorrido para frente e para trás, causando sobrecarga adicional.
  3. Web Sockets: possibilitam a abertura de comunicação interativa entre o client e o servidor. É possível enviar uma solicitação para o servidor e receber respostas orientadas por eventos sem consultar o servidor para obter uma resposta, o que torna os Web Sockets a melhor opção para o nosso caso de uso.

Informações mais detalhadas sobre as três tecnologias acima podem ser lidas aqui (texto em inglês).

Vamos aprender a criar uma aplicação em tempo real, abordando o seguinte cenário: imagine que você está sentado em seu restaurante favorito e tem um menu digital. Você faz o pedido e a cozinha é atualizada sobre ele em tempo real. Quando a cozinha termina de fazer o pedido, ela também o atualiza em tempo real.

Características detalhadas:

  1. Realizar o pedido: interface para selecionar a quantidade e fazer o pedido de um item alimentar selecionado para a cozinha.
  2. Cozinha: interface que pode ser aberta em várias cozinhas e atualiza em tempo real os chefs e cozinheiros com relação ao total de pedidos criados e à quantidade prevista de itens alimentícios, dando a eles a flexibilidade de atualizá-los. Também possui uma funcionalidade para baixar o relatório na forma de uma planilha do Excel.
  3. Mudança prevista: interface para atualizar a quantidade prevista de itens alimentares.
6EyW3Wo0cKhjTFMskVgaWeTAaVS-3m26bhzL

Para uma melhor compreensão, abra-o em diferentes abas/dispositivos ao mesmo tempo para ver a alteração dos dados em tempo real.

O código-fonte está aqui. Sinta-se à vontade para criar algo inovador/útil com base nele.

Então, vamos começar.

Stack de tecnologias:

Front-end: React.js, Reactstrap, Socket.io

Back-end: Node.js (Express), MongoDB, Socket.io

Estrutura de pastas:

/*
Vá para o diretório raiz no código-fonte e encontre os arquivos mencionados abaixo. Essa arquitetura ajuda a criar uma grande aplicação modular.
*/
backend-my-app/ /* Código do back-end da aplicação */
 server.js       /* O código do soquete e do back-end fica aqui*/
 build/      /* Opcional para a implementação da build do front-end */ 
 package.json /* Dependências do back-end */
 ...
public/
src/  /*      Código-fonte do front-end      */
 global/      /*   Componentes usados em todos os lugares   */
  header.css
  header.js     
 main/           
  Kitchen.js
  PlaceOrder.js
  UpdatePredicted.js
 App.js   /* Lógica de roteamento e parte de montagem dos componentes */
package.json /* Dependências do front-end */ 
 ............

Explicação do código-fonte:

Front-end:

git clone https://github.com/honey93/OrderKitchen.git
cd OrderKitchen
npm install
npm start

Pacotes utilizados:

  1. Reactstrap: componentes do bootstrap 4 fáceis de usar.
  2. Socket.io: o Socket.io é uma biblioteca que permite a comunicação em tempo real, bidirecional e baseada em eventos entre o navegador e o servidor.
  3. react-html-table-to-excel: fornece uma geração de arquivos Excel (.xls) no lado do client a partir de um elemento de tabela HTML.
  4. react-router-dom: vinculações do DOM para o React Router. Ele consiste em vários componentes importantes, como BrowserRouter, usado quando há um servidor para lidar com solicitações dinâmicas, Switch, Route etc.

Componente App

Caminho: src/App.js

Este componente contém a lógica de roteamento principal do front-end. Esse arquivo é usado em src/index.js dentro do módulo Browser Router. O código abaixo demonstra uma das abordagens para manter sua aplicação modular.

import React, { Component } from "react";
import "./App.css";
import { Header } from "./global/header";
import { Switch, Route } from "react-router-dom";
import PlaceOrder from "./main/PlaceOrder";
import UpdatePredicted from "./main/UpdatePredicted";
import Kitchen from "./main/Kitchen";
/*O componente <Route> é a parte principal do React Router. Em qualquer lugar em que você queira renderizar apenas o conteúdo com base no nome do caminho do local, você deve usar um elemento <Route>. */
/* O componente Route espera uma propriedade de caminho, que é uma cadeia de caracteres que descreve o nome do caminho ao qual a rota corresponde */
/* O <Switch> iterará as rotas e renderizará apenas a primeira que corresponder ao nome do caminho atual */
class App extends Component {
  render() {
    return (
      <div className="App">
        <Header />
        <Switch>
          <Route exact path="/" component={PlaceOrder} />
          <Route path="/updatepredicted" component={UpdatePredicted} />
          <Route path="/kitchen" component={Kitchen} />
        </Switch>
      </div>
    );
  }
}
export default App;

Componente do cabeçalho

Caminho: src/global/header.js

Este componente será comum e usado em todas as seções, como Realizar Pedido (em inglês, Place Order), Mudança Prevista (em inglês, Change Predicted), Cozinha (em inglês, Kitchen). Essa abordagem ajuda a evitar a duplicação de código e mantém a aplicação modular.

import React, { Component } from "react";
import { NavLink } from "react-router-dom";
import socketIOClient from "socket.io-client";
import "./header.css";
// O cabeçalho cria links que podem ser usados para navegar
// entre as rotas.
var socket;
class Header extends Component {
/* Cria um client do Socket e o exporta no final para ser usado nos componentes Place Order, Kitchen etc. */
  constructor() {
    super();
    this.state = {
      endpoint: 'http://localhost:3001/'
    };
socket = socketIOClient(this.state.endpoint);
  }
render() {
    return (
      <header>
        <nav>
          <ul className="NavClass">
            <li>
              <NavLink exact to="/">
                Place Order
              </NavLink>
            </li>
            <li>
              <NavLink to="/updatepredicted">Change Predicted </NavLink>
            </li>
            <li>
              <NavLink to="/kitchen"> Kitchen </NavLink>
            </li  >
          </ul>
        </nav>
      </header>
    );
  }
}
export { Header, socket };

Componente da cozinha

Caminho: src/main/Kitchen.js

A lógica da UI da tela da Cozinha e o código html residem neste componente:

import React, { Component } from "react";
import { Button, Table, Container } from "reactstrap";
import { socket } from "../global/header";
import ReactHTMLTableToExcel from "react-html-table-to-excel";
class Kitchen extends Component {
  constructor() {
    super();
    this.state = {
      food_data: []
      // é onde estamos nos conectando com os soquetes,
    };
  }
getData = foodItems => {
    console.log(foodItems);
    this.setState({ food_data: foodItems });
  };
changeData = () => socket.emit("initial_data");
/* Assim que o componente for montado, ou seja, no método componentDidMount, dispara o evento initial_data para obter os dados para inicializar o Kitchen Dashboard */
/* Adiciona o ouvinte change_data para ouvir todas as alterações feitas pelos componentes Fazer pedido e Pedido previsto */ 
componentDidMount() {
    var state_current = this;
    socket.emit("initial_data");
    socket.on("get_data", this.getData);
    socket.on("change_data", this.changeData);
  }

/* Remove o ouvinte antes de desmontar o componente para evitar a adição de vários ouvintes no momento da revisão */
componentWillUnmount() {
    socket.off("get_data");
    socket.off("change_data");
  }
/* Quando Done (Pronto) é clicado, essa função é chamada e o evento mark_done é emitido, o qual é ouvido no back-end, explicado mais adiante*/
markDone = id => {
    // console.log(predicted_details);
    socket.emit("mark_done", id);
  };
getFoodData() {
    return this.state.food_data.map(food => {
      return (
        <tr key={food._id}>
          <td> {food.name} </td>
          <td> {food.ordQty} </td>
          <td> {food.prodQty} </td>
          <td> {food.predQty} </td>
          <td>
            <button onClick={() => this.markDone(food._id)}>Done</button>
          </td>
        </tr>
      );
    });
  }
render() {
    return (
      <Container>
        <h2 className="h2Class">Kitchen Area</h2>
        <ReactHTMLTableToExcel
          id="test-table-xls-button"
          className="download-table-xls-button"
          table="table-to-xls"
          filename="tablexls"
          sheet="tablexls"
          buttonText="Download as XLS"
        />
<Table striped id="table-to-xls">
          <thead>
            <tr>
              <th>Name</th>
              <th>Quantity</th>
              <th>Created Till Now</th>
              <th>Predicted</th>
              <th>Status</th>
            </tr>
          </thead>
          <tbody>{this.getFoodData()}</tbody>
        </Table>
      </Container>
    );
  }
}
export default Kitchen;

Componente Fazer Pedido

Caminho: src/main/PlaceOrder.js

import React, { Component } from "react";
import { Button, Table, Container } from "reactstrap";
import { socket } from "../global/header";
class PlaceOrder extends Component {
  constructor() {
    super();
    this.state = {
      food_data: []
      // é onde estamos nos conectando com os soquetes,
    };
  }
getData = foodItems => {
    console.log(foodItems);
    foodItems = foodItems.map(food => {
      food.order = 0;
return food;
    });
    this.setState({ food_data: foodItems });
  };
componentDidMount() {
    socket.emit("initial_data");
    var state_current = this;
    socket.on("get_data", state_current.getData);
  }
componentWillUnmount() {
    socket.off("get_data", this.getData);
  }
// Função para fazer o pedido.
sendOrder = id => {
    var order_details;
    this.state.food_data.map(food => {
      if (food._id == id) {
        order_details = food;
      }
      return food;
    });
    console.log(order_details);
    socket.emit("putOrder", order_details);
    var new_array = this.state.food_data.map(food => {
      food.order = 0;
      return food;
    });
    this.setState({ food_data: new_array });
  };
// Altera a quantidade no estado que é emitido para o back-end no momento da realização do pedido.
changeQuantity = (event, foodid) => {
    if (parseInt(event.target.value) < 0) {
      event.target.value = 0;
    }
    var new_array = this.state.food_data.map(food => {
      if (food._id == foodid) {
        food.order = parseInt(event.target.value);
      }
      return food;
    });
    this.setState({ food_data: new_array });
  };
// Obtém os dados iniciais
getFoodData() {
    return this.state.food_data.map(food => {
      return (
        <tr key={food._id}>
          <td> {food.name} </td>
          <td>
            <input
              onChange={e => this.changeQuantity(e, food._id)}
              value={food.order}
              type="number"
              placeholder="Quantity"
            />
          </td>
          <td>
            <button onClick={() => this.sendOrder(food._id)}>Order</button>
          </td>
        </tr>
      );
    });
  }
render() {
    return (
      <Container>
        <h2 className="h2Class">Order Menu</h2>
        <Table striped>
          <thead>
            <tr>
              <th>Product</th>
              <th>Quantity</th>
              <th>Order</th>
            </tr>
          </thead>
          <tbody>{this.getFoodData()}</tbody>
        </Table>
      </Container>
    );
  }
}
export default PlaceOrder;

Mais uma seção chamada Atualizar caminho previsto (em inglês, Update Predicted Path): src/main/UpdatePredicted.js, semelhante à seção acima, está no repositório de código.

Back-end

Iniciando o back-end:

cd backend-my-app
npm install
node server.js

Pacotes utilizados:

  1. Monk: uma pequena camada que oferece melhorias simples, mas substanciais, na usabilidade do uso do MongoDB no Node.JS.
  2. Socket.io: o Socket.io é uma biblioteca que permite a comunicação em tempo real, bidirecional e baseada em eventos entre o navegador e o servidor.

3. Express: estrutura da Web rápida e minimalista para o node.

Código principal

Caminho: backend-my-app/server.js

const express = require("express");
const http = require("http");
const socketIO = require("socket.io");
// Cadeia de conexão do banco de dados MongoDb hospedado no Mlab ou localmente
var connection_string = "**********";
// O nome da coleção deve ser "FoodItems", pois há apenas uma coleção no momento.
// O formato do documento deve ser o mencionado abaixo, pelo menos um desses documentos:
// {
//     "_id": {
//         "$oid": "5c0a1bdfe7179a6ca0844567"
//     },
//     "name": "Veg Roll",
//     "predQty": 100,
//     "prodQty": 295,
//     "ordQty": 1
// }
const db = require("monk")(connection_string);
const collection_foodItems = db.get("FoodItems");
// nossa porta do localhost
const port = process.env.PORT || 3000;
const app = express();
// nossa instância de servidor
const server = http.createServer(app);
// Isso cria nosso soquete usando a instância do servidor
const io = socketIO(server);
io.on("connection", socket => {
// console.log("New client connected" + socket.id);
// console.log(socket);
// Retornando os dados iniciais do menu de comida da coleção FoodItems
  socket.on("initial_data", () => {
    collection_foodItems.find({}).then(docs => {
      io.sockets.emit("get_data", docs);
    });
  });
// A realização do pedido é chamada em /src/main/PlaceOrder.js do Frontend
  socket.on("putOrder", order => {
    collection_foodItems
      .update({ _id: order._id }, { $inc: { ordQty: order.order } })
      .then(updatedDoc => {
        // Emissão de evento para atualizar a Cozinha aberta nos dispositivos com os valores do pedido em tempo real
        io.sockets.emit("change_data");
      });
  });
// Conclusão do pedido, chamada a partir de /src/main/Kitchen.js
  socket.on("mark_done", id => {
    collection_foodItems
      .update({ _id: id }, { $inc: { ordQty: -1, prodQty: 1 } })
      .then(updatedDoc => {
        // Atualizar as diferentes áreas da cozinha com o status atual.
        io.sockets.emit("change_data");
      });
  });

// Funcionalidade para alterar o valor da quantidade prevista, chamada de /src/main/UpdatePredicted.js
  socket.on("ChangePred", predicted_data => {
    collection_foodItems
      .update(
        { _id: predicted_data._id },
        { $set: { predQty: predicted_data.predQty } }
      )
      .then(updatedDoc => {
        // Evento de soquete para atualizar a quantidade prevista na cozinha
        io.sockets.emit("change_data");
      });
  });

// A desconexão é acionada quando um cliente deixa o servidor
  socket.on("disconnect", () => {
    console.log("user disconnected");
  });
});
/* As etapas mencionadas abaixo são executadas para retornar a compilação do Front-end do create-react-app da pasta de compilação do Back-end.*/
app.use(express.static("build"));
app.use("/kitchen", express.static("build"));
app.use("/updatepredicted", express.static("build"));
server.listen(port, () => console.log(`Listening on port ${port}`));

Banco de dados: MongoDB

Mlab: banco de dados como um serviço para o MongoDB

Nome da coleção: FoodItems

Formato do documento: é necessário pelo menos um documento na coleção FoodItems com o formato mencionado abaixo.

{
"name": "Veg Roll",  // Nome do alimento
"predQty": 100,  // Quantidade prevista
"prodQty": 295,  // Quantidade produzida
"ordQty": 1   // Quantidade total do pedido
}

Espero que você tenha entendido como criar uma aplicação modular em tempo real usando a pilha MERN. Se você achou útil, deixe uma estrela no repositório do GitHub do projeto e compartilhe com seus amigos também.