Как построить систему аукциона в реальном времени — подключение Socket.io с React 🔥 (часть 2)

Всем привет и с возвращением!

Небольшой обзор

Как и на настоящем аукционе, если вы делаете ставку на товар, вы получаете встречные ставки от других участников. Аукцион работает на «быстрой» ставке решения, где кто-то другой выиграет или перебьет вашу ставку, если вы не сделаете ставку достаточно быстро.

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

В предыдущей статье этого цикла мы познакомились с Socket.io, как подключить приложение React к серверу Node.js с помощью Socket.io и создать пользовательский интерфейс для системы торгов.

Прочитать первую часть цикла можно здесь:
https://dev.to/novu/how-to-build-a-real-time-auction-system-with-socketio-and-reactjs-3ble

В этой заключительной статье я проведу вас через отправку уведомлений и сообщений между клиентом и сервером Node.js.

Novu — первая архитектура уведомлений с открытым исходным кодом

Небольшая справка о нас. Novu — это первая инфраструктура уведомлений с открытым исходным кодом. Мы помогаем управлять всеми уведомлениями продукта. Это может быть In-App (значок колокольчика, как в Facebook — Websockets), электронная почта, SMS и так далее.
Я буду очень рад, если вы дадите нам звезду! И сообщите мне об этом в комментариях ❤️.
https://github.com/novuhq/novu

Мы вернулись! Мы продолжим с того места, где остановились в последний раз

Создание JSON файла «базы данных»

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

Перейдите в папку server и создайте файл JSON.

cd server
touch data.json
Войдите в полноэкранный режим Выйдите из полноэкранного режима

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

{
  "products": [
    {
      "name": "Audi 250",
      "price": "500000",
      "owner": "admiralty20",
      "last_bidder": "samson35"
    },
    {
      "name": "Lamborghini S50",
      "price": "200000",
      "owner": "susaske40",
      "last_bidder": "geraldt01"
    },
    {
      "name": "Ferrari F560",
      "price": "100000",
      "owner": "samson35",
      "last_bidder": "admiralty20"
    }
  ]
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Обновите файл index.js для рендеринга файла data.json. Приведенный ниже фрагмент кода считывает файл data.json и возвращает файл JSON по адресу http://localhost:4000/api, что позволяет веб-браузеру легко получить его и отобразить пользователям.

const express = require('express');
const app = express();
const PORT = 4000;
const fs = require('fs');
const http = require('http').Server(app);
const cors = require('cors');
const socketIO = require('socket.io')(http, {
  cors: {
    origin: 'http://localhost:3000',
  },
});

//Gets the JSON file and parse the file into JavaScript object
const rawData = fs.readFileSync('data.json');
const productData = JSON.parse(rawData);

app.use(cors());

socketIO.on('connection', (socket) => {
  console.log(`⚡: ${socket.id} user just connected!`);
  socket.on('disconnect', () => {
    console.log('🔥: A user disconnected');
  });
});

//Returns the JSON file
app.get('/api', (req, res) => {
  res.json(productData);
});

http.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
});
Вход в полноэкранный режим Выход из полноэкранного режима

Далее обновите страницу Products из папки клиента, чтобы получить продукты из файла JSON и отобразить его содержимое.

import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';

