什么是 Socket.io?
Socket.io是一个流行的 JavaScript 库,具体请看上一篇文章有介绍Socket.io 时刻:释放实时通信的强大力量
如何通过 Socket.io 将 React.js 应用程序连接到 Node.js
创建包含两个名为 client 和 server 的子文件夹的项目文件夹。
bash
mkdir chat-app
cd chat-app
mkdir client server
通过终端导航到客户端文件夹并创建一个新的 React.js 项目。
bash
cd client
npx create-react-app ./
安装 Socket.io 客户端 API 和 React Router。 React Router 是一个 JavaScript 库,使我们能够在 React 应用程序中的页面之间导航。
lua
npm install socket.io-client react-router-dom
从React应用程序中删除冗余文件,例如徽标和测试文件,并更新文件App.js
以显示Hello World,如下所示。
javascript
function App() {
return (
<div>
<p>Hello World!</p>
</div>
);
}
接下来,导航到服务器文件夹并创建一个package.json
文件。
bash
cd server
npm init -y
安装 Express.js、CORS、Nodemon 和 Socket.io 服务器 API。
Express.js是一个快速、简约的框架,为在 Node.js 中构建 Web 应用程序提供了多种功能。 CORS是一个 Node.js 包,允许不同域之间进行通信。
Nodemon是一个 Node.js 工具,它在检测到文件更改后自动重新启动服务器,而 Socket.io允许我们在服务器上配置实时连接。
lua
npm install express cors nodemon socket.io
创建一个 index.js 文件 - Web 服务器的入口点。
bash
touch index.js
使用 Express.js 设置一个简单的 Node.js 服务器。当您在浏览器中访问时,下面的代码片段会返回一个 JSON 对象http://localhost:8088/api
。
ini
const express = require('express');
const app = express();
const PORT = 8088;
app.get('/api', (req, res) => {
res.json({
message: 'Hello world',
});
});
app.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
导入 HTTP 和 CORS 库以允许客户端和服务器域之间的数据传输。
ini
const express = require('express');
const app = express();
const PORT = 8088;
//New imports
const http = require('http').Server(app);
const cors = require('cors');
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()
,复制以下代码。
javascript
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', () => {
console.log('🔥: A user disconnected');
});
});
从上面的代码片段来看,该socket.io("connection")
函数与 React 应用程序建立了连接,然后为每个套接字创建一个唯一的 ID,并在用户访问网页时将该 ID 记录到控制台。
当您刷新或关闭网页时,套接字会触发断开连接事件,显示用户已与套接字断开连接。
接下来,通过将启动命令添加到文件中的脚本列表来配置 Nodemon package.json
。下面的代码片段使用 Nodemon 启动服务器。
bash
//In server/package.json
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start": "nodemon index.js"
},
现在,您可以使用以下命令通过 Nodemon 运行服务器。
sql
npm start
打开客户端文件夹中的 App.js 文件并将 React 应用程序连接到 Socket.io 服务器。
javascript
import socketIO from 'socket.io-client';
const socket = socketIO.connect('http://localhost:4000');
function App() {
return (
<div>
<p>Hello World!</p>
</div>
);
}
启动 React.js 服务器。
sql
npm start
检查服务器运行的终端;React.js 客户端的 ID 显示在终端中。
至此,React 应用已经通过 Socket.io 成功连接到服务器。
创建聊天应用程序的主页
该文件夹中创建一个名为 Components 的文件夹client/src
。然后,创建主页组件。
bash
cd src
mkdir components & cd components
touch Home.js
将以下代码复制到Home.js
文件中。该代码片段显示一个表单输入,该输入接受用户名并将其存储在本地存储中。
ini
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const Home = () => {
const navigate = useNavigate();
const [userName, setUserName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
localStorage.setItem('userName', userName);
navigate('/chat');
};
return (
<form className="home__container" onSubmit={handleSubmit}>
<h2 className="home__header">Sign in to Open Chat</h2>
<label htmlFor="username">Username</label>
<input
type="text"
minLength={6}
name="username"
id="username"
className="username__input"
value={userName}
onChange={(e) => setUserName(e.target.value)}
/>
<button className="home__cta">SIGN IN</button>
</form>
);
};
export default Home;
接下来,配置 React Router 以启用聊天应用程序页面之间的导航。对于这个应用程序来说,主页和聊天页面就足够了。
将以下代码复制到src/App.js
文件中。
javascript
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './components/Home';
import ChatPage from './components/ChatPage';
import socketIO from 'socket.io-client';
const socket = socketIO.connect('http://localhost:4000');
function App() {
return (
<BrowserRouter>
<div>
<Routes>
<Route path="/" element={<Home socket={socket} />}></Route>
<Route path="/chat" element={<ChatPage socket={socket} />}></Route>
</Routes>
</div>
</BrowserRouter>
);
}
export default App;
该代码片段使用 React Router v6 为应用程序的主页和聊天页面分配不同的路由,并将 Socket.io 库传递到组件中。我们将在接下来的部分中创建聊天页面。
导航到该 src/index.css
文件并复制下面的代码。它包含该项目样式所需的所有 CSS。
css
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap');
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Poppins', sans-serif;
}
.home__container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.home__container > * {
margin-bottom: 10px;
}
.home__header {
margin-bottom: 30px;
}
.username__input {
padding: 10px;
width: 50%;
}
.home__cta {
width: 200px;
padding: 10px;
font-size: 16px;
cursor: pointer;
background-color: #607eaa;
color: #f9f5eb;
outline: none;
border: none;
border-radius: 5px;
}
.chat {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
}
.chat__sidebar {
height: 100%;
background-color: #f9f5eb;
flex: 0.2;
padding: 20px;
border-right: 1px solid #fdfdfd;
}
.chat__main {
height: 100%;
flex: 0.8;
}
.chat__header {
margin: 30px 0 20px 0;
}
.chat__users > * {
margin-bottom: 10px;
color: #607eaa;
font-size: 14px;
}
.online__users > * {
margin-bottom: 10px;
color: rgb(238, 102, 102);
font-style: italic;
}
.chat__mainHeader {
width: 100%;
height: 10vh;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
background-color: #f9f5eb;
}
.leaveChat__btn {
padding: 10px;
width: 150px;
border: none;
outline: none;
background-color: #d1512d;
cursor: pointer;
color: #eae3d2;
}
.message__container {
width: 100%;
height: 80vh;
background-color: #fff;
padding: 20px;
overflow-y: scroll;
}
.message__container > * {
margin-bottom: 10px;
}
.chat__footer {
padding: 10px;
background-color: #f9f5eb;
height: 10vh;
}
.form {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.message {
width: 80%;
height: 100%;
border-radius: 10px;
border: 1px solid #ddd;
outline: none;
padding: 15px;
}
.sendBtn {
width: 150px;
background-color: green;
padding: 10px;
border: none;
outline: none;
color: #eae3d2;
cursor: pointer;
}
.sendBtn:hover {
background-color: rgb(129, 201, 129);
}
.message__recipient {
background-color: #f5ccc2;
width: 300px;
padding: 10px;
border-radius: 10px;
font-size: 15px;
}
.message__sender {
background-color: rgb(194, 243, 194);
max-width: 300px;
padding: 10px;
border-radius: 10px;
margin-left: auto;
font-size: 15px;
}
.message__chats > p {
font-size: 13px;
}
.sender__name {
text-align: right;
}
.message__status {
position: fixed;
bottom: 50px;
font-size: 13px;
font-style: italic;
}
我们已经创建了聊天应用程序的主页。接下来,让我们设计聊天页面的用户界面。
创建应用程序的聊天页面
聊天页面分为三个部分,聊天栏 - 显示活跃用户的侧边栏,包含已发送消息和标题的聊天正文,以及聊天页脚 - 消息框和发送按钮。
由于我们已经能够定义聊天页面的布局,因此您现在可以为设计创建组件。
创建ChatPage.js
文件并将以下代码复制到其中。您将需要 ChatBar、ChatBody 和 ChatFooter 组件。
javascript
import React from 'react';
import ChatBar from './ChatBar';
import ChatBody from './ChatBody';
import ChatFooter from './ChatFooter';
const ChatPage = ({ socket }) => {
return (
<div className="chat">
<ChatBar />
<div className="chat__main">
<ChatBody />
<ChatFooter />
</div>
</div>
);
};
export default ChatPage;
聊天栏组件
将以下代码复制到ChatBar.js
文件中。
javascript
import React from 'react';
const ChatBar = () => {
return (
<div className="chat__sidebar">
<h2>Open Chat</h2>
<div>
<h4 className="chat__header">ACTIVE USERS</h4>
<div className="chat__users">
<p>User 1</p>
<p>User 2</p>
<p>User 3</p>
<p>User 4</p>
</div>
</div>
</div>
);
};
export default ChatBar;
聊天正文组件
在这里,我们将创建显示已发送消息和页面标题的界面。
javascript
import React from 'react';
import { useNavigate } from 'react-router-dom';
const ChatBody = () => {
const navigate = useNavigate();
const handleLeaveChat = () => {
localStorage.removeItem('userName');
navigate('/');
window.location.reload();
};
return (
<>
<header className="chat__mainHeader">
<p>Hangout with Colleagues</p>
<button className="leaveChat__btn" onClick={handleLeaveChat}>
LEAVE CHAT
</button>
</header>
{/*This shows messages sent from you*/}
<div className="message__container">
<div className="message__chats">
<p className="sender__name">You</p>
<div className="message__sender">
<p>Hello there</p>
</div>
</div>
{/*This shows messages received by you*/}
<div className="message__chats">
<p>Other</p>
<div className="message__recipient">
<p>Hey, I'm good, you?</p>
</div>
</div>
{/*This is triggered when a user is typing*/}
<div className="message__status">
<p>Someone is typing...</p>
</div>
</div>
</>
);
};
export default ChatBody;
聊天页脚组件
在这里,我们将在聊天页面底部创建输入和发送按钮。提交表单后,消息和用户名将显示在控制台中。
ini
import React, { useState } from 'react';
const ChatFooter = () => {
const [message, setMessage] = useState('');
const handleSendMessage = (e) => {
e.preventDefault();
console.log({ userName: localStorage.getItem('userName'), message });
setMessage('');
};
return (
<div className="chat__footer">
<form className="form" onSubmit={handleSendMessage}>
<input
type="text"
placeholder="Write message"
className="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button className="sendBtn">SEND</button>
</form>
</div>
);
};
export default ChatFooter;
在 React 应用程序和 Socket.io 服务器之间发送消息
更新ChatPage.js
文件以将 Socket.io 库传递到ChatFooter
组件中。
javascript
import React from 'react';
import ChatBar from './ChatBar';
import ChatBody from './ChatBody';
import ChatFooter from './ChatFooter';
const ChatPage = ({ socket }) => {
return (
<div className="chat">
<ChatBar />
<div className="chat__main">
<ChatBody />
<ChatFooter socket={socket} />
</div>
</div>
);
};
export default ChatPage;
更新组件handleSendMessage
中的函数ChatFooter
以将消息发送到 Node.js 服务器。
javascript
import React, { useState } from 'react';
const ChatFooter = ({ socket }) => {
const [message, setMessage] = useState('');
const handleSendMessage = (e) => {
e.preventDefault();
if (message.trim() && localStorage.getItem('userName')) {
socket.emit('message', {
text: message,
name: localStorage.getItem('userName'),
id: `${socket.id}${Math.random()}`,
socketID: socket.id,
});
}
setMessage('');
};
return <div className="chat__footer">...</div>;
};
export default ChatFooter;
该handleSendMessage
函数在发送包含用户输入、用户名、生成的消息 ID 以及套接字或客户端 ID 的消息事件之前,检查文本字段是否为空以及用户名是否存在于本地存储中(从主页登录)到 Node.js 服务器。
打开index.js
服务器上的文件,更新 Socket.io 代码块以侦听来自 React 应用程序客户端的消息事件,并将消息记录到服务器的终端。
javascript
socketIO.on('connection', (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
//Listens and logs the message to the console
socket.on('message', (data) => {
console.log(data);
});
socket.on('disconnect', () => {
console.log('🔥: A user disconnected');
});
});
我们已经能够在服务器上检索消息;因此,让我们将消息发送给所有连接的客户端。
javascript
socketIO.on('connection', (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
//sends the message to all the users on the server
socket.on('message', (data) => {
socketIO.emit('messageResponse', data);
});
socket.on('disconnect', () => {
console.log('🔥: A user disconnected');
});
});
更新ChatPage.js
文件以侦听来自服务器的消息并将其显示给所有用户。
javascript
import React, { useEffect, useState } from 'react';
import ChatBar from './ChatBar';
import ChatBody from './ChatBody';
import ChatFooter from './ChatFooter';
const ChatPage = ({ socket }) => {
const [messages, setMessages] = useState([]);
useEffect(() => {
socket.on('messageResponse', (data) => setMessages([...messages, data]));
}, [socket, messages]);
return (
<div className="chat">
<ChatBar socket={socket} />
<div className="chat__main">
<ChatBody messages={messages} />
<ChatFooter socket={socket} />
</div>
</div>
);
};
export default ChatPage;
Socket.io 监听通过事件发送的消息messageResponse
并将数据传播到消息数组中。消息数组被传递到ChatBody
组件中以显示在 UI 上。
更新ChatBody.js
文件以呈现消息数组中的数据。
javascript
import React from 'react';
import { useNavigate } from 'react-router-dom';
const ChatBody = ({ messages }) => {
const navigate = useNavigate();
const handleLeaveChat = () => {
localStorage.removeItem('userName');
navigate('/');
window.location.reload();
};
return (
<>
<header className="chat__mainHeader">
<p>Hangout with Colleagues</p>
<button className="leaveChat__btn" onClick={handleLeaveChat}>
LEAVE CHAT
</button>
</header>
<div className="message__container">
{messages.map((message) =>
message.name === localStorage.getItem('userName') ? (
<div className="message__chats" key={message.id}>
<p className="sender__name">You</p>
<div className="message__sender">
<p>{message.text}</p>
</div>
</div>
) : (
<div className="message__chats" key={message.id}>
<p>{message.name}</p>
<div className="message__recipient">
<p>{message.text}</p>
</div>
</div>
)
)}
<div className="message__status">
<p>Someone is typing...</p>
</div>
</div>
</>
);
};
export default ChatBody;
上面的代码片段根据用户发送的消息来显示消息。绿色的消息是您发送的消息,红色的是其他用户的消息。
完美!!!
聊天应用程序现已正常运行。您可以打开多个选项卡并将消息从一个选项卡发送到另一个选项卡。
如何从 Socket.io 获取活跃用户
打开src/Home.js
并创建一个在用户登录时监听用户的事件。更新函数handleSubmit
如下:
javascript
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const Home = ({ socket }) => {
const navigate = useNavigate();
const [userName, setUserName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
localStorage.setItem('userName', userName);
//sends the username and socket ID to the Node.js server
socket.emit('newUser', { userName, socketID: socket.id });
navigate('/chat');
};
return (...)
...
创建一个事件侦听器,每当用户加入或离开聊天应用程序时,该事件侦听器都会更新 Node.js 服务器上的用户数组。
javascript
let users = [];
socketIO.on('connection', (socket) => {
console.log(`⚡: ${socket.id} user just connected!`);
socket.on('message', (data) => {
socketIO.emit('messageResponse', data);
});
//Listens when a new user joins the server
socket.on('newUser', (data) => {
//Adds the new user to the list of users
users.push(data);
// console.log(users);
//Sends the list of users to the client
socketIO.emit('newUserResponse', users);
});
socket.on('disconnect', () => {
console.log('🔥: A user disconnected');
//Updates the list of users when a user disconnects from the server
users = users.filter((user) => user.socketID !== socket.id);
// console.log(users);
//Sends the list of users to the client
socketIO.emit('newUserResponse', users);
socket.disconnect();
});
});
socket.on("newUser")
当新用户加入聊天应用程序时触发。用户的详细信息( socket ID 和用户名)被保存到数组中users
,并发送回 React 应用程序newUserResponse
。
在 中socket.io("disconnect")
,users
当用户离开聊天应用程序时,数组会更新,并且newUserReponse
会触发事件以将更新后的用户列表发送到客户端。
接下来,让我们更新用户界面,ChatBar.js
以显示活动用户列表。
javascript
import React, { useState, useEffect } from 'react';
const ChatBar = ({ socket }) => {
const [users, setUsers] = useState([]);
useEffect(() => {
socket.on('newUserResponse', (data) => setUsers(data));
}, [socket, users]);
return (
<div className="chat__sidebar">
<h2>Open Chat</h2>
<div>
<h4 className="chat__header">ACTIVE USERS</h4>
<div className="chat__users">
{users.map((user) => (
<p key={user.socketID}>{user.userName}</p>
))}
</div>
</div>
</div>
);
};
export default ChatBar;
当用户打字时通知其他人
为了在用户键入时通知用户,我们将onKeyDown
在输入字段上使用 JavaScript 事件侦听器,该事件侦听器会触发向 Socket.io 发送消息的函数,如下所示:
javascript
import React, { useState } from 'react';
const ChatFooter = ({ socket }) => {
const [message, setMessage] = useState('');
const handleTyping = () =>
socket.emit('typing', `${localStorage.getItem('userName')} is typing`);
const handleSendMessage = (e) => {
e.preventDefault();
if (message.trim() && localStorage.getItem('userName')) {
socket.emit('message', {
text: message,
name: localStorage.getItem('userName'),
id: `${socket.id}${Math.random()}`,
socketID: socket.id,
});
}
setMessage('');
};
return (
<div className="chat__footer">
<form className="form" onSubmit={handleSendMessage}>
<input
type="text"
placeholder="Write message"
className="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
{/*OnKeyDown function*/}
onKeyDown={handleTyping}
/>
<button className="sendBtn">SEND</button>
</form>
</div>
);
};
export default ChatFooter;
perl
socketIO.on('connection', (socket) => {
// console.log(`⚡: ${socket.id} user just connected!`);
// socket.on('message', (data) => {
// socketIO.emit('messageResponse', data);
// });
socket.on('typing', (data) => socket.broadcast.emit('typingResponse', data));
// socket.on('newUser', (data) => {
// users.push(data);
// socketIO.emit('newUserResponse', users);
// });
// socket.on('disconnect', () => {
// console.log('🔥: A user disconnected');
// users = users.filter((user) => user.socketID !== socket.id);
// socketIO.emit('newUserResponse', users);
// socket.disconnect();
// });
});
javascript
mport React, { useEffect, useState, useRef } from 'react';
import ChatBar from './ChatBar';
import ChatBody from './ChatBody';
import ChatFooter from './ChatFooter';
const ChatPage = ({ socket }) => {
// const [messages, setMessages] = useState([]);
// const [typingStatus, setTypingStatus] = useState('');
// const lastMessageRef = useRef(null);
// useEffect(() => {
// socket.on('messageResponse', (data) => setMessages([...messages, data]));
// }, [socket, messages]);
// useEffect(() => {
// // 👇️ scroll to bottom every time messages change
// lastMessageRef.current?.scrollIntoView({ behavior: 'smooth' });
// }, [messages]);
useEffect(() => {
socket.on('typingResponse', (data) => setTypingStatus(data));
}, [socket]);
return (
<div className="chat">
<ChatBar socket={socket} />
<div className="chat__main">
<ChatBody
messages={messages}
typingStatus={typingStatus}
lastMessageRef={lastMessageRef}
/>
<ChatFooter socket={socket} />
</div>
</div>
);
};
export default ChatPage;
xml
<div className="message__status">
<p>{typingStatus}</p>
</div>
聊天应用程序竣工啦!
您可以随意通过添加 Socket.io 私人消息传递功能来改进该应用程序,该功能允许用户创建 私人聊天室 和 直接消息传递,使用身份验证库进行用户授权和身份验证,并使用实时数据库进行存储。
结论
Socket.io 是一款出色的工具,具有出色的功能,使我们能够通过在 Web 浏览器和 Node.js 服务器之间创建持久连接来构建高效的实时应用程序,如果您希望在 Node.js 中构建聊天应用程序,那么 Socket.io 可能是一个不错的选择。