Вы когда-нибудь задумывались, как создаются приложения в реальном времени? Вы когда-нибудь замечали важность и варианты использования приложений реального времени?

Если вам интересны приведенные выше вопросы и вам нужен ответ, то этот пост для вас.

Во-первых, давайте определим несколько вариантов использования, требующих приложений реального времени:

  1. Получение обновлений местоположения вашего такси на карте в приложении для бронирования такси.
  2. Мгновенное получение новых сообщений в вашем любимом приложении для чата.
  3. Информация о заказе еды обновляется на кухне вашего любимого ресторана.

Все это обычные сценарии нашей повседневной жизни, когда мы не можем терпеть задержки в обновлении информации и, следовательно, нуждаемся в общении в реальном времени.

Технологии, которые можно использовать для связи в реальном времени:

  1. Короткий опрос: AJAX, создает большой трафик.
  2. Длительный опрос: аналогично AJAX, но сервер задерживает ответ до тех пор, пока не получит обновление. После его получения клиент отправляет другой запрос и требует, чтобы дополнительный заголовок проходил туда и обратно, вызывая дополнительные накладные расходы.
  3. Веб-сокеты: позволяют установить интерактивную связь между клиентом и сервером. Можно отправить запрос на сервер и получить ответы, управляемые событиями, без запроса ответа на сервере, что делает веб-сокеты лучшим выбором для нашего случая использования.

Более подробную информацию о трех вышеперечисленных технологиях можно прочитать здесь.

Мы научимся создавать приложения реального времени, рассмотрев следующий сценарий.

Представьте, что вы сидите в любимом ресторане и у вас есть цифровое меню. Вы размещаете заказ, и кухня получает информацию о вашем заказе в режиме реального времени. Когда на кухне готов заказ, они тоже обновляют его в режиме реального времени.

Подробнее об особенностях:

  1. Разместить заказ: интерфейс для выбора количества и размещения заказа на выбранный продукт питания на кухне.
  2. Кухня: интерфейс, который можно открыть на нескольких кухнях, и в режиме реального времени обновляет повара и повара в отношении общего количества созданных заказов и прогнозируемого количества продуктов, что дает им возможность обновлять его. Также есть функция загрузки отчета в виде таблицы Excel.
  3. Прогнозируемое изменение: интерфейс для обновления прогнозируемого количества продуктов.

Живую демонстрацию этого сценария можно найти здесь.

Для лучшего понимания откройте его на разных вкладках / устройствах одновременно, чтобы увидеть изменение данных в реальном времени.

Исходный код находится здесь. Не стесняйтесь делать что-нибудь новаторское / полезное поверх этого.

Итак, приступим.

Стек технологий:

Интерфейс: React.js, Reactstrap, Socket.io.

Серверная часть: Node.js (Express), MongoDB, Socket.io.

Структура папки:

/*
Go to the root directory in the source code and find out the below-mentioned files. This architecture helps in creating a big modular App.
*/
backend-my-app/ /* Backend code of the app */
 server.js       /* Socket and backend code resides here*/
 build/      /* Optional for deployment of Frontend Build */ 
 package.json /* Backend dependency */
 ...
public/
src/  /*      Frontend Sourcecode      */
 global/      /*   Components getting used everywhere   */
  header.css
  header.js     
 main/           
  Kitchen.js
  PlaceOrder.js
  UpdatePredicted.js
 App.js   /* Routing logic and component assembly part */
package.json /* Frontend dependency */ 
 ............

Объяснение исходного кода:

Внешний интерфейс:

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

Используемые пакеты:

  1. Reactstrap: Простые в использовании компоненты bootstrap4
  2. Socket.io: Socket.io - это библиотека, которая обеспечивает двустороннюю связь между браузером и сервером в реальном времени и на основе событий.
  3. Response-html-table-to-excel: Обеспечивает создание на стороне клиента файла Excel (.xls) из элемента таблицы HTML.
  4. React-router-dom: привязки DOM для реагирующего маршрутизатора. Он состоит из многих важных компонентов, таких как BrowserRouter, который используется, когда есть сервер для обработки динамических запросов, Switch, Route и т. Д.

Компонент приложения

Путь: src / App.js

Этот компонент содержит основную логику маршрутизации Frontend. Этот файл используется в src / index.js внутри модуля маршрутизатора браузера. Приведенный ниже код демонстрирует один из подходов к сохранению модульности вашего приложения.

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";
/*The <Route> component is the main part of React Router. Anywhere that you want to only render content based on the location’s pathname, you should use a <Route> element. */
/* The Route component expects a path prop, which is a string that describes the pathname that the route matches */
/* The <Switch> will iterate over routes and only render the first one that matches the current pathname */
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;

Компонент заголовка

Путь: src / global / header.js

Этот компонент будет общим и использоваться в таких разделах, как «Заказ», «Прогноз изменений», «Кухня». Такой подход помогает избежать дублирования кода и сохраняет модульность приложения.

import React, { Component } from "react";
import { NavLink } from "react-router-dom";
import socketIOClient from "socket.io-client";
import "./header.css";
// The Header creates links that can be used to navigate
// between routes.
var socket;
class Header extends Component {
/* Creating a Socket client and exporting it at the end to be used across the Place Order, Kitchen, etc components*/
  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 };

