实时交流无界限:打造基于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 可能是一个不错的选择。

相关推荐
亭台烟雨中7 分钟前
【前端记事】关于electron的入门使用
前端·javascript·electron
泯泷21 分钟前
「译」解析 JavaScript 中的循环依赖
前端·javascript·架构
Senar1 小时前
Web端选择本地文件的几种方式
前端·javascript·html
烛阴1 小时前
JavaScript 的 8 大“阴间陷阱”,你绝对踩过!99% 程序员崩溃瞬间
前端·javascript·面试
lh_12542 小时前
ECharts 地图开发入门
前端·javascript·echarts
周之鸥3 小时前
使用 Electron 打包可执行文件和资源:完整实战教程
前端·javascript·electron
前端snow3 小时前
前端全栈第二课:用typeorm向数据库添加数据---一对多关系
前端·javascript
全栈老李技术面试3 小时前
【高频考点精讲】async/await原理剖析:Generator和Promise的完美结合
前端·javascript·css·vue·html·react·面试题