实时交流无界限:打造基于Socket.io和React的聊天神器!

什么是 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 可能是一个不错的选择。

相关推荐
joan_8518 分钟前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
还是大剑师兰特41 分钟前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
一个处女座的程序猿O(∩_∩)O3 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
燃先生._.9 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖10 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
black^sugar11 小时前
纯前端实现更新检测
开发语言·前端·javascript
2401_8576009512 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_8576009512 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js