发布于 2026-01-05 8 阅读
0

如何构建实时拍卖系统 - 将 Socket.io 与 React 连接 🔥(第二部分)

如何构建实时拍卖系统 - 将 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
Enter fullscreen mode Exit fullscreen mode

复制以下代码,向文件中添加一些产品——一个包含不同产品及其价格、名称、所有者和最后出价者的数组。

{
  "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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

更新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}`);
});
Enter fullscreen mode Exit fullscreen mode

接下来,更新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;
Enter fullscreen mode Exit fullscreen mode

从上面的代码片段可以看出,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;
Enter fullscreen mode Exit fullscreen mode

接下来,我们将通过 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;
Enter fullscreen mode Exit fullscreen mode

从上面的代码片段可以看出,该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>
Enter fullscreen mode Exit fullscreen mode

服务器数据

既然我们已经能够访问客户端发送的数据,让我们将数据保存到数据库文件中。

/*
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>
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

从上面的代码片段可以看出,该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);
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
  });
}
Enter fullscreen mode Exit fullscreen mode

该函数接收产品列表、产品名称、最后出价者和产品的新价格作为参数,然后遍历数组中的每个对象,直到找到匹配的产品名称。之后,它会更新文件中该产品的最后出价者和价格data.json

在 Socket.io 代码中调用该函数,以设置所选产品的价格和最后出价者。

....
....
  socket.on('bidProduct', (data) => {
    //Function call
    findProduct(
      data.name,
      productData['products'],
      data.last_bidder,
      data.amount
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

恭喜!用户现在可以在网页应用上竞拍商品了。接下来,我们将学习如何在新增商品或商品上架时通知用户。

通过 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);
  });
});
Enter fullscreen mode Exit fullscreen mode

当用户执行操作时,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;
Enter fullscreen mode Exit fullscreen mode

恭喜你走到这一步!💃🏻

结论

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