So erstellen Sie eine einfache React-Chat-Anwendung mit Socket.IO
Veröffentlicht: 2022-02-10Hey, hast du dich jemals gefragt, wie Anwendungen wie Slack funktionieren? Oder wie schwer es wäre, eine solche App zu erstellen?
In diesem Artikel zeigen wir Ihnen eine Schritt-für-Schritt-Anleitung zum Erstellen einer Slack-ähnlichen einfachen Reaktions-Chat-Anwendung mit ReactJS und SocketIO. Wir werden eine ziemlich vereinfachte Version aller Funktionen erstellen, die Slack zu bieten hat, also nehmen Sie dieses Tutorial als gutes Einstiegsbeispiel.
Bevor wir mit der eigentlichen Entwicklungsarbeit beginnen, gibt es einige grundlegende Dinge, die Sie bereit haben müssen.
Die 3 wichtigsten Voraussetzungen:
- Sie benötigen grundlegende JavaScript-Kenntnisse.
- Sie sollten NodeJS und NPM auf Ihrem Gerät installiert haben.
- Haben Sie eine IDE oder einen beliebigen Texteditor.
Sobald Sie diese festgelegt haben, werden wir die Schritte durchgehen, um eine Anwendung mit 3 sehr einfachen Funktionen zu erhalten:
- Melden Sie sich an, indem Sie einen Spitznamen angeben.
- Wechseln Sie zwischen statisch bereitgestellten Kanälen.
- Senden Sie Nachrichten an die Kanäle (inkl. Emojis).
Wenn wir fertig sind, sollten Sie eine Anwendung haben, die so aussieht:
Haben Sie alles bereit? Jawohl!? Kommen wir also dazu, sollen wir…
1. Initialisieren Sie die ReactJS-Anwendung
Zuerst müssen wir die ReactJS-Anwendung erstellen und initialisieren. Dafür verwenden wir create-react-app.
Öffnen Sie Ihr Terminal und führen Sie Folgendes aus:
npx create-react-app simple-react-js-chat-application
Dadurch wird ein neues Verzeichnis simple-react-js-chat-application mit dem grundlegenden ReactJS-Skelett erstellt. Auf die Struktur des Basisprojekts gehen wir im Moment nicht ein.
2. Installieren Sie Abhängigkeiten
Der nächste Schritt besteht darin, die erforderlichen Abhängigkeiten für unseren Front-End-Client zu installieren. An Ihrem Endgerät:
- Wechseln Sie in das Projektverzeichnis:
cd simple-react-js-chat-application
- Laufen:
npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid
Dadurch werden die vorausgesetzten Abhängigkeiten installiert:
- axios – Wir verwenden es, um Anrufe an das Back-End zu tätigen, um Kanäle und Nachrichten abzurufen.
- emoji-mart – Es ist eine React-Komponente für Emojis.
- skeleton-css – Eine einfache responsive CSS-Boilerplate.
- socket.io-client – NPM-Paket zum Verbinden mit dem Socket.
- uuid – eindeutige Benutzer-ID-Bibliothek
- node-sass – Wir verwenden SCSS.
3. Erstellen Sie den Back-End-Server
Um Socket.IO zu verwenden, müssen wir einen Server erstellen, der die Ereignisse und einige der API-Endpunkte verarbeitet – dh Kanäle und Nachrichten abruft. In diesem Fall verwenden wir einen möglichst einfachen Server, der in NodeJS verwaltet wird.
Beginnen Sie mit der Erstellung eines neuen Verzeichnisservers im Ordner src . Beginnen Sie dann mit der Erstellung der folgenden Dateien:
Eine Package.json-Datei
Die Datei „package.json“ gibt die npm-Verarbeitung, Abhängigkeiten und Entwicklerabhängigkeiten an. Es ist eine tatsächliche JSON-Datei, kein JavaScript-Objekt.
Die Hauptfelder, die Node.JS selbst benötigt, sind Name und Version. Der Name enthält den Namen und die Version des Projekts – die Paketversion.
{ "name": "server", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "cors": "^2.8.5", "express": "^4.17.1", "socket.io": "^3.0.4", "uuid": "^8.3.2" } }
Eine Server.js-Datei
Diese Datei folgt der Logik, dass der Back-End-Server die Serverinstanziierung, benutzerdefinierte Routen und die Ereignis-/Emitter-Listener verarbeitet.
const http = require("http"); const express = require("express"); const cors = require("cors"); const socketIO = require("socket.io"); const { addMessage, getChannelMessages } = require("./messages"); const { channels, addUserToChannel } = require("./channels"); const { addUser, removeUser } = require("./users"); const app = express(); app.use(cors()); const server = http.createServer(app); const io = socketIO(server, { cors: { origin: "*", }, }); const PORT = process.env.PORT || 8080; io.on("connection", (socket) => { // Get nickname and channel. const { nickname, channel } = socket.handshake.query; console.log(`${nickname} connected`); // Join the user to the channel. socket.join(channel); addUser(nickname, socket.id); addUserToChannel(channel, nickname); // Handle disconnect socket.on("disconnect", () => { console.log(`${nickname} disconnected`); removeUser(nickname); }); socket.on("CHANNEL_SWITCH", (data) => { const { prevChannel, channel } = data; if (prevChannel) { socket.leave(prevChannel); } if (channel) { socket.join(channel); } }); socket.on("MESSAGE_SEND", (data) => { addMessage(data); const { channel } = data; socket.broadcast.to(channel).emit("NEW_MESSAGE", data); }); }); app.get("/channels/:channel/messages", (req, res) => { const allMessages = getChannelMessages(req.params.channel); return res.json({ allMessages }); }); app.get("/getChannels", (req, res) => { return res.json({ channels }); }); server.listen(PORT, () => console.log(`Server listening to port ${PORT}`));
Eine Users.js-Datei
Diese Komponente ist für die „Benutzer“ der von uns erstellten Anwendung verantwortlich. In diesem Fall müssen wir nur einen Benutzer hinzufügen/löschen.
const users = {}; const addUser = (nickname, socketId) => { users[nickname] = socketId; } const removeUser = (nickname) => { if(users.hasOwnProperty(nickname)) { delete users[nickname]; } } module.exports = { users, addUser, removeUser };
Eine Messages.js-Datei
Diese Datei fügt die Funktionalität für die Nachrichten hinzu – dh sie zum Array hinzufügen und bestimmte Kanalnachrichten erhalten.
const messages = []; const addMessage = (data) => { messages.push(data); return data; }; const getChannelMessages = (channel) => messages.filter((message) => message.channel === channel); module.exports = { addMessage, getChannelMessages };
Eine Channels.js-Datei
Diese Datei enthält die Logik für die Kanäle – die Initialisierung der Standardkanäle und eine Funktion zum Hinzufügen von Benutzern zu einem Kanal
const channels = [ { id: 1, name: "general", users: [], }, { id: 2, name: "test 1", users: [], }, { id: 3, name: "test 2", users: [], }, ]; const addUserToChannel = (channel, nickname) => { channels.filter((c) => c.name === channel).map((c) => { c.users.push(nickname); return c; }); } module.exports = { channels, addUserToChannel };
Führen Sie das Terminal aus und installieren Sie im Verzeichnis die NPM-Abhängigkeiten:
npm run install
Warten Sie, bis NPM abgeschlossen ist und der Server bereit ist. Sie können es versuchen, indem Sie laufen
npm start
4. Erstellen Sie das Frontend-Teil
Das Letzte, was in Ordnung, aber nicht wichtig ist, ist die Erstellung des Front-End-Teils der Anwendung. Das Front-End kommuniziert mit dem Back-End-Server, um die Kernfunktionen bereitzustellen – dh Spitznamen eingeben, zwischen Kanälen wechseln und Nachrichten senden.
Beginnen wir mit dem Startbildschirm der Anwendung.
Navigieren Sie zum Ordner src und öffnen Sie App.js . Ersetzen Sie dann den Inhalt durch den folgenden:
import "./index.css"; import "./App.css"; import { useState } from "react"; import Chat from "./components/Chat/Chat"; import LoginDialog from "./components/LoginDialog"; function App() { const [nickname, setNickname] = useState(""); const [loggedIn, setLoggedIn] = useState(false); const handleNicknameChange = (event) => { setNickname(event.target.value.trim()); }; const handleNicknameSubmit = (e) => { if (!nickname.length) return; e.preventDefault(); setLoggedIn(true); }; return ( <div className="main-div"> {!loggedIn ? ( <LoginDialog nicknameChange={handleNicknameChange} nicknameSubmit={handleNicknameSubmit} /> ) : ( <Chat nickname={nickname} /> )} </div> ); } export default App;
Die App-Komponente enthält die Logik für den Status des Spitznamens und ob ein Benutzer „eingeloggt“ ist. Es rendert auch die entsprechende Komponente, sei es der LoginDialog oder der Chat – was vom Zustand abhängt.
Fügen Sie eine Form hinzu
Öffnen Sie App.css und ersetzen Sie den Inhalt durch:
.login-container { position: fixed; left: 10%; right: 10%; top: 50%; transform: translate(0, -50%); display: flex; flex-direction: column; } .text-input-field { padding: 24px 12px; border-radius: 7px; font-size: 24px; } .text-input-field:focus { outline: none; } .login-button { margin-top: 20px; padding: 24px 12px; font-size: 28px; background-color: rgb(0, 132, 255); color: white; font-weight: 600; text-align: center; text-decoration: none; border-radius: 7px; }
Erstellen Sie neue Ordnerhelfer und platzieren Sie eine Datei mit dem Namen socket.js darin.
import io from "socket.io-client"; import axios from "axios"; let socket; const SOCKET_URL = "http://localhost:8080"; export const initiateSocket = (channel, nickname) => { socket = io(SOCKET_URL, { query: { channel, nickname }, }); console.log("Connecting to socket"); if (socket && channel) { socket.emit("CHANNEL_JOIN", channel); } }; export const switchChannel = (prevChannel, channel) => { if (socket) { socket.emit("CHANNEL_SWITCH", { prevChannel, channel }); } }; export const subscribeToMessages = (callback) => { if (!socket) { return; } socket.on("NEW_MESSAGE", (data) => { callback(null, data); }); }; export const sendMessage = (data) => { if (!socket) { return; } socket.emit("MESSAGE_SEND", data); }; export const fetchChannels = async () => { const response = await axios.get(`${SOCKET_URL}/getChannels`); return response.data.channels; }; export const fetchChannelMessages = async (channel) => { const response = await axios.get( `${SOCKET_URL}/channels/${channel}/messages` ); return response.data.allMessages; };
Diese Komponente exportiert notwendige Hilfsfunktionen, die wir später in den React-Komponenten verwenden werden, um mit dem Back-End-Server zu kommunizieren.
Wir sind fast fertig!
Nun geht es weiter mit der Erstellung des Dialogs zur Nickname-Vergabe und des Chat-Layouts.
Richten Sie die Anmeldung ein
Erstellen Sie neue Ordnerkomponenten und fahren Sie mit der Erstellung der erforderlichen Komponenten für die Anwendung fort.
LoginDialog.js:
function LoginDialog({ nicknameChange, nicknameSubmit }) { return ( <div className="dialog-container"> <div className="dialog"> <form className="dialog-form" onSubmit={nicknameSubmit}> <label className="username-label" htmlFor="username"> Nickname: </label> <input className="username-input" autoFocus onChange={nicknameChange} type="text" name="userId" placeholder="Enter your nickname to continue" /> <button type="submit" className="submit-btn"> Continue </button> </form> </div> </div> ); } export default LoginDialog;
Dies ist die Komponente des Anmeldebildschirms, die geöffnet wird, wenn die Anwendung zum ersten Mal geladen wird. An dieser Stelle wird kein Spitzname angegeben. Es enthält nur Markups und nichts, was sich hier auf den Zustand bezieht. Wie es aus dem Code hervorgeht, werden der Zustand und die Handler durch Requisiten an ihn weitergegeben.

Erwecken Sie den Chat zum Leben
Fahren Sie mit dem Erstellen eines weiteren Ordners fort – Chat , in dem wir mehrere Komponenten erstellen werden:
Chat.js
import { useEffect, useRef, useState } from "react"; import "skeleton-css/css/normalize.css"; import "skeleton-css/css/skeleton.css"; import "./Chat.scss"; import { initiateSocket, switchChannel, fetchChannels, fetchChannelMessages, sendMessage, subscribeToMessages, } from "../../helpers/socket"; import { v4 as uuidv4 } from "uuid"; import "emoji-mart/css/emoji-mart.css"; import Channels from "./Channels"; import ChatScreen from "./ChatScreen"; function Chat({ nickname }) { const [message, setMessage] = useState(""); const [channel, setChannel] = useState("general"); const [channels, setChannels] = useState([]); const [messages, setMessages] = useState([]); const [messagesLoading, setMessagesLoading] = useState(true); const [channelsLoading, setChannelsLoading] = useState(true); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const prevChannelRef = useRef(); useEffect(() => { prevChannelRef.current = channel; }); const prevChannel = prevChannelRef.current; useEffect(() => { if (prevChannel && channel) { switchChannel(prevChannel, channel); setChannel(channel); } else if (channel) { initiateSocket(channel, nickname); } }, [channel]); useEffect(() => { setMessages([]); setMessagesLoading(true); fetchChannelMessages(channel).then((res) => { setMessages(res); setMessagesLoading(false); }); }, [channel]); useEffect(() => { fetchChannels().then((res) => { setChannels(res); setChannelsLoading(false); }); subscribeToMessages((err, data) => { setMessages((messages) => [...messages, data]); }); }, []); const handleMessageChange = (event) => { setMessage(event.target.value); }; const handleMessageSend = (e) => { if (!message) return; e.preventDefault(); const data = { id: uuidv4(), channel, user: nickname, body: message, time: Date.now(), }; setMessages((messages) => [...messages, data]); sendMessage(data); setMessage(""); }; const handleEmojiSelect = (emoji) => { const newText = `${message}${emoji.native}`; setMessage(newText); setShowEmojiPicker(false); }; return ( <div className="chat-container"> <Channels nickname={nickname} channelsLoading={channelsLoading} channels={channels} }channel={channel} setChannel={setChannel} /> <ChatScreen channel={channel} messagesLoading={messagesLoading} messages={messages} showEmojiPicker={showEmojiPicker} handleEmojiSelect={handleEmojiSelect} handleMessageSend={handleMessageSend} setShowEmojiPicker={setShowEmojiPicker} message={message} handleMessageChange={handleMessageChange} /> </div> ); export default Chat;
Dies ist die Hauptkomponente, die die Logik für die Chat-Anwendung aufrechterhält, wenn ein Benutzer „eingeloggt“ ist. Es enthält die Handler und Zustände für die Kanäle, Nachrichten und das Senden von Nachrichten.
Gestalten Sie es auf
Fahren wir mit den Stilen für die Komponente und ihre untergeordneten Komponenten fort:
Chat.scss
.chat-container { width: 100vw; height: 100vh; display: grid; grid-template-columns: 1fr 4fr; } .right-sidebar { border-left: 1px solid #ccc; } .left-sidebar { border-right: 1px solid #ccc; } .user-profile { height: 70px; display: flex; align-items: flex-start; padding-right: 20px; padding-left: 20px; justify-content: center; flex-direction: column; border-bottom: 1px solid #ccc; } .user-profile span { display: block; } .user-profile .username { font-size: 20px; font-weight: 700; } .chat-channels li, .room-member { display: flex; align-items: center; padding: 15px 20px; font-size: 18px; color: #181919; cursor: pointer; border-bottom: 1px solid #eee; margin-bottom: 0; } .room-member { justify-content: space-between; padding: 0 20px; height: 60px; } .send-dm { opacity: 0; pointer-events: none; font-size: 20px; border: 1px solid #eee; border-radius: 5px; margin-bottom: 0; padding: 0 10px; line-height: 1.4; height: auto; } .room-member:hover .send-dm { opacity: 1; pointer-events: all; } .presence { display: inline-block; width: 10px; height: 10px; background-color: #ccc; margin-right: 10px; border-radius: 50%; } .presence.online { background-color: green; } .chat-channels .active { background-color: #eee; color: #181919; } .chat-channels li:hover { background-color: #d8d1d1; } .room-icon { display: inline-block; margin-right: 10px; } .chat-screen { display: flex; flex-direction: column; height: 100vh; } .chat-header { height: 70px; flex-shrink: 0; border-bottom: 1px solid #ccc; padding-left: 10px; padding-right: 20px; display: flex; flex-direction: column; justify-content: center; } .chat-header h3 { margin-bottom: 0; text-align: center; } .chat-messages { flex-grow: 1; overflow-y: auto; display: flex; flex-direction: column; justify-content: flex-end; margin-bottom: 0; min-height: min-content; position: relative; } .message { padding-left: 20px; padding-right: 20px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; } .message span { display: block; text-align: left; } .message .user-id { font-weight: bold; } .message-form { border-top: 1px solid #ccc; width: 100%; display: flex; align-items: center; } .message-form, .message-input { width: 100%; margin-bottom: 0; } .rta { flex-grow: 1; } .emoji-mart { position: absolute; bottom: 20px; right: 10px; } input[type="text"].message-input, textarea.message-input { height: 50px; flex-grow: 1; line-height: 35px; padding-left: 20px; border-radius: 0; border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 0; border: none; font-size: 16px; color: #333; min-height: auto; overflow-y: hidden; resize: none; border-left: 1px solid #ccc; } .message-input:focus { outline: none; } .toggle-emoji { border: none; width: 50px; height: auto; padding: 0; margin-bottom: 0; display: flex; align-items: center; justify-content: center; } .toggle-emoji svg { width: 28px; height: 28px; } /* RTA ========================================================================== */ .rta { position: relative; border-left: 1px solid #ccc; display: flex; flex-direction: column; } .rta__autocomplete { position: absolute; width: 300px; background-color: white; border: 1px solid #ccc; border-radius: 5px; } .rta__autocomplete ul { list-style: none; text-align: left; margin-bottom: 0; } .rta__autocomplete li { margin-bottom: 5px; padding: 3px 20px; cursor: pointer; } .rta__autocomplete li:hover { background-color: skyblue; } /* Dialog ========================================================================== */ .dialog-container { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; } .dialog { width: 500px; background-color: white; display: flex; align-items: center; } .dialog-form { width: 100%; margin-bottom: 0; padding: 20px; } .dialog-form > * { display: block; } .username-label { text-align: left; font-size: 16px; } .username-input { width: 100%; } input[type="text"]:focus { border-color: #5c8436; } .submit-btn { color: #5c8436; background-color: #181919; width: 100%; } .submit-btn:hover { color: #5c8436; background-color: #222; }
ChatMessages.js
import ChatMessages from "./ChatMessages"; import MessageForm from "./MessageForm"; function ChatScreen({ channel, messagesLoading, messages, showEmojiPicker, handleEmojiSelect, handleMessageSend, setShowEmojiPicker, message, handleMessageChange, }) { return ( <section className="chat-screen"> <header className="chat-header"> <h3>#{channel}</h3> </header> <ChatMessages messagesLoading={messagesLoading} messages={messages} /> <footer className="chat-footer"> <MessageForm emojiSelect={handleEmojiSelect} handleMessageSend={handleMessageSend} setShowEmojiPicker={setShowEmojiPicker} showEmojiPicker={showEmojiPicker} message={message} handleMessageChange={handleMessageChange} /> </footer> </section> ); } export default ChatScreen;
Die Hauptkomponente für den Chat-Bildschirm enthält die Komponenten ChatMessages und MessageForm.
MessageForm.js
import { Smile } from "react-feather"; import { Picker } from "emoji-mart"; function MessageForm({ emojiSelect, handleMessageSend, setShowEmojiPicker, showEmojiPicker, message, handleMessageChange, }) { let messageInput; const handleEmojiSelect = (emoji) => { emojiSelect(emoji); messageInput.focus(); }; return ( <div> {showEmojiPicker ? ( <Picker title="" set="apple" onSelect={handleEmojiSelect} /> ) : null} <form onSubmit={handleMessageSend} className="message-form"> <button type="button" onClick={() => setShowEmojiPicker(!showEmojiPicker)} className="toggle-emoji" > <Smile /> </button> <input type="text" value={message} ref={(input) => (messageInput = input)} onChange={handleMessageChange} placeholder="Type your message here..." className="message-input" /> </form> </div> ); } export default MessageForm;
Enthält die Logik und Darstellung der Eingabefelder für die Nachricht – den Emoji-Picker und das Eingabefeld für die Nachricht.
Erstellen Sie die Seitenleiste
Die letzte Komponente der Anwendungsstruktur enthält die „Sidebar“ – also die Senderliste.
Kanäle.js
import { useState } from "react"; function Channels({ nickname, channelsLoading, channels, channel, setChannel, }) { return ( <aside className="sidebar left-sidebar"> <div className="user-profile"> <span className="username">@ {nickname}</span> </div> <div className="channels"> <ul className="chat-channels"> {channelsLoading ? ( <li> <span className="channel-name">Loading channels....</span> </li> ) : channels.length ? ( channels.map((c) => { return ( <li key={c.id} onClick={() => setChannel(c.name)} className={c.name === channel ? "active" : ""} > <span className="channel-name">{c.name}</span> </li> ); }) ) : ( <li> <span className="channel-name">No channels available</span> </li> )} </ul> </div> </aside> ); } export default Channels;
Die Anwendung ist fertig, das einzige, was hier zu beachten ist, ist die Überprüfung der API-URL in helpers/socket.js :
const SOCKET_URL = "http://localhost:8080";
Stellen Sie sicher, dass Sie es entsprechend auf die URL und den PORT für den von Ihnen verwendeten Back-End-Server ändern.
Führen Sie sowohl den Front-End-Teil als auch den Server aus:
Navigieren Sie zum Stammverzeichnis und führen Sie Folgendes aus:
npm start
Navigieren Sie zu src/server und führen Sie Folgendes aus:
npm start
Jetzt können Sie http://localhost:3000 oder einen anderen Port öffnen, den Sie für den Frontend-Teil verwenden, und darauf zugreifen.
Denken Sie daran, dass diese Anwendung sehr grundlegende Funktionen hat. Es gibt keine Login-Authentifizierung – Sie brauchen nur einen Spitznamen. Und Sie können keine eigenen Kanäle erstellen – Sie können nur zwischen den bereitgestellten statischen Kanälen wechseln.
Gut gemacht!
Sie sind unserem Leitfaden gefolgt und haben jetzt Ihre eigene vereinfachte Slack-ähnliche Chat-Anwendung.
Fühlen Sie sich frei, zu experimentieren und die App zu erweitern. Sie können Dinge hinzufügen wie:
- Eine „Benutzer tippt“-Funktion.
- Eine Funktion zum Beitreten/Erstellen von Kanälen.
- Authentifizierung.
- Benutzer-Avatare.
- Eine Möglichkeit, anzuzeigen, welche Benutzer online aktiv sind.
Lassen Sie uns wissen, wie es Ihnen gefällt. Und wenn Sie weitere Hilfe benötigen, zögern Sie nicht, uns zu kontaktieren.