Cum să creați o aplicație simplă React Chat folosind Socket.IO

Publicat: 2022-02-10

Hei, te-ai întrebat vreodată cum funcționează aplicațiile precum Slack? Sau cât de greu ar fi să creezi o astfel de aplicație?

În acest articol, vă vom arăta un ghid pas cu pas despre cum să creați o aplicație de chat de reacție simplă asemănătoare Slack folosind ReactJS și SocketIO. Vom construi o versiune destul de simplificată a tuturor funcțiilor pe care le oferă Slack, așa că luați acest tutorial ca un bun exemplu de pornire.

Înainte de a intra în activitatea de dezvoltare esențială, există câteva elemente esențiale pe care trebuie să le aveți pregătite.

Cele 3 premise cheie:

  1. Ar trebui să aveți cunoștințe de bază JavaScript.
  2. Ar trebui să aveți instalate NodeJS și NPM pe dispozitiv.
  3. Aveți un IDE sau orice editor de text preferat.

Odată ce le aveți setate, vom parcurge pașii pentru a realiza o aplicație care are 3 caracteristici foarte simple:

  1. Conectați-vă prin furnizarea unui pseudonim.
  2. Comutați între canalele furnizate static.
  3. Trimiteți mesaje către canale (inclusiv emoji-uri).

Când terminăm, ar trebui să aveți o aplicație care arată astfel:

Conectare și principal la aplicația de chat

Ai totul gata? Da!? Să trecem la asta atunci, să...

1. Inițializați aplicația ReactJS

În primul rând, trebuie să creăm și să inițializam aplicația ReactJS. Pentru asta vom folosi create-react-app.

Deschideți terminalul și rulați:
npx create-react-app simple-react-js-chat-application

Acest lucru va crea un nou director simple-react-js-chat-application cu scheletul de bază ReactJS. Nu vom parcurge structura proiectului de bază momentan.

2. Instalați dependențe

Următorul pas este să instalați dependențele necesare pentru clientul nostru front-end. La terminalul dvs.:

  • Accesați directorul proiectului:
    cd simple-react-js-chat-application
  • Alerga:
    npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid

Aceasta va instala dependențele de cerințe preliminare:

  • axios – Îl folosim pentru a efectua apeluri către back-end pentru a prelua canale și mesaje.
  • emoji-mart – Este componenta React pentru emoji.
  • skeleton-css – Un simplu boilerplate CSS receptiv.
  • socket.io-client – ​​pachet NPM pentru conectarea la socket.
  • uuid – bibliotecă unică de identificare a utilizatorului
  • node-sass – Vom folosi SCSS.

3. Creați serverul de back-end

Pentru a folosi Socket.IO, trebuie să creăm un server care să se ocupe de evenimente și unele dintre punctele finale API – adică să preia canalele și mesajele. În acest caz, vom folosi un server cât mai simplu posibil, gestionat în NodeJS.

Începeți cu crearea unui nou server de director în folderul src . Apoi începeți să creați următoarele fișiere:

Un fișier Package.json

Fișierul package.json specifică gestionarea npm, dependențele și dependențele dev. Este un fișier JSON real, nu un obiect JavaScript.

Câmpurile principale de care are nevoie Node.JS în sine sunt numele și versiunea. Numele conține numele și versiunea proiectului – versiunea pachetului.

 { "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" } }

Un fișier Server.js

Acest fișier urmează logica conform căreia serverul back-end gestionează instanțierea serverului, rutele personalizate și ascultătorii de evenimente/emite.

 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}`));

Un fișier Users.js

Această componentă este responsabilă pentru „utilizatorii” aplicației pe care o construim. În acest caz, trebuie doar să adăugăm/ștergem un utilizator.

 const users = {}; const addUser = (nickname, socketId) => { users[nickname] = socketId; } const removeUser = (nickname) => { if(users.hasOwnProperty(nickname)) { delete users[nickname]; } } module.exports = { users, addUser, removeUser };

Un fișier Messages.js

Acest fișier adaugă funcționalitatea mesajelor – adică adăugarea lor la matrice și obținerea de mesaje specifice canalului.

 const messages = []; const addMessage = (data) => { messages.push(data); return data; }; const getChannelMessages = (channel) => messages.filter((message) => message.channel === channel); module.exports = { addMessage, getChannelMessages };

Un fișier Channels.js

Acest fișier conține logica canalelor - inițializarea celor implicite și o funcționalitate pentru a adăuga utilizatori la un canal

 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 };

Rulați terminalul și instalați în director dependențele NPM:

 npm run install

Așteptați ca NPM să se termine și serverul este gata. Puteți încerca alergând

 npm start

Serverul aplicației de chat a pornit

4. Creați partea frontală

Ultimul lucru pe care trebuie să-l atingeți, dar nu ca importanță, este să creați partea front-end a aplicației. Front-end-ul va comunica cu serverul back-end pentru a oferi caracteristicile de bază – adică introduceți porecle, comutați între canale și trimiteți mesaje.

Să începem cu ecranul inițial al aplicației.
Navigați la folderul src și deschideți App.js. Apoi înlocuiți conținutul acestuia cu cel de mai jos:

 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;

Componenta aplicație conține logica pentru starea poreclei și dacă un utilizator este „conectat”. De asemenea, redă componenta adecvată, fie că este LoginDialog sau Chat - care depinde de stare.

Adăugați o formă

Deschideți App.css și înlocuiți conținutul cu:

 .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; }

Creați noi asistență pentru foldere și plasați un fișier cu numele socket.js în interior.

 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; };

Această componentă exportă funcțiile de ajutor necesare pe care le vom folosi mai târziu în componentele React pentru a comunica cu serverul back-end.

Suntem aproape gata!
Acum vom continua cu crearea casetei de dialog pentru furnizarea de porecle și aspectul chatului.

Configurați autentificarea

Creați noi componente de folder și să continuăm cu crearea componentelor necesare pentru aplicație.

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;

Aceasta este componenta ecranului de conectare care se deschide când aplicația este încărcată inițial. În acest moment nu este furnizat niciun pseudonim. Conține doar markupuri și nimic legat de starea de aici. După cum se pare din cod, statul și handlerii îi sunt transmise prin elemente de recuzită.

Dă viață chatului

Continuați cu crearea încă un folder – Chat , în care vom crea mai multe componente:

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;

Aceasta este componenta principală care menține logica aplicației de chat, atunci când un utilizator este „conectat”. Deține manevrele și stările pentru canale, mesaje și mesaje trimise.

Da stil

Să continuăm cu stilurile pentru componentă și componentele sale secundare:

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;

Componenta principală pentru ecranul de chat conține componentele ChatMessages și 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;

Conține logica și redarea câmpurilor de intrare pentru mesaj – selectorul de emoji și caseta de introducere a mesajului.

Creați bara laterală

Ultima componentă a structurii aplicației conține „bara laterală” – adică lista de canale.

Channels.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;

Aplicația este gata, singurul lucru de luat în considerare aici este să verificați adresa URL API în helpers/socket.js :

 const SOCKET_URL = "http://localhost:8080";

Asigurați-vă că îl schimbați în consecință la adresa URL și PORT pentru serverul back-end pe care îl utilizați.

Rulați atât partea front-end, cât și serverul:

Navigați la directorul rădăcină și rulați:
npm start


Navigați la src/server și rulați:
npm start

Acum puteți deschide http://localhost:3000 sau orice alt port pe care îl utilizați pentru partea front-end și să îl accesați.

Rețineți că această aplicație are caracteristici foarte de bază. Nu există autentificare de conectare – aveți nevoie doar de o poreclă. Și nu vă veți putea crea propriile canale – puteți comuta doar între cele statice furnizate.

Bine făcut!

Ai urmat ghidul nostru și acum ai propria ta aplicație de chat simplificată asemănătoare Slack.

Simțiți-vă liber să experimentați și să extindeți aplicația. Puteți adăuga lucruri precum:

  • O funcție „utilizatorul tastează”.
  • O caracteristică pentru aderarea/crearea canalelor.
  • Autentificare.
  • Avatare de utilizator.
  • O capacitate de a arăta care utilizatori sunt activi online.

Spune-ne cum îți place. Și dacă aveți nevoie de ajutor suplimentar, nu ezitați să ne contactați.