Всем привет и с возвращением!
Небольшой обзор
Как и на настоящем аукционе, если вы делаете ставку на товар, вы получаете встречные ставки от других участников. Аукцион работает на «быстрой» ставке решения, где кто-то другой выиграет или перебьет вашу ставку, если вы не сделаете ставку достаточно быстро.
Чтобы использовать онлайн-торги, мы должны придерживаться тех же принципов. Мы должны предоставлять информацию о нашем участнике торгов, как только поступает новая ставка.
В предыдущей статье этого цикла мы познакомились с 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
Спасибо за прочтение! 🚀