Компонент для кухни

Путь: src / main / Kitchen.js

Логика пользовательского интерфейса Kitchen Screen и HTML-код находятся в этом компоненте.

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: []
      // this is where we are connecting to with sockets,
    };
  }
getData = foodItems => {
    console.log(foodItems);
    this.setState({ food_data: foodItems });
  };
changeData = () => socket.emit("initial_data");
/*As soon as the component gets mounted ie in componentDidMount method, firing the initial_data event to get the data to initialize the Kitchen Dashboard */
/* Adding change_data listener for listening to any changes made by Place Order and Predicted Order components*/ 
componentDidMount() {
    var state_current = this;
    socket.emit("initial_data");
    socket.on("get_data", this.getData);
    socket.on("change_data", this.changeData);
  }

/* Removing the listener before unmounting the component in order to avoid addition of multiple listener at the time revisit*/
componentWillUnmount() {
    socket.off("get_data");
    socket.off("change_data");
  }
/* When Done gets clicked, this function is called and mark_done event gets emitted which gets listened on the backend explained later on*/
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;

Компонент размещения заказа

Путь: 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: []
      // this is where we are connecting to with sockets,
    };
  }
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);
  }
//Function to place the order.
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 });
  };
// Changing the quantity in the state which is emitted to the backend at the time of placing the order.
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 });
  };
// To get the initial data
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;

Еще один раздел, называемый «Прогнозируемый путь обновления»: src / main / UpdatePredicted.js, аналогичный приведенному выше разделу, находится в репозитории кода.

Бэкэнд

Запуск Backend:

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

Используемые пакеты:

  1. Monk: крошечный слой, который обеспечивает простые, но существенные улучшения удобства использования MongoDB в Node.JS.
  2. Socket.io: Socket.io - это библиотека, которая обеспечивает двусторонний обмен данными между браузером и сервером в реальном времени и на основе событий.

3. Экспресс: быстрый минималистичный веб-фреймворк для узла.

Основной код

Путь: backend-my-app / server.js

const express = require("express");
const http = require("http");
const socketIO = require("socket.io");
// Connection string of MongoDb database hosted on Mlab or locally
var connection_string = "**********";
// Collection name should be "FoodItems", only one collection as of now.
// Document format should be as mentioned below, at least one such document:
// {
//     "_id": {
//         "$oid": "5c0a1bdfe7179a6ca0844567"
//     },
//     "name": "Veg Roll",
//     "predQty": 100,
//     "prodQty": 295,
//     "ordQty": 1
// }
const db = require("monk")(connection_string);
const collection_foodItems = db.get("FoodItems");
// our localhost port
const port = process.env.PORT || 3000;
const app = express();
// our server instance
const server = http.createServer(app);
// This creates our socket using the instance of the server
const io = socketIO(server);
io.on("connection", socket => {
//  console.log("New client connected" + socket.id);
  //console.log(socket);
// Returning the initial data of food menu from FoodItems collection
  socket.on("initial_data", () => {
    collection_foodItems.find({}).then(docs => {
      io.sockets.emit("get_data", docs);
    });
  });
// Placing the order, gets called from /src/main/PlaceOrder.js of Frontend
  socket.on("putOrder", order => {
    collection_foodItems
      .update({ _id: order._id }, { $inc: { ordQty: order.order } })
      .then(updatedDoc => {
        // Emitting event to update the Kitchen opened across the devices with the realtime order values
        io.sockets.emit("change_data");
      });
  });
// Order completion, gets called from /src/main/Kitchen.js
  socket.on("mark_done", id => {
    collection_foodItems
      .update({ _id: id }, { $inc: { ordQty: -1, prodQty: 1 } })
      .then(updatedDoc => {
        //Updating the different Kitchen area with the current Status.
        io.sockets.emit("change_data");
      });
  });

// Functionality to change the predicted quantity value, called from /src/main/UpdatePredicted.js
  socket.on("ChangePred", predicted_data => {
    collection_foodItems
      .update(
        { _id: predicted_data._id },
        { $set: { predQty: predicted_data.predQty } }
      )
      .then(updatedDoc => {
        // Socket event to update the Predicted quantity across the Kitchen
        io.sockets.emit("change_data");
      });
  });

// disconnect is fired when a client leaves the server
  socket.on("disconnect", () => {
    console.log("user disconnected");
  });
});
/* Below mentioned steps are performed to return the Frontend build of create-react-app from build folder of backend Comment it out if running locally*/
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}`));

База данных: MongoDB

Mlab: База данных как сервис для MongoDB

Название коллекции: FoodItems

Формат документа: в коллекции FoodItems необходим хотя бы один документ с указанным ниже форматом.

{
"name": "Veg Roll",  // Food Name
"predQty": 100,  // Predicted Quantity
"prodQty": 295,  // Produced Quantity
"ordQty": 1   // Total Order Quantity
}

Надеюсь, вы поняли, как создать модульное приложение в реальном времени с использованием трендового стека MERN. Если вы нашли это полезным хлопать ниже, поставьте звездочки проекту репо и поделитесь им с друзьями.