使用 React 和 Socket.io 创建一个点赞系统 🥳 🔝
这篇文章是关于什么的?
点赞已经成为了解访客需求的绝佳方式。你可以利用ProductHunt这样的网站,以及Gleap、Upvoty、Prodcamp等公开路线图,让用户分享他们的想法(以投票的形式)。
甚至连Reddit,这个最受欢迎的社交媒体平台之一,也允许人们为你的帖子点赞或点踩。我们也打算用图片来构建类似的功能!
在本文中,您将学习如何创建一个点赞应用程序,让用户使用 Websockets 上传图片并为自己喜欢的照片点赞。您还将学习如何通过 EmailJS 发送电子邮件,在图片获得点赞时通知用户。

为什么选择 Socket.io (Websockets)?
Websockets 允许我们与服务器进行双向通信。这意味着,如果我们点赞了,我们无需刷新页面或使用长轮询即可通知其他用户新的点赞。
Socket.io 是一个流行的 JavaScript 库,它允许我们在软件应用程序和 Node.js 服务器之间创建实时双向通信。它经过优化,能够以最小的延迟处理大量数据,并提供更强大的功能,例如回退到 HTTP 长轮询或自动重新连接。
Novu——第一个开源通知基础设施
简单介绍一下我们。Novu 是第一个开源通知基础设施。我们主要负责管理所有产品通知。这些通知可以是应用内通知(类似 Facebook 的铃铛图标 - Websockets)、电子邮件、短信等等。
如果你能给我们一颗星,我会非常高兴!也请在评论区告诉我❤️
https://github.com/novuhq/novu
如何使用 React 和 Socket.io 创建实时连接
在这里,我们将为图片点赞应用程序搭建项目环境。您还将学习如何将 Socket.io 添加到 React 和 Node.js 应用程序中,以及如何通过 Socket.io 连接两个开发服务器进行实时通信。
创建包含两个名为客户端和服务器的子文件夹的项目文件夹。
mkdir upvote-app
cd upvote-app
mkdir client server
通过终端导航到客户端文件夹并创建一个新的 React.js 项目。
cd client
npx create-react-app ./
安装 Socket.io 客户端 API、React Toastify 和 React Router。React Router是一个 JavaScript 库,它使我们能够在 React 应用程序中的页面之间导航,而 React Toastify用于向用户显示彩色通知。
npm install socket.io-client react-router-dom react-toastify
从 React 应用中删除 logo 和测试文件等冗余文件,并更新 App.js 文件Hello World
以如下所示显示。
function App() {
return (
<div>
<p>Hello World!</p>
</div>
);
}
export default App;
将 Socket.io 客户端 API 添加到 React 应用程序,如下所示:
import { io } from "socket.io-client";
//👇🏻 http://localhost:4000 is where the server host URL.
const socket = io.connect("http://localhost:4000");
function App() {
return (
<div>
<p>Hello World!</p>
</div>
);
}
export default App;
导航到服务器文件夹并创建一个package.json
文件。
cd server & npm init -y
安装 Express.js、CORS、Nodemon 和 Socket.io 服务器 API。
npm install express cors nodemon socket.io react-icons
Express.js 是一个快速、简约的框架,它提供了多种用于在 Node.js 中构建 Web 应用程序的功能。CORS 是一个 Node.js包 ,允许不同域之间进行通信。
Nodemon 是一个 Node.js 工具,它在检测到文件更改后会自动重启服务器,而 Socket.io 允许我们在服务器上配置实时连接。
创建一个 index.js 文件 - Web 服务器的入口点。
touch index.js
使用 Express.js 设置 Node.js 服务器。当您http://localhost:4000/api
在浏览器中访问时,下面的代码片段会返回一个 JSON 对象。
//👇🏻index.js
const express = require("express");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get("/api", (req, res) => {
res.json({
message: "Hello world",
});
});
app.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
导入 HTTP 和 CORS 库以允许客户端和服务器域之间进行数据传输。
const express = require("express");
const app = express();
const PORT = 4000;
//👇🏻 New imports
const http = require("http").Server(app);
const cors = require("cors");
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());
app.get("/api", (req, res) => {
res.json({
message: "Hello world",
});
});
http.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
接下来,将 Socket.io 添加到项目中,以创建实时连接。在app.get()
代码块之前,复制以下代码。
//👇🏻 New imports
.....
const socketIO = require('socket.io')(http, {
cors: {
origin: "http://localhost:3000"
}
});
//👇🏻 Add this before the app.get() block
socketIO.on('connection', (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on('disconnect', () => {
socket.disconnect()
console.log('🔥: A user disconnected');
});
});
从上面的代码片段中,该socket.io("connection")
函数与 React 应用程序建立连接,然后为每个套接字创建一个唯一的 ID,并在用户访问网页时将该 ID 记录到控制台。
当您刷新或关闭网页时,套接字会触发断开事件,表明用户已与套接字断开连接。
通过将启动命令添加到package.json
文件中的脚本列表中来配置 Nodemon。下面的代码片段使用 Nodemon 启动服务器。
//👇🏻 In server/package.json"
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
}
您现在可以使用以下命令通过 Nodemon 运行服务器。
npm start
构建用户界面
在这里,我们将创建点赞应用的用户界面,以便用户登录、上传图片以及为自己选择的任何图片点赞。
构建点赞应用时需要遵循两条规则:
- 用户只能投票一次。
- 用户不能对自己的图像进行点赞。
在本教程的后面,我将指导您如何构建这样一个高效的投票系统。
导航到client/src
文件夹并创建一个包含Login.js
、Register.js
、Photos.js
、UploadPhoto.js
、MyPhotos
和SharePhoto.js
文件的组件文件夹。
cd client
mkdir components
cd components
touch Login.js Register.js Photos.js UploadPhoto.js MyPhoto.js SharePhoto.js
从上面的代码片段来看:
- 该
Login
组件是应用程序的主页。它提示用户登录应用程序。 - 该
Register
组件允许新用户在登录应用程序之前创建一个帐户。 - 该
Photos
组件是用户认证后显示的主页。用户可以在此页面上查看所有可用的图片并进行点赞。 - 只有
UploadPhoto
经过身份验证的用户才能看到,并允许用户将图像上传到 Web 应用程序上的照片列表。 - 该
MyPhoto
页面允许用户仅查看他们上传的图像并与朋友分享他们的个人资料链接。 - 该
SharePhoto
组件是一个动态路由,显示用户上传的所有图像。
更新App.js
文件以通过 React Router 在不同的路由上呈现新创建的组件,如下所示:
import React from "react";
//👇🏻 React Router configuration & routes
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Photos from "./components/Photos";
import Login from "./components/Login";
import Register from "./components/Register";
import UploadPhoto from "./components/UploadPhoto";
import MyPhotos from "./components/MyPhotos";
import SharePhoto from "./components/SharePhoto";
//👇🏻 React Toastify configuration
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
//👇🏻 Websockets configuration
import { io } from "socket.io-client";
const App = () => {
const socket = io.connect("http://localhost:4000");
return (
<>
<BrowserRouter>
<Routes>
<Route path='/' element={<Login socket={socket} />} />
<Route path='/register' element={<Register socket={socket} />} />
<Route path='/photos' element={<Photos socket={socket} />} />
<Route
path='/photo/upload'
element={<UploadPhoto socket={socket} />}
/>
<Route path='/user/photos' element={<MyPhotos socket={socket} />} />
<Route path='/share/:user' element={<SharePhoto socket={socket} />} />
</Routes>
</BrowserRouter>
<ToastContainer />
</>
);
};
export default App;
导航到src/index.css
文件并复制以下代码。它包含设计此项目所需的所有 CSS。
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
* {
margin: 0;
box-sizing: border-box;
font-family: "Space Grotesk", sans-serif;
color: #2b3a55;
}
body {
padding: 0;
margin: 0;
}
button {
border: none;
outline: none;
cursor: pointer;
}
input {
padding: 10px 15px;
}
.navbar {
width: 100%;
min-height: 10vh;
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #f2e5e5;
position: sticky;
top: 0;
z-index: 10;
}
.nav__BtnGroup a,
.nav__BtnGroup button {
padding: 15px;
width: 200px;
font-size: 16px;
cursor: pointer;
outline: none;
background-color: #fff;
border: none;
border-radius: 3px;
text-decoration: none;
}
.nav__BtnGroup a {
margin-right: 10px;
}
.nav__BtnGroup a:hover,
.nav__BtnGroup button:hover {
background-color: #ce7777;
color: #fff;
}
.login,
.register {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.login__form,
.register__form {
width: 500px;
display: flex;
flex-direction: column;
}
.input {
padding: 12px 10px;
margin-bottom: 20px;
border-radius: 4px;
border: 1px solid #e8c4c4;
}
.loginBtn,
.registerBtn {
padding: 15px;
font-size: 16px;
cursor: pointer;
background-color: #f2e5e5;
color: #2b3a55;
border: none;
outline: none;
border-radius: 5px;
margin-bottom: 20px;
}
.loginBtn:hover,
.registerBtn:hover {
background-color: #ce7777;
color: #fff;
}
.link {
color: #ce7777;
cursor: pointer;
text-decoration: none;
}
.photoContainer {
width: 100%;
min-height: 90vh;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.photoContainer > * {
margin: 15px;
}
.photo {
width: 300px;
height: 350px;
position: relative;
border-radius: 10px;
box-shadow: 0 2px 8px 0 rgba(99, 99, 99, 0.2);
}
.imageContainer {
width: 100%;
position: relative;
height: 100%;
}
.photo__image {
width: 100%;
height: 100%;
position: absolute;
object-fit: cover;
border-radius: 10px;
}
.upvoteIcon {
background-color: #fff;
padding: 10px 20px;
position: absolute;
bottom: 5px;
right: 5px;
cursor: pointer;
border-radius: 5px;
display: flex;
flex-direction: column;
align-items: center;
}
.upvoteIcon:hover {
background-color: #f2e5e5;
}
.uploadContainer {
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.uploadText {
width: 80%;
}
.uploadText form {
display: flex;
flex-direction: column;
}
.uploadText > h2 {
margin-bottom: 20px;
}
.uploadBtn {
margin-top: 20px;
padding: 10px;
background-color: #ce7777;
color: #fff;
}
.copyDiv {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.copyContainer {
margin-top: 20px;
padding: 15px;
cursor: pointer;
background-color: #ce7777;
border-radius: 5px;
}
登录页面
将以下代码复制到Login
组件中。应用程序将接受用户的用户名和密码。
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
const Login = ({ socket }) => {
const navigate = useNavigate();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSignIn = (e) => {
if (username.trim() && password.trim()) {
e.preventDefault();
console.log({ username, password });
setPassword("");
setUsername("");
}
};
return (
<div className='login'>
<h2 style={{ marginBottom: "30px" }}>Login</h2>
<form className='login__form' method='POST' onSubmit={handleSignIn}>
<label htmlFor='username'>Username</label>
<input
type='text'
className='input'
name='username'
id='username'
required
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<label htmlFor='password'>Password</label>
<input
type='password'
className='input'
name='password'
id='password'
required
minLength={6}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button className='loginBtn'>LOG IN</button>
<p style={{ textAlign: "center" }}>
Don't have an account?{" "}
<Link className='link' to='/register'>
Create one
</Link>
</p>
</form>
</div>
);
};
export default Login;
注册页面
将以下代码复制到Register.js
文件中以接受用户的电子邮件、用户名和密码。
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
const Register = ({ socket }) => {
const navigate = useNavigate();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
const handleRegister = (e) => {
e.preventDefault();
if (username.trim() && password.trim() && email.trim()) {
console.log({ username, email, password });
setPassword("");
setUsername("");
setEmail("");
}
};
return (
<div className='register'>
<h2 style={{ marginBottom: "30px" }}>Register</h2>
<form className='register__form' method='POST' onSubmit={handleRegister}>
<label htmlFor='email'>Email Address</label>
<input
type='email'
className='input'
name='email'
id='email'
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label htmlFor='username'>Username</label>
<input
type='text'
className='input'
name='username'
id='username'
required
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<label htmlFor='password'>Password</label>
<input
type='password'
className='input'
name='password'
id='password'
required
minLength={6}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button className='registerBtn'>REGISTER</button>
<p style={{ textAlign: "center" }}>
Already have an account?{" "}
<Link className='link' to='/'>
Sign in
</Link>
</p>
</form>
</div>
);
};
export default Register;
照片组件
该组件分为两个子组件,即导航和包含可用图像的主容器。
将下面的代码复制到Photos.js
文件中。
import React, { useEffect, useState } from "react";
import Nav from "./Nav";
import PhotoContainer from "./PhotoContainer";
const Home = ({ socket }) => {
const [photos, setPhotos] = useState([
{
id: "1",
image_url:
"https://raw.githubusercontent.com/novuhq/blog/main/upvote-app-with-react-and-nodejs/server/images/dog1.jpg",
vote_count: 0,
},
{
id: "2",
image_url:
"https://raw.githubusercontent.com/novuhq/blog/main/upvote-app-with-react-and-nodejs/server/images/dog2.jpg",
vote_count: 0,
},
]);
return (
<div>
<Nav />
<PhotoContainer photos={photos} socket={socket} />
</div>
);
};
export default Home;
创建 Nav 和 PhotoContainer 子组件。
touch Nav.js PhotoContainer.js
将下面的代码复制到Nav.js
文件中。
import React from "react";
import { Link } from "react-router-dom";
const Nav = () => {
return (
<nav className='navbar'>
<h3>PhotoShare</h3>
<div className='nav__BtnGroup'>
<Link to='/user/photos' style={{ marginRight: "10px" }}>
My Photos
</Link>
<Link to='/photo/upload'>Upload Photo</Link>
</div>
</nav>
);
};
export default Nav;
更新PhotoContainer.js
文件如下:
import React, { useEffect } from "react";
import { MdOutlineArrowUpward } from "react-icons/md";
const PhotoContainer = ({ photos, socket }) => {
const handleUpvote = (id) => {
console.log("Upvote", id);
};
return (
<main className='photoContainer'>
{photos.map((photo) => (
<div className='photo' key={photo.id}>
<div className='imageContainer'>
<img
src={photo.image_url}
alt={photo.id}
className='photo__image'
/>
</div>
<button className='upvoteIcon' onClick={() => handleUpvote(photo.id)}>
<MdOutlineArrowUpward
style={{ fontSize: "20px", marginBottom: "5px" }}
/>
<p style={{ fontSize: "12px", color: "#ce7777" }}>
{photo.vote_count}
</p>
</button>
</div>
))}
</main>
);
};
export default PhotoContainer;
UploadPhoto 组件
更新UploadPhoto.js
以包含以下代码:
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
const UploadPhoto = ({ socket }) => {
const navigate = useNavigate();
const [photoURL, setPhotoURL] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log(photoURL);
};
return (
<main className='uploadContainer'>
<div className='uploadText'>
<h2>Upload Image</h2>
<form method='POST' onSubmit={handleSubmit}>
<label>Paste the image URL</label>
<input
type='text'
name='fileImage'
value={photoURL}
onChange={(e) => setPhotoURL(e.target.value)}
/>
<button className='uploadBtn'>UPLOAD</button>
</form>
</div>
</main>
);
};
export default UploadPhoto;
MyPhotos 组件
在这里,你将学习如何使用React-copy-to-clipboard库,通过单击按钮复制和粘贴内容 。在该MyPhotos
组件中,用户可以复制自己的个人资料 URL 并与他人共享。
React-copy-to-clipboard是一个包,允许我们通过单击 React 中的按钮来复制和粘贴内容。
通过运行以下代码安装 React-copy-toclipboard库。
npm install react-copy-to-clipboard
将下面的代码复制到MyPhotos.js
文件中。
import React, { useEffect, useState } from "react";
//👇🏻 React Router configs
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import PhotoContainer from "./PhotoContainer";
//👇🏻 React-copy-to-clipboard config
import { CopyToClipboard } from "react-copy-to-clipboard";
const MyPhotos = ({ socket }) => {
const navigate = useNavigate();
const [photos, setPhotos] = useState([]);
const [userLink, setUserLink] = useState("");
//👇🏻 navigates users to the homepage (for now)
const handleSignOut = () => {
localStorage.removeItem("_id");
localStorage.removeItem("_myEmail");
navigate("/");
};
//👇🏻 This function runs immediately the content is copied
const copyToClipBoard = () => alert(`Copied ✅`);
return (
<div>
<nav className='navbar'>
<h3>PhotoShare</h3>
<div className='nav__BtnGroup'>
<Link to='/photo/upload'>Upload Photo</Link>
<button onClick={handleSignOut}>Sign out</button>
</div>
</nav>
<div className='copyDiv'>
<CopyToClipboard
text={userLink}
onCopy={copyToClipBoard}
className='copyContainer'
>
<span className='shareLink'>Copy your share link</span>
</CopyToClipboard>
</div>
<PhotoContainer socket={socket} photos={photos} />
</div>
);
};
export default MyPhotos;
从上面的代码片段中,是React-copy-to-clipboardCopyToClipboard
提供的一个组件 ,它接受两个 props:- 内容和复制内容后运行的函数 - 。text
onCopy
上面的代码片段代表了页面布局。我们将在后续部分中创建该功能。
SharePhoto 组件
将下面的代码复制到SharePhoto.js
文件中。
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import Nav from "./Nav";
import PhotoContainer from "./PhotoContainer";
const SharePhoto = ({ socket }) => {
const navigate = useNavigate();
const [photos, setPhotos] = useState([]);
//👇🏻 This accepts the username from the URL (/share/:user)
const { user } = useParams();
return (
<div>
<Nav />
<PhotoContainer socket={socket} photos={photos} />
</div>
);
};
export default SharePhoto;
恭喜!🥂 您已完成点赞应用程序的用户界面。
在本文的剩余部分,您将学习如何在 React.js 应用程序和 Node.js 服务器之间发送数据。
如何使用 Socket.io 创建身份验证流程
在这里,我将指导您通过 Socket.io 创建身份验证流程。用户可以创建帐户,并登录和退出应用程序。
用户注册工作流程
更新组件handleRegister
内的功能Register.js
,将用户的电子邮件、用户名和密码发送到服务器。
const handleRegister = (e) => {
e.preventDefault();
if (username.trim() && password.trim() && email.trim()) {
//👇🏻 triggers a register event
socket.emit("register", { username, email, password });
setPassword("");
setUsername("");
setEmail("");
}
};
监听服务器上的事件,如下所示:
socketIO.on("connection", (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
//👇🏻 Create a listener to the event
socket.on("register", (data) => {
/*
👇🏻 data will be an object containing the data sent from the React app
*/
console.log(data);
});
socket.on("disconnect", () => {
socket.disconnect();
console.log("🔥: A user disconnected");
});
});
我们已经成功检索了服务器上 React 应用发送的数据。接下来,让我们保存用户的详细信息。
首先,创建一个空数组来保存所有用户详细信息,并创建一个生成随机字符串作为 ID 的函数。
//👇🏻 outside the socket.io block
const database = [];
const generateID = () => Math.random().toString(36).substring(2, 10);
更新事件监听器以保存用户的详细信息。
const database = [];
const generateID = () => Math.random().toString(36).substring(2, 10);
socket.on("register", (data) => {
//👇🏻 Destructure the user details from the object
const { username, email, password } = data;
//👇🏻 Filters the database (array) to check if there is no existing user with the same email or username
let result = database.filter(
(user) => user.email === email || user.username === username
);
//👇🏻 If none, saves the data to the array. (the empty images array is required for the image uploads)
if (result.length === 0) {
database.push({
id: generateID(),
username,
password,
email,
images: [],
});
//👇🏻 returns an event stating that the registration was successful
return socket.emit("registerSuccess", "Account created successfully!");
}
//👇🏻 This runs only when there is an error/the user already exists
socket.emit("registerError", "User already exists");
});
接下来,监听用户在 Web 应用程序上注册时可能触发的两个事件。
//👇🏻 Import toast from React Toastify
import { toast } from "react-toastify";
//👇🏻 Add a useEffect hook that listens to both both events
useEffect(() => {
socket.on("registerSuccess", (data) => {
toast.success(data);
//👇🏻 navigates to the login page
navigate("/");
});
socket.on("registerError", (error) => {
toast.error(error);
});
}, [socket, navigate]);
登录工作流程
更新组件handleSignIn
内的功能Login.js
,将用户的用户名和密码发送到后端服务器。
const handleSignIn = (e) => {
if (username.trim() && password.trim()) {
e.preventDefault();
//👇🏻 triggers a login event
socket.emit("login", { username, password });
setPassword("");
setUsername("");
}
};
在服务器上创建该事件的监听器。
socketIO.on("connection", (socket) => {
//...other functions
socket.on("login", (data) => {
//👇🏻 data - contains the username and password
console.log(data)
})
}
更新事件监听器以将用户登录到 Web 应用程序,如下所示:
socket.on("login", (data) => {
//👇🏻 Destructures the credentials from the object
const { username, password } = data;
//👇🏻 Filters the array for existing objects with the same email and password
let result = database.filter(
(user) => user.username === username && user.password === password
);
//👇🏻 If there is none, it returns this error message
if (result.length !== 1) {
return socket.emit("loginError", "Incorrect credentials");
}
//👇🏻 Returns the user's email & id if the user exists
socket.emit("loginSuccess", {
message: "Login successfully",
data: {
_id: result[0].id,
_email: result[0].email,
},
});
});
听听Login.js
文件中可能发生的两件事。
useEffect(() => {
socket.on("loginSuccess", (data) => {
toast.success(data.message);
//👇🏻 Saves the user's id and email to local storage for easy identification & for making authorized requests
localStorage.setItem("_id", data.data._id);
localStorage.setItem("_myEmail", data.data._email);
//👇🏻 Redirects the user to the Photos component
navigate("/photos");
});
//👇🏻 Notifies the user of the error message
socket.on("loginError", (error) => {
toast.error(error);
});
}, [socket, navigate]);
恭喜!我们已经为应用程序创建了身份验证流程。
最后,让我们通过仅允许经过身份验证的用户查看页面来保护剩余的路由。将下面的代码片段复制到UploadPhoto
、MyPhotos
、SharePhoto
和Photos
组件中。
useEffect(() => {
function authenticateUser() {
const id = localStorage.getItem("_id");
/*
👇🏻 If ID is false, redirects the user to the login page
*/
if (!id) {
navigate("/");
}
}
authenticateUser();
}, [navigate]);
使用 Socket.io 添加和显示图像
在本节中,您将学习如何将图像上传到服务器并通过 Socket.io 在 React 应用程序中显示它们。
通过Socket.io上传图像到服务器
导航到UploadPhoto
组件并更新handleSubmit
功能如下:
const handleSubmit = (e) => {
e.preventDefault();
//👇🏻 Gets the id and email from the local storage
const id = localStorage.getItem("_id");
const email = localStorage.getItem("_myEmail");
/*
👇🏻 triggers an event to the server
containing the user's credentials and the image url
*/
socket.emit("uploadPhoto", { id, email, photoURL });
};
在服务器上创建将图像添加到数据库的事件监听器。
socket.on("uploadPhoto", (data) => {
//👇🏻 Gets the id, email, and image URL
const { id, email, photoURL } = data;
//👇🏻 Search the database for the user
let result = database.filter((user) => user.id === id);
//👇🏻 creates the data structure for the image
const newImage = {
id: generateID(),
image_url: photoURL,
vote_count: 0,
votedUsers: [],
_ref: email,
};
//👇🏻 adds the new image to the images array
result[0]?.images.unshift(newImage);
//👇🏻 sends a new event containing the server response
socket.emit("uploadPhotoMessage", "Upload Successful!");
});
在 React 应用程序中监听服务器的响应。
useEffect(() => {
socket.on("uploadPhotoMessage", (data) => {
//👇🏻 Displays the server's response
toast.success(data);
navigate("/photos");
});
}, [socket, navigate]);
在 React 应用程序中显示图像
在这里,我们将更新Photos
、MyPhotos
和SharePhoto
组件以显示图像。
- 该
Photos
组件显示应用程序内所有可用的照片。 - 该
MyPhotos
组件仅显示用户上传的图像。 - 该
SharePhoto
组件显示用户通过其用户名上传的图像。
照片组件
在文件中添加一个 useEffect 钩子Photos.js
,当组件安装时从服务器检索所有图像。
useEffect(() => {
//👇🏻 search can be anything
socket.emit("allPhotos", "search");
}, [socket]);
监听事件并返回服务器上所有可用的图像。
socket.on("allPhotos", (data) => {
//👇🏻 an array to contain all the images
let images = [];
//👇🏻 loop through the items in the database
for (let i = 0; i < database.length; i++) {
//👇🏻 collect the images into the array
images = images.concat(database[i]?.images);
}
//👇🏻 sends all the images through another event
socket.emit("allPhotosMessage", {
message: "Photos retrieved successfully",
photos: images,
});
});
更新组件内的 useEffect 钩子Photos
以检索图像,如下所示:
useEffect(() => {
socket.emit("allPhotos", "search");
//👇🏻 retrieve all the images from the server
socket.on("allPhotosMessage", (data) => {
setPhotos(data.photos);
});
}, [socket]);
MyPhotos 组件
更新组件内的 useEffect 钩子MyPhotos
以触发通过 Socket.io 将用户 ID 发送到服务器的事件。
useEffect(() => {
function authenticateUser() {
const id = localStorage.getItem("_id");
if (!id) {
navigate("/");
} else {
//👇🏻 sends the user id to the server
socket.emit("getMyPhotos", id);
}
}
authenticateUser();
}, [navigate, socket]);
监听服务器上的事件并返回用户的图像。
socket.on("getMyPhotos", (id) => {
//👇🏻 Filter the database items
let result = database.filter((db) => db.id === id);
//👇🏻 Returns the images and the username
socket.emit("getMyPhotosMessage", {
data: result[0]?.images,
username: result[0]?.username,
});
});
按照以下步骤从服务器检索图像和用户名:
useEffect(() => {
socket.on("getMyPhotosMessage", (data) => {
//👇🏻 sets the user's images
setPhotos(data.data);
//👇🏻 sets the user's profile link
setUserLink(`http://localhost:3000/share/${data.username}`);
});
}, [socket]);
SharePhoto 组件
更新组件useEffect
内的钩子SharePhoto
,以便在组件安装时请求用户的图像。
useEffect(() => {
function authenticateUser() {
const id = localStorage.getItem("_id");
if (!id) {
navigate("/");
} else {
//👇🏻 user - is the username from the profile link
socket.emit("sharePhoto", user);
}
}
authenticateUser();
}, [socket, navigate, user]);
监听服务器上的事件并返回用户的图像。
socket.on("sharePhoto", (name) => {
//👇🏻 Filters the database via the username
let result = database.filter((db) => db.username === name);
//👇🏻 Returns the images via another event
socket.emit("sharePhotoMessage", result[0]?.images);
});
从服务器检索图像如下:
useEffect(() => {
socket.on("sharePhotoMessage", (data) => setPhotos(data));
}, [socket]);
恭喜你完成了这么多!😊 你已经学会了如何操作数据库中的数据,以及如何检索每条路线的商品。在接下来的部分中,我将指导你如何为图片点赞。
如何为图片点赞
现在,我将指导您在 Web 应用程序中为图片点赞。请记住,用户无法为照片点赞,并且只能点赞一次。
更新handleUpvote
其中的功能PhotoContainer.js
以将用户和图像 ID 发送到服务器。
const handleUpvote = (id) => {
socket.emit("photoUpvote", {
userID: localStorage.getItem("_id"),
photoID: id,
});
};
监听服务器上的事件并通过其 ID 对选定的图像进行点赞。
socket.on("photoUpvote", (data) =>
const { userID, photoID } = data;
let images = [];
//👇🏻 saves all the images not belonging to the user into the images array
for (let i = 0; i < database.length; i++) {
//👇🏻 ensures that only other users' images are separated into the images array
if (!(database[i].id === userID)) {
images = images.concat(database[i]?.images);
}
}
//👇🏻 Filter the images array for the image selected for upvote
const item = images.filter((image) => image.id === photoID);
/*
👇🏻 Returns this error if the selected image doesn't belong to other users
*/
if (item.length < 1) {
return socket.emit("upvoteError", {
error_message: "You cannot upvote your photos",
});
}
//👇🏻 Gets the list of voted users from the selected image
const voters = item[0]?.votedUsers;
//👇🏻 Checks if the user has not upvoted the image before
const authenticateUpvote = voters.filter((voter) => voter === userID);
//👇🏻 If true (the first time the user is upvoting the image)
if (!authenticateUpvote.length) {
//👇🏻 increases the vote count
item[0].vote_count += 1;
//👇🏻 adds the user ID to the list of voters
voters.push(userID);
//👇🏻 triggers this event to reflect the change in vote count
socket.emit("allPhotosMessage", {
message: "Photos retrieved successfully",
photos: images,
});
//👇🏻 Returns the upvote response
return socket.emit("upvoteSuccess", {
message: "Upvote successful",
item,
});
}
/*
👇🏻 nullifies duplicate votes. (if the user ID already exists in the array of voted users)
*/
socket.emit("upvoteError", {
error_message: "Duplicate votes are not allowed",
});
});
监听文件中服务器可能的响应PhotoContainer.js
。
useEffect(() => {
socket.on("upvoteSuccess", (data) => {
toast.success(data.message);
//👇🏻 logs the email of the user who owns the image.
console.log(data.item[0]._ref);
});
socket.on("upvoteError", (data) => {
toast.error(data.error_message);
});
}, [socket]);
从上面的代码片段来看,data.item[0]._ref
这是必需的,因为我们希望在用户赞同他们的图像时向他们发送电子邮件通知。
如何在 React 中通过 EmailJS 发送电子邮件
EmailJS 是一个 JavaScript 库,它使我们能够仅通过客户端技术发送电子邮件,而无需服务器。使用 EmailJS,您可以发送文本和电子邮件模板,并在电子邮件中添加附件。
在这里,我将指导您将 EmailJS 添加到 React.js 应用程序,以及如何在用户的图像获得投票时向用户发送电子邮件。
通过运行以下代码将 EmailJS 安装到 React 应用程序:
npm install @emailjs/browser
在此创建一个 EmailJS 帐户 并将电子邮件服务提供商添加到您的帐户。
创建一个电子邮件模板,如下图所示:
更新PhotoContainer.js
文件以便在用户的图像获得投票时向用户发送电子邮件模板。
import emailjs from "@emailjs/browser";
const PhotoContainer = ({ photos, socket }) => {
const handleUpvote = (id) => {
socket.emit("photoUpvote", {
userID: localStorage.getItem("_id"),
photoID: id,
});
};
//👇🏻 The function sends email to the user - (to_email key)
const sendEmail = (email) => {
emailjs
.send(
"YOUR_SERVICE_ID",
"YOUR_TEMPLATE_ID",
{
to_email: email,
from_email: localStorage.getItem("_myEmail"),
},
"YOUR_PUBLIC_KEY"
)
.then(
(result) => {
console.log(result.text);
},
(error) => {
console.log(error.text);
}
);
};
useEffect(() => {
socket.on("upvoteSuccess", (data) => {
toast.success(data.message);
//👇🏻 Pass the image owner email into the function
sendEmail(data.item[0]._ref);
});
socket.on("upvoteError", (data) => {
toast.error(data.error_message);
});
}, [socket]);
return <div>...</div>;
};
您可以从 EmailJS 仪表板的帐户部分获取 EmailJS 公钥。
恭喜!您已完成本教程的项目。
结论
到目前为止,您已经学习了以下内容:
- 如何在 React 和 Node.js 应用程序中设置 Socket.io,
- 使用 Socket.io 和 React 创建身份验证,
- 通过 Socket.io 在服务器和客户端之间进行通信,并且
- 使用 EmailJS 发送电子邮件。
本教程将引导您完成一个使用 Socket.io 和 React 构建的项目。您可以随意添加身份验证库和实时数据库来改进应用程序。
本教程的源代码可以在这里找到:
https://github.com/novuhq/blog/tree/main/upvote-app-with-react-and-nodejs
感谢您的阅读!
帮帮我!
如果您觉得这篇文章帮助您更好地理解了 WebSocket!请给我们一个 Star,我会非常高兴!也请在评论区告诉我❤️
https://github.com/novuhq/novu