const Products = () => {
  const [products, setProducts] = useState(null);
  const [loading, setLoading] = useState(true);
  const navigate = useNavigate();

  const handleBidBtn = (product) =>
    navigate(`/products/bid/${product.name}/${product.price}`);

  useEffect(() => {
    const fetchProducts = () => {
      fetch('http://localhost:4000/api')
        .then((res) => res.json())
        .then((data) => {
          setProducts(data.products);
          setLoading(false);
        });
    };
    fetchProducts();
  }, []);

  return (
    <div>
      <div className="table__container">
        <Link to="/products/add" className="products__cta">
          ADD PRODUCTS
        </Link>

        <table>
          <thead>
            <tr>
              <th>Name</th>
              <th>Price</th>
              <th>Last Bidder</th>
              <th>Creator</th>
              <th>Edit</th>
            </tr>
          </thead>
          <tbody>
            {loading ? (
              <tr>
                <td>Loading</td>
              </tr>
            ) : (
              products.map((product) => (
                <tr key={`${product.name}${product.price}`}>
                  <td>{product.name}</td>
                  <td>{product.price}</td>
                  <td>{product.last_bidder || 'None'}</td>
                  <td>{product.owner}</td>
                  <td>
                    <button onClick={() => handleBidBtn(product)}>Edit</button>
                  </td>
                </tr>
              ))
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
};

export default Products;
Вход в полноэкранный режим Выход из полноэкранного режима

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

Далее давайте узнаем, как добавить продукты через форму в приложении React на сервер Node.js.

Добавление продуктов в файл JSON

У нас есть призыв к действию в компоненте Products, который ссылается на страницу AddProduct, где пользователь указывает название и цену продукта, доступного для торгов. Имя пользователя извлекается из локального хранилища.

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const AddProduct = () => {
  const [name, setName] = useState('');
  const [price, setPrice] = useState(0);
  const navigate = useNavigate();

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ name, price, owner: localStorage.getItem('userName') });
    navigate('/products');
  };

  return (
    <div>
      <div className="addproduct__container">
        <h2>Add a new product</h2>
        <form className="addProduct__form" onSubmit={handleSubmit}>
          <label htmlFor="name">Name of the product</label>
          <input
            type="text"
            name="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
            required
          />

          <label htmlFor="price">Starting price</label>
          <input
            type="number"
            name="price"
            value={price}
            onChange={(e) => setPrice(e.target.value)}
            required
          />

          <button className="addProduct__cta">SEND</button>
        </form>
      </div>
    </div>
  );
};

export default AddProduct;
Вход в полноэкранный режим Выход из полноэкранного режима

Далее мы отправим данные о товаре на сервер Node.js для хранения через Socket.io. Мы передали Socket.io как prop в каждый компонент из файла src/App.js.
Выделите Socket.io из объекта props и обновите функцию handleSubmit, как показано ниже:

const AddProduct = ({ socket }) => {
  const [name, setName] = useState('');
  const [price, setPrice] = useState(0);
  const navigate = useNavigate();

  const handleSubmit = (e) => {
    e.preventDefault();
    // console.log({ name, price, owner: localStorage.getItem('userName') });
    socket.emit('addProduct', {
      name,
      price,
      owner: localStorage.getItem('userName'),
    });
    navigate('/products');
  };

  return <div>...</div>;
};
export default AddProduct;
Вход в полноэкранный режим Выйти из полноэкранного режима

Из приведенного выше фрагмента кода следует, что событие addProduct отправляет объект, содержащий название товара, цену и владельца, на сервер Node.js через Socket.io.

Создайте событие на сервере Node.js, которое слушает сообщение addProduct от клиента.

/*
The other lines of code
*/
const rawData = fs.readFileSync('data.json');
const productData = JSON.parse(rawData);

socketIO.on('connection', (socket) => {
  console.log(`⚡: ${socket.id} user just connected!`);
  socket.on('disconnect', () => {
    console.log('🔥: A user disconnected');
  });

  //Listens to the addProduct event
  socket.on('addProduct', (data) => {
    console.log(data); //logs the message from the client
  });
});
// ....<The other lines of code>
Вход в полноэкранный режим Выход из полноэкранного режима

Поскольку мы получили доступ к данным, отправленным клиентом, давайте сохраним их в файл базы данных.

/*
The other lines of code
*/
const rawData = fs.readFileSync('data.json');
const productData = JSON.parse(rawData);

socketIO.on('connection', (socket) => {
  console.log(`⚡: ${socket.id} user just connected!`);
  socket.on('disconnect', () => {
    console.log('🔥: A user disconnected');
  });
  socket.on('addProduct', (data) => {
    productData['products'].push(data);
    const stringData = JSON.stringify(productData, null, 2);
    fs.writeFile('data.json', stringData, (err) => {
      console.error(err);
    });
  });
});
// ....<The other lines of code>
Вход в полноэкранный режим Выход из полноэкранного режима

