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:
- Obter atualizações de localização do seu táxi em um mapa de uma aplicação de reserva de táxi.
- Receba novas mensagens instantaneamente em sua aplicação de bate-papo favorito.
- 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:
- Short Polling: AJAX, cria tráfego intenso.
- 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.
- 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:
- Realizar o pedido: interface para selecionar a quantidade e fazer o pedido de um item alimentar selecionado para a cozinha.
- 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.
- Mudança prevista: interface para atualizar a quantidade prevista de itens alimentares.
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:
- Reactstrap: componentes do bootstrap 4 fáceis de usar.
- 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.
- 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.
- 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:
- Monk: uma pequena camada que oferece melhorias simples, mas substanciais, na usabilidade do uso do MongoDB no Node.JS.
- 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.