如何在 Node.js 中管理 socket.io 中的用户?
Socket.IO 是一个库,可以在浏览器和服务器之间实现实时、双向和基于事件的通信。
方法:首先,重要的是要注意,当创建一个新的套接字时,它会被分配一个唯一的 ID,该 ID 通过调用 socket.id 来检索。这个 id 可以存储在用户对象中,我们可以分配一个标识符,例如用户名。对于前端,我们将使用 React,对于后端 node.js 和 express。在主目录名称服务器(后端)和客户端(前端)中创建两个文件夹。 Socket.on 将是一个在需要时将被调用的事件,该事件由 socket.emit 调用,我们在其中调用事件并在需要时传递参数。
首先,我们将创建应用程序的后端部分:
第 1 步:为后端安装依赖项
npm init
npm install cors
npm install express
npm install nodemon
npm install socket.io
npm install http
项目结构:它将如下所示。
第 2 步:创建Index.js。 Socket.on 用于每当用户加入时,前端将发出加入事件,后端将发出消息事件并发送用户已加入的消息。
index.js 中使用了三种方法:
- 连接:当用户从主页加入房间时,将触发连接事件,房间中的每个人都会收到用户已加入的消息,并将用户添加到该房间数据中。
- sendMessage:现在,当任何用户从聊天中的输入发送消息时,sendmessage 事件将被触发,并且消息被添加到带有用户名的消息数组中。
- disconnect:当用户离开聊天时,disconnect 事件将被触发,并且用户从该房间的用户数组中删除,并且将发送用户已离开聊天的消息。
文件名:index.js
Javascript
const express = require('express');
const socketio = require('socket.io');
const http = require('http');
const cors = require('cors');
const { addUser, removeUser, getUser,
getUsersInRoom } = require("./users");
const app = express();
const server = http.createServer(app);
const io = socketio(server);
app.use(cors())
io.on("connection", (socket) => {
socket.on('join', ({ name, room }, callback) => {
const { error, user } = addUser(
{ id: socket.id, name, room });
if (error) return callback(error);
// Emit will send message to the user
// who had joined
socket.emit('message', { user: 'admin', text:
`${user.name},
welcome to room ${user.room}.` });
// Broadcast will send message to everyone
// in the room except the joined user
socket.broadcast.to(user.room)
.emit('message', { user: "admin",
text: `${user.name}, has joined` });
socket.join(user.room);
io.to(user.room).emit('roomData', {
room: user.room,
users: getUsersInRoom(user.room)
});
callback();
})
socket.on('sendMessage', (message, callback) => {
const user = getUser(socket.id);
io.to(user.room).emit('message',
{ user: user.name, text: message });
io.to(user.room).emit('roomData', {
room: user.room,
users: getUsersInRoom(user.room)
});
callback();
})
socket.on('disconnect', () => {
const user = removeUser(socket.id);
if (user) {
io.to(user.room).emit('message',
{ user: 'admin', text:
`${user.name} had left` });
}
})
})
server.listen(process.env.PORT || 5000,
() => console.log(`Server has started.`));
Javascript
const users = [];
const addUser = ({id, name, room}) => {
name = name.trim().toLowerCase();
room = room.trim().toLowerCase();
const existingUser = users.find((user) => {
user.room === room && user.name === name
});
if(existingUser) {
return{error: "Username is taken"};
}
const user = {id,name,room};
users.push(user);
return {user};
}
const removeUser = (id) => {
const index = users.findIndex((user) => {
user.id === id
});
if(index !== -1) {
return users.splice(index,1)[0];
}
}
const getUser = (id) => users
.find((user) => user.id === id);
const getUsersInRoom = (room) => users
.filter((user) => user.room === room);
module.exports = {addUser, removeUser,
getUser, getUsersInRoom};
Javascript
import React from 'react';
import Chat from './components/Chat/Chat';
import Join from './components/Join/Join';
import { BrowserRouter as Router, Route }
from "react-router-dom";
const App = () => {
return (
);
}
export default App;
Javascript
import React, {useState, useEffect} from "react";
import queryString from "query-string";
import io from 'socket.io-client';
import TextContainer from '../TextContainer/TextContainer';
import Messages from '../Messages/Messages';
import InfoBar from '../InfoBar/InfoBar';
import Input from '../Input/Input';
import "./Chat.css";
var connectionOptions = {
"force new connection" : true,
"reconnectionAttempts": "Infinity",
"timeout" : 10000,
"transports" : ["websocket"]
};
var socket = io.connect('https://localhost:5000',connectionOptions);
const Chat = ({location}) => {
const [name, setName] = useState('');
const [room, setRoom] = useState("");
const [users, setUsers] = useState('');
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]);
const ENDPOINT = 'localhost:5000';
useEffect( () => {
const {name, room} = queryString.parse(location.search);
setName(name);
setRoom(room);
socket.emit('join',{name, room}, (error) => {
if(error) {
alert(error);
}
})
return () => {
socket.emit('disconnect');
socket.off();
}
},[ENDPOINT, location.search]);
useEffect( () => {
socket.on('message', (message) => {
setMessages([...messages,message]);
})
socket.on("roomData", ({ users }) => {
setUsers(users);
});
},[messages, users])
//Function for Sending Message
const sendMessage = (e) => {
e.preventDefault();
if(message) {
socket.emit('sendMessage', message, () => setMessage(''))
}
}
console.log(message ,messages);
return (
)
};
export default Chat;
CSS
.outerContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #1A1A1D;
}
.container {
display: flex;
flex-direction: column;
justify-content: space-between;
background: #FFFFFF;
border-radius: 8px;
height: 60%;
width: 35%;
}
@media (min-width: 320px) and (max-width: 480px) {
.outerContainer {
height: 100%;
}
.container {
width: 100%;
height: 100%;
}
}
@media (min-width: 480px) and (max-width: 1200px) {
.container {
width: 60%;
}
}
Javascript
import React from 'react';
import './InfoBar.css';
const InfoBar = ({ room }) => (
{room}
);
export default InfoBar;
CSS
.infoBar {
display: flex;
align-items: center;
justify-content: space-between;
background: #2979FF;
border-radius: 4px 4px 0 0;
height: 60px;
width: 100%;
}
.leftInnerContainer {
flex: 0.5;
display: flex;
align-items: center;
margin-left: 5%;
color: white;
}
.rightInnerContainer {
display: flex;
flex: 0.5;
justify-content: flex-end;
margin-right: 5%;
}
.onlineIcon {
margin-right: 5%;
}
Javascript
import React from 'react';
import './Input.css';
const Input = ({ setMessage, sendMessage, message }) => (
)
export default Input;
CSS
.form {
display: flex;
border-top: 2px solid #D3D3D3;
}
.input {
border: none;
border-radius: 0;
padding: 5%;
width: 80%;
font-size: 1.2em;
}
input:focus, textarea:focus, select:focus{
outline: none;
}
.sendButton {
color: #fff !important;
text-transform: uppercase;
text-decoration: none;
background: #2979FF;
padding: 20px;
display: inline-block;
border: none;
width: 20%;
}
Javascript
import React, {useState} from "react";
import {Link} from 'react-router-dom';
import './Join.css';
const Join = () => {
const [name, setName] = useState('');
const [room, setRoom] = useState("");
return (
Join
setName(event.target.value)} />
setRoom(event.target.value)} />
(!name || !room) ?
e.preventDefault() : null}
to={`/chat?name=${name}&room=${room}`
}>
);
};
export default Join;
CSS
html, body {
font-family: 'Roboto', sans-serif;
padding: 0;
margin: 0;
}
#root {
height: 100vh;
}
* {
box-sizing: border-box;
}
.joinOuterContainer {
display: flex;
justify-content: center;
text-align: center;
height: 100vh;
align-items: center;
background-color: #1A1A1D;
}
.joinInnerContainer {
width: 20%;
}
.joinInput {
border-radius: 0;
padding: 15px 20px;
width: 100%;
}
.heading {
color: white;
font-size: 2.5em;
padding-bottom: 10px;
border-bottom: 2px solid white;
}
.button {
color: #fff !important;
text-transform: uppercase;
text-decoration: none;
background: #2979FF;
padding: 20px;
border-radius: 5px;
display: inline-block;
border: none;
width: 100%;
}
.mt-20 {
margin-top: 20px;
}
@media (min-width: 320px) and (max-width: 480px) {
.joinOuterContainer {
height: 100%;
}
.joinInnerContainer {
width: 90%;
}
}
button:focus {
outline: 0;
}
Javascript
import React from 'react';
import './Message.css';
import ReactEmoji from 'react-emoji';
const Message = ({ message: { text, user }, name }) => {
let isSentByCurrentUser = false;
const trimmedName = name.trim().toLowerCase();
if(user === trimmedName) {
isSentByCurrentUser = true;
}
return (
isSentByCurrentUser
? (
{trimmedName}
{ReactEmoji.emojify(text)}
)
: (
{ReactEmoji.emojify(text)}
{user}
)
);
}
export default Message;
CSS
.messageBox {
background: #F3F3F3;
border-radius: 20px;
padding: 5px 20px;
color: white;
display: inline-block;
max-width: 80%;
}
.messageText {
width: 100%;
letter-spacing: 0;
float: left;
font-size: 1.1em;
word-wrap: break-word;
}
.messageText img {
vertical-align: middle;
}
.messageContainer {
display: flex;
justify-content: flex-end;
padding: 0 5%;
margin-top: 3px;
}
.sentText {
display: flex;
align-items: center;
font-family: Helvetica;
color: #828282;
letter-spacing: 0.3px;
}
.pl-10 {
padding-left: 10px;
}
.pr-10 {
padding-right: 10px;
}
.justifyStart {
justify-content: flex-start;
}
.justifyEnd {
justify-content: flex-end;
}
.colorWhite {
color: white;
}
.colorDark {
color: #353535;
}
.backgroundBlue {
background: #2979FF;
}
.backgroundLight {
background: #F3F3F3;
}
Javascript
import React from 'react';
import Message from './Message/Message';
import './Messages.css';
const Messages = ({ messages, name }) => (
{messages.map((message, i) =>
)}
);
export default Messages;
CSS
.messages {
padding: 5% 0;
overflow: auto;
flex: auto;
}
Javascript
import React from 'react';
import onlineIcon from '../../icons/onlineIcon.png';
import './TextContainer.css';
const TextContainer = ({ users }) => (
{
users
? (
People currently chatting:
{users.map(({name}) => (
{name}
))}
)
: null
}
);
export default TextContainer;
CSS
.textContainer {
display: flex;
flex-direction: column;
margin-left: 100px;
color: rgb(201, 25, 25);
height: 60%;
justify-content: space-between;
}
.activeContainer {
display: flex;
align-items: center;
margin-bottom: 50%;
}
.activeItem {
display: flex;
align-items: center;
}
.activeContainer img {
padding-left: 10px;
}
.textContainer h1 {
margin-bottom: 0px;
}
@media (min-width: 320px) and (max-width: 1200px) {
.textContainer {
display: none;
}
}
第 3 步:创建包含以下函数的 User.js 文件。
这些是存储用户、获取用户、删除用户和在 index.js 文件中使用的函数。
- addUser:每当建立连接或新用户加入时都会调用此函数,并且用户名存储在该房间的数组中,如果该用户名已被该房间内的任何用户使用,则该用户将无法进入聊天。
- removeUser:每当连接被破坏或用户离开并且用户名从该房间的数组中删除时,都会调用此函数。
- getUser:此函数以 id 作为参数,并通过从数组中找到用户名来返回用户名。
- getUsersInRoom:此函数将返回房间中所有用户的姓名以显示在聊天中。
文件名:User.js
Javascript
const users = [];
const addUser = ({id, name, room}) => {
name = name.trim().toLowerCase();
room = room.trim().toLowerCase();
const existingUser = users.find((user) => {
user.room === room && user.name === name
});
if(existingUser) {
return{error: "Username is taken"};
}
const user = {id,name,room};
users.push(user);
return {user};
}
const removeUser = (id) => {
const index = users.findIndex((user) => {
user.id === id
});
if(index !== -1) {
return users.splice(index,1)[0];
}
}
const getUser = (id) => users
.find((user) => user.id === id);
const getUsersInRoom = (room) => users
.filter((user) => user.room === room);
module.exports = {addUser, removeUser,
getUser, getUsersInRoom};
现在我们将处理应用程序的前端部分:
第 1 步:为前端安装 react。
npx create react-app "client"
第 2 步:安装 react 后,在客户端文件夹中安装 Project 的依赖项。
npm install query-string;
npm install react-emoji;
npm install react-router;
npm install socket.io-client;
项目结构:它将如下所示。
第 3 步:在 App.js 中,为页面加入页面和聊天页面创建路由,并导入两个页面的组件以显示在该路由上。
文件名:App.js
Javascript
import React from 'react';
import Chat from './components/Chat/Chat';
import Join from './components/Join/Join';
import { BrowserRouter as Router, Route }
from "react-router-dom";
const App = () => {
return (
);
}
export default App;
第 4 步:在Chat.js中,socket.emit 调用 join 和其他事件,socket.on 创建从后端调用的事件,例如用于存储消息、用户数据。这是主要组件,将通过发送参数(如用户名、要在聊天中显示的消息)来调用所有其他组件。每当用户访问此页面时,都会从后端调用加入事件。每当消息或用户数据发生变化时,即如果任何用户加入或离开或任何用户发布消息事件将被调用,并且 roomdata 事件将在 useEffect 内被调用以显示用户进入或离开的消息并将其存储在消息数组中。信息栏将显示房间的名称。文本容器将显示房间中所有用户的名称。消息将显示聊天中的所有消息。 Input 将接受输入,将其存储在一个数组中,并触发 sendmessage 事件以发送带有用户名的消息。
文件名:Chat.js
Javascript
import React, {useState, useEffect} from "react";
import queryString from "query-string";
import io from 'socket.io-client';
import TextContainer from '../TextContainer/TextContainer';
import Messages from '../Messages/Messages';
import InfoBar from '../InfoBar/InfoBar';
import Input from '../Input/Input';
import "./Chat.css";
var connectionOptions = {
"force new connection" : true,
"reconnectionAttempts": "Infinity",
"timeout" : 10000,
"transports" : ["websocket"]
};
var socket = io.connect('https://localhost:5000',connectionOptions);
const Chat = ({location}) => {
const [name, setName] = useState('');
const [room, setRoom] = useState("");
const [users, setUsers] = useState('');
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]);
const ENDPOINT = 'localhost:5000';
useEffect( () => {
const {name, room} = queryString.parse(location.search);
setName(name);
setRoom(room);
socket.emit('join',{name, room}, (error) => {
if(error) {
alert(error);
}
})
return () => {
socket.emit('disconnect');
socket.off();
}
},[ENDPOINT, location.search]);
useEffect( () => {
socket.on('message', (message) => {
setMessages([...messages,message]);
})
socket.on("roomData", ({ users }) => {
setUsers(users);
});
},[messages, users])
//Function for Sending Message
const sendMessage = (e) => {
e.preventDefault();
if(message) {
socket.emit('sendMessage', message, () => setMessage(''))
}
}
console.log(message ,messages);
return (
)
};
export default Chat;
第 5 步:创建 Chat.css 以将样式添加到 Chat.js。
文件名:Chat.css
CSS
.outerContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #1A1A1D;
}
.container {
display: flex;
flex-direction: column;
justify-content: space-between;
background: #FFFFFF;
border-radius: 8px;
height: 60%;
width: 35%;
}
@media (min-width: 320px) and (max-width: 480px) {
.outerContainer {
height: 100%;
}
.container {
width: 100%;
height: 100%;
}
}
@media (min-width: 480px) and (max-width: 1200px) {
.container {
width: 60%;
}
}
第 6 步:在页面标题上显示房间名称,我们从聊天组件中获取房间名称。
文件名:Infobar.js
Javascript
import React from 'react';
import './InfoBar.css';
const InfoBar = ({ room }) => (
{room}
);
export default InfoBar;
第 7 步:创建 Infobar.css 文件。
文件名:Infobar.css
CSS
.infoBar {
display: flex;
align-items: center;
justify-content: space-between;
background: #2979FF;
border-radius: 4px 4px 0 0;
height: 60px;
width: 100%;
}
.leftInnerContainer {
flex: 0.5;
display: flex;
align-items: center;
margin-left: 5%;
color: white;
}
.rightInnerContainer {
display: flex;
flex: 0.5;
justify-content: flex-end;
margin-right: 5%;
}
.onlineIcon {
margin-right: 5%;
}
第 8 步:用户将输入并发送消息,然后将从后端调用 sendmessage 事件并将数据存储在消息数组中。
文件名:Input.js
Javascript
import React from 'react';
import './Input.css';
const Input = ({ setMessage, sendMessage, message }) => (
)
export default Input;
第 9 步:创建 Input.css 文件。
文件名:Input.css
CSS
.form {
display: flex;
border-top: 2px solid #D3D3D3;
}
.input {
border: none;
border-radius: 0;
padding: 5%;
width: 80%;
font-size: 1.2em;
}
input:focus, textarea:focus, select:focus{
outline: none;
}
.sendButton {
color: #fff !important;
text-transform: uppercase;
text-decoration: none;
background: #2979FF;
padding: 20px;
display: inline-block;
border: none;
width: 20%;
}
第 10 步:从用户输入他们的姓名和要从主页加入的房间以及用户何时加入以将用户发送到聊天页面。
文件名:Join.js
Javascript
import React, {useState} from "react";
import {Link} from 'react-router-dom';
import './Join.css';
const Join = () => {
const [name, setName] = useState('');
const [room, setRoom] = useState("");
return (
Join
setName(event.target.value)} />
setRoom(event.target.value)} />
(!name || !room) ?
e.preventDefault() : null}
to={`/chat?name=${name}&room=${room}`
}>
);
};
export default Join;
第 11 步:创建 Join.css 文件以向 Join.js 组件添加样式。
文件名:Join.css
CSS
html, body {
font-family: 'Roboto', sans-serif;
padding: 0;
margin: 0;
}
#root {
height: 100vh;
}
* {
box-sizing: border-box;
}
.joinOuterContainer {
display: flex;
justify-content: center;
text-align: center;
height: 100vh;
align-items: center;
background-color: #1A1A1D;
}
.joinInnerContainer {
width: 20%;
}
.joinInput {
border-radius: 0;
padding: 15px 20px;
width: 100%;
}
.heading {
color: white;
font-size: 2.5em;
padding-bottom: 10px;
border-bottom: 2px solid white;
}
.button {
color: #fff !important;
text-transform: uppercase;
text-decoration: none;
background: #2979FF;
padding: 20px;
border-radius: 5px;
display: inline-block;
border: none;
width: 100%;
}
.mt-20 {
margin-top: 20px;
}
@media (min-width: 320px) and (max-width: 480px) {
.joinOuterContainer {
height: 100%;
}
.joinInnerContainer {
width: 90%;
}
}
button:focus {
outline: 0;
}
第 12 步:在聊天框中显示消息,如果消息来自当前用户,那么它将在右侧以不同的背景颜色显示,否则它将在左侧以不同的背景颜色显示。
文件名:Message.js
Javascript
import React from 'react';
import './Message.css';
import ReactEmoji from 'react-emoji';
const Message = ({ message: { text, user }, name }) => {
let isSentByCurrentUser = false;
const trimmedName = name.trim().toLowerCase();
if(user === trimmedName) {
isSentByCurrentUser = true;
}
return (
isSentByCurrentUser
? (
{trimmedName}
{ReactEmoji.emojify(text)}
)
: (
{ReactEmoji.emojify(text)}
{user}
)
);
}
export default Message;
第 13 步:Message.css
CSS
.messageBox {
background: #F3F3F3;
border-radius: 20px;
padding: 5px 20px;
color: white;
display: inline-block;
max-width: 80%;
}
.messageText {
width: 100%;
letter-spacing: 0;
float: left;
font-size: 1.1em;
word-wrap: break-word;
}
.messageText img {
vertical-align: middle;
}
.messageContainer {
display: flex;
justify-content: flex-end;
padding: 0 5%;
margin-top: 3px;
}
.sentText {
display: flex;
align-items: center;
font-family: Helvetica;
color: #828282;
letter-spacing: 0.3px;
}
.pl-10 {
padding-left: 10px;
}
.pr-10 {
padding-right: 10px;
}
.justifyStart {
justify-content: flex-start;
}
.justifyEnd {
justify-content: flex-end;
}
.colorWhite {
color: white;
}
.colorDark {
color: #353535;
}
.backgroundBlue {
background: #2979FF;
}
.backgroundLight {
background: #F3F3F3;
}
步骤14:从消息数组和消息组件中的名称中逐一发送消息,然后将其显示给用户。
文件名:Messages.js
Javascript
import React from 'react';
import Message from './Message/Message';
import './Messages.css';
const Messages = ({ messages, name }) => (
{messages.map((message, i) =>
)}
);
export default Messages;
消息.css
CSS
.messages {
padding: 5% 0;
overflow: auto;
flex: auto;
}
第 15 步:我们获取房间中所有用户的数组,并通过应用一些 css 将其显示在页面上。
文件名:TextContainer.js
Javascript
import React from 'react';
import onlineIcon from '../../icons/onlineIcon.png';
import './TextContainer.css';
const TextContainer = ({ users }) => (
{
users
? (
People currently chatting:
{users.map(({name}) => (
{name}
))}
)
: null
}
);
export default TextContainer;
文件名:TextContainer.css
CSS
.textContainer {
display: flex;
flex-direction: column;
margin-left: 100px;
color: rgb(201, 25, 25);
height: 60%;
justify-content: space-between;
}
.activeContainer {
display: flex;
align-items: center;
margin-bottom: 50%;
}
.activeItem {
display: flex;
align-items: center;
}
.activeContainer img {
padding-left: 10px;
}
.textContainer h1 {
margin-bottom: 0px;
}
@media (min-width: 320px) and (max-width: 1200px) {
.textContainer {
display: none;
}
}
运行后端的步骤:进入后端文件夹并打开终端以编写以下命令。
npm start
运行前端的步骤:进入文件夹并打开终端以编写以下命令。
npm start
输出:打开浏览器并输入 localhost 3000 以查看应用程序正在运行。