如何构建实时拍卖系统 - 将 Socket.io 与 React 连接 🔥(第二部分)
大家好,欢迎回来!
简要回顾
就像真正的拍卖一样,如果你出价购买一件商品,其他竞标者会提出还价。拍卖采用“快速决策”出价机制,如果你出价不够快,其他人就会赢得竞标或出价更高。
要使用在线竞价,我们必须遵循同样的原则。一旦有新的出价,我们必须立即向竞标者提供信息。
本系列的前一篇文章介绍了 Socket.io,如何使用 Socket.io 将 React 应用连接到 Node.js 服务器,以及如何创建竞价系统的用户界面。
要阅读本系列的第一部分,请访问这里:
https://dev.to/novu/how-to-build-a-real-time-auction-system-with-socketio-and-reactjs-3ble
在最后一篇文章中,我将指导您如何在客户端和Node.js服务器之间发送通知和消息。
Novu——首个开源通知架构
简单介绍一下我们。Novu是首个开源通知基础设施。我们主要帮助用户管理所有产品通知,包括应用内通知(类似 Facebook 的铃铛图标 - 基于WebSocket)、电子邮件、短信等等。
如果您能给我们点个赞,我会非常开心!也欢迎在评论区留言 ❤️
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,方便 Web 浏览器获取并显示给用户。
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组件从服务器获取产品信息并将其渲染成表格。
表格中的“编辑”按钮有一个点击事件监听器,该监听器接收与每个产品相关的数据,并使用产品名称和价格跳转到竞价页面。
接下来,我们来学习如何通过 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;
接下来,我们将通过 Socket.io 将产品数据发送到 Node.js 服务器进行存储。我们在文件中将 Socket.io 作为 prop 传递给了每个组件src/App.js。请
从 props 对象中解构 Socket.io,并按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事件通过 Socket.io 将包含产品名称、价格和所有者的对象发送到 Node.js 服务器。
在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 参数接收产品数据,我们需要使用React RouteruseParams提供的钩子。
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>;
};
URLbidProduct包含页面上所选产品的名称和价格Products。该useParams钩子允许我们从 URL 中提取产品名称和价格。然后,我们可以将输入字段(出价)的默认值设置为页面上的当前价格Products。
BidProduct.js通过添加 Socket.io 属性来更新上面的组件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在服务器端创建事件监听器,以接收客户端发送的数据。请按如下方式更新服务器端 index.js 文件中的 Socket.io 代码块:
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 能够在 Web 浏览器和 Node.js 服务器之间建立持久连接。
本项目演示了如何使用 Socket.io 构建应用;您可以通过添加身份验证和创建产品类别来改进此应用程序。
本教程的完整代码可 在 GitHub 上找到。
帮帮我!
如果您觉得这篇文章对您理解 WebSocket 有所帮助,请给我们点个赞!也欢迎在评论区留言 ❤️
https://github.com/novuhq/novu
感谢阅读!🚀
文章来源:https://dev.to/novu/how-to-build-a-real-time-auction-system-connecting-socketio-with-react-20kh