Событие addProduct слушает сообщения от клиента и обновляет файл data.json, добавляя данные о продукте в массив products и сохраняя их в файл data.json.

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

Обновление файла JSON

В этом разделе мы дадим пользователям возможность обновлять цены на товары в JSON-файле. Изменения будут сохраняться даже после обновления страницы.

Поскольку страница BidProduct принимает данные о товаре через параметры URL, нам потребуется использовать хук useParams, предоставляемый React Router.

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useParams } from 'react-router-dom';

const BidProduct = () => {
  //sets the default value as the current price from the Product page
  const [userInput, setUserInput] = useState(price);

  //Destructured from the URL
  const { name, price } = useParams();
  const navigate = useNavigate();

  const handleSubmit = (e) => {
    e.preventDefault();
    navigate('/products');
  };

  return <div>...</div>;
};
Вход в полноэкранный режим Выход из полноэкранного режима

URL bidProduct содержит название и цену выбранного продукта со страницы Products. Хук useParams позволяет нам деструктурировать название и цену продукта из URL. Затем мы можем установить значение по умолчанию поля ввода (bid) на текущую цену со страницы Products.

Обновите компонент BidProduct.js выше, добавив Socket.io prop из src/App.js, чтобы мы могли отправить новую ставку на сервер Node.js.

import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useParams } from 'react-router-dom';

const BidProduct = ({ socket }) => {
  const { name, price } = useParams();
  const [userInput, setUserInput] = useState(price);
  const navigate = useNavigate();
  const [error, setError] = useState(false);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (userInput > Number(price)) {
      socket.emit('bidProduct', {
        userInput,
        last_bidder: localStorage.getItem('userName'),
        name,
      });
      navigate('/products');
    } else {
      setError(true);
    }
  };

  return (
    <div>
      <div className="bidproduct__container">
        <h2>Place a Bid</h2>
        <form className="bidProduct__form" onSubmit={handleSubmit}>
          <h3 className="bidProduct__name">{name}</h3>

          <label htmlFor="amount">Bidding Amount</label>
          {/* The error message */}
          {error && (
            <p style={{ color: 'red' }}>
              The bidding amount must be greater than {price}
            </p>
          )}

          <input
            type="number"
            name="amount"
            value={userInput}
            onChange={(e) => setUserInput(e.target.value)}
            required
          />

          <button className="bidProduct__cta">SEND</button>
        </form>
      </div>
    </div>
  );
};

export default BidProduct;
Вход в полноэкранный режим Выход из полноэкранного режима

Из приведенного выше фрагмента кода функция handleSubmit проверяет, превышает ли новое значение, предоставленное пользователем, цену по умолчанию. Если да, то она вызывает событие bidProduct, которое отправляет объект, содержащий введенные пользователем данные (новую цену), название товара и последнего участника торгов на сервер Node.js. В противном случае React выводит пользователю сообщение об ошибке.

Далее создадим слушателя события bidProduct на сервере для приема данных, отправленных с клиента. Обновите блок кода Socket.io в файле index.js на сервере, как показано ниже:

socketIO.on('connection', (socket) => {
  console.log(`⚡: ${socket.id} user just connected!`);
  socket.on('disconnect', () => {
    console.log('🔥: A user disconnected');
  });

  socket.on('addProduct', (data) => {
    productData['products'].push(data);
    const stringData = JSON.stringify(productData, null, 2);
    fs.writeFile('data.json', stringData, (err) => {
      console.error(err);
    });
  });

  //Listens for new bids from the client
  socket.on('bidProduct', (data) => {
    console.log(data);
  });
});
Войти в полноэкранный режим Выход из полноэкранного режима

Обновите цену выбранного товара и сохраните ее в файле data.json, скопировав приведенную ниже функцию:

function findProduct(nameKey, productsArray, last_bidder, new_price) {
  for (let i = 0; i < productsArray.length; i++) {
    if (productsArray[i].name === nameKey) {
      productsArray[i].last_bidder = last_bidder;
      productsArray[i].price = new_price;
    }
  }
  const stringData = JSON.stringify(productData, null, 2);
  fs.writeFile('data.json', stringData, (err) => {
    console.error(err);
  });
}
Войти в полноэкранный режим Выход из полноэкранного режима

Функция принимает список продуктов, название, последнего участника торгов и новую цену продукта, затем перебирает все объекты в массиве, пока не найдет подходящее название продукта. Затем она обновляет последнего участника торгов и цену товара в файле data.json.

Вызовите функцию в коде Socket.io, чтобы установить цену и последнего участника торгов выбранного товара.

....
....
  socket.on('bidProduct', (data) => {
    //Function call
    findProduct(
      data.name,
      productData['products'],
      data.last_bidder,
      data.amount
    );
  });
});
Вход в полноэкранный режим Выход из полноэкранного режима

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

Отправка уведомлений пользователям через Socket.io

В этом разделе мы подключим компонент Nav к серверу Node.js, чтобы всякий раз, когда пользователь добавляет товар и делает ставку, сервер отправлял сообщение в приложение React.

Обновите блок кода Socket.io в файле index.js, как показано ниже:

socketIO.on('connection', (socket) => {
  console.log(`⚡: ${socket.id} user just connected!`);
  socket.on('disconnect', () => {
    console.log('🔥: A user disconnected');
  });

  socket.on('addProduct', (data) => {
    productData['products'].push(data);
    const stringData = JSON.stringify(productData, null, 2);
    fs.writeFile('data.json', stringData, (err) => {
      console.error(err);
    });

    //Sends back the data after adding a new product
    socket.broadcast.emit('addProductResponse', data);
  });

  socket.on('bidProduct', (data) => {
    findProduct(
      data.name,
      productData['products'],
      data.last_bidder,
      data.amount
    );

    //Sends back the data after placing a bid
    socket.broadcast.emit('bidProductResponse', data);
  });
});
Войти в полноэкранный режим Выйти из полноэкранного режима

Socket.io отправляет ответ в приложение React каждый раз, когда пользователь выполняет одно из действий.
Теперь вы можете создать слушателя событий на клиенте и отобразить данные в виде уведомления.

import React, { useState, useEffect } from 'react';

const Nav = ({ socket }) => {
  const [notification, setNotification] = useState('');

  //Listens after a product is added
  useEffect(() => {
    socket.on('addProductResponse', (data) => {
      setNotification(
        `@${data.owner} just added ${data.name} worth $${Number(
          data.price
        ).toLocaleString()}`
      );
    });
  }, [socket]);

  //Listens after a user places a bid
  useEffect(() => {
    socket.on('bidProductResponse', (data) => {
      setNotification(
        `@${data.last_bidder} just bid ${data.name} for $${Number(
          data.amount
        ).toLocaleString()}`
      );
    });
  }, [socket]);

  return (
    <nav className="navbar">
      <div className="header">
        <h2>Bid Items</h2>
      </div>

      <div>
        <p style={{ color: 'red' }}>{notification}</p>
      </div>
    </nav>
  );
};

export default Nav;
Вход в полноэкранный режим Выход из полноэкранного режима

Поздравляем, что вы дошли до этого! 💃🏻

Заключение

Socket.io — это отличный инструмент с превосходными возможностями, который позволяет нам создавать различные приложения реального времени, такие как приложения для чата, приложения для торговли на рынке Форекс и многие другие. Socket.io создает прочные соединения между веб-браузерами и сервером Node.js.

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

Полный код этого руководства доступен на GitHub.

Помогите мне!

Если вы чувствуете, что эта статья помогла вам лучше понять WebSockets! Я буду очень рад, если вы поставите звезду! А также сообщите мне об этом в комментариях ❤️.
https://github.com/novuhq/novu

Спасибо за прочтение! 🚀

Оцените статью
devanswers.ru
Добавить комментарий