Come creare un'applicazione di chat React semplice utilizzando Socket.IO

Pubblicato: 2022-02-10

Ehi, ti sei mai chiesto come funzionano applicazioni come Slack? O su quanto sarebbe difficile creare un'app del genere?

In questo articolo, ti mostreremo una guida passo passo su come creare un'applicazione di chat di reazione semplice simile a Slack usando ReactJS e SocketIO. Costruiremo una versione piuttosto semplificata di tutte le funzionalità che Slack ha da offrire, quindi prendi questo tutorial come un buon esempio di partenza.

Prima di entrare nel nocciolo del lavoro di sviluppo, ci sono alcuni elementi essenziali che devi avere pronti.

I 3 prerequisiti chiave:

  1. Dovresti avere una conoscenza di base di JavaScript.
  2. Dovresti avere NodeJS e NPM installati sul tuo dispositivo.
  3. Avere un IDE o qualsiasi editor di testo preferito.

Una volta impostati questi, analizzeremo i passaggi per ottenere un'applicazione con 3 funzionalità molto semplici:

  1. Accedi fornendo un nickname.
  2. Passa tra i canali forniti staticamente.
  3. Invia messaggi ai canali (incluse le emoji).

Al termine, dovresti avere un'applicazione simile a questa:

Accesso all'app di chat e principale

Hai tutto pronto? Sì!? Andiamo al punto allora, dovremmo...

1. Inizializzare l'applicazione ReactJS

Innanzitutto, dobbiamo creare e inizializzare l'applicazione ReactJS. Per questo useremo create-react-app.

Apri il tuo terminale ed esegui:
npx create-react-app simple-react-js-chat-application

Questo creerà una nuova directory simple-react-js-chat-application con lo scheletro di base di ReactJS. Al momento non esamineremo la struttura del progetto di base.

2. Installa le dipendenze

Il passaggio successivo consiste nell'installare le dipendenze necessarie per il nostro client front-end. Al tuo terminale:

  • Vai alla directory del progetto:
    cd simple-react-js-chat-application
  • Correre:
    npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid

Questo installerà le dipendenze dei prerequisiti:

  • axios – Lo stiamo usando per effettuare chiamate al back-end per recuperare canali e messaggi.
  • emoji-mart – È il componente React per gli emoji.
  • skeleton-css – Un semplice boilerplate CSS reattivo.
  • socket.io-client – ​​Pacchetto NPM per la connessione al socket.
  • uuid – libreria di ID utente univoca
  • node-sass – Useremo SCSS.

3. Creare il server back-end

Per utilizzare Socket.IO dobbiamo creare un server che gestirà gli eventi e alcuni degli endpoint API, ovvero recupererà canali e messaggi. In questo caso, utilizzeremo un server il più semplice possibile gestito in NodeJS.

Inizia con la creazione di un nuovo server di directory nella cartella src . Quindi inizia a creare i seguenti file:

Un file Package.json

Il file package.json specifica la gestione di npm, le dipendenze e le dipendenze dev. È un vero file JSON, non un oggetto JavaScript.

I campi principali di cui Node.JS stesso ha bisogno sono il nome e la versione. Il nome contiene il nome e la versione del progetto: la versione del pacchetto.

 { "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 file Server.js

Questo file segue la logica in base alla quale il server back-end gestisce l'istanza del server, i percorsi personalizzati e gli eventi/emettono listener.

 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 file Users.js

Questo componente è responsabile degli "utenti" dell'applicazione che stiamo costruendo. In questo caso, dobbiamo solo aggiungere/eliminare un utente.

 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 file Messages.js

Questo file aggiunge la funzionalità per i messaggi, ad esempio aggiungendoli all'array e ricevendo messaggi di canale specifici.

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

Un file Channels.js

Questo file contiene la logica per i canali, inizializzando quelli predefiniti e una funzionalità per aggiungere utenti a un canale

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

Esegui il terminale e nella directory installa le dipendenze NPM:

 npm run install

Attendi che NPM finisca e il server è pronto. Puoi provarlo correndo

 npm start

Server dell'app di chat avviato

4. Creare la parte front-end

L'ultima cosa in ordine, ma non in importanza, da realizzare è creare la parte front-end dell'applicazione. Il front-end comunicherà con il server back-end per fornire le funzionalità principali, ad esempio immissione di nickname, passaggio da un canale all'altro e invio di messaggi.

Iniziamo con la schermata iniziale dell'applicazione.
Passa alla cartella src e apri App.js . Quindi sostituisci il suo contenuto con quello qui sotto:

 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;

Il componente App contiene la logica per lo stato del nickname e se un utente è "accesso". Rende anche il componente appropriato, sia esso LoginDialog o Chat, che dipende dallo stato.

Aggiungi un po' di forma

Apri App.css e sostituisci il contenuto con:

 .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 nuove cartelle di supporto e inserisci un file con il nome socket.js all'interno .

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

Questo componente esporta le funzioni di supporto necessarie che useremo più avanti nei componenti React per comunicare con il server back-end.

Siamo quasi pronti!
Ora continueremo con la creazione della finestra di dialogo per fornire il nickname e il layout della chat.

Imposta l'accesso

Crea nuovi componenti della cartella e proseguiamo con la creazione dei componenti necessari per l'applicazione.

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;

Questo è il componente della schermata di accesso che si apre quando l'applicazione viene inizialmente caricata. A questo punto non viene fornito alcun nickname. Contiene solo markup e nulla relativo allo stato qui. Come sembra dal codice, lo stato e i gestori vengono passati tramite prop ad esso.

Dai vita alla chat

Continua con la creazione di un'altra cartella – Chat , in cui creeremo diversi componenti:

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;

Questo è il componente principale che mantiene la logica per l'applicazione di chat, quando un utente è "loggato". Sostiene i gestori e gli stati per i canali, i messaggi e l'invio di messaggi.

Stile su

Continuiamo con gli stili per il componente e i suoi componenti figlio:

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;

Il componente principale per la schermata della chat contiene i componenti ChatMessages e 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;

Contiene la logica e il rendering dei campi di input per il messaggio: il selettore di emoji e la casella di input per il messaggio.

Crea la barra laterale

L'ultimo componente della struttura dell'applicazione contiene la “barra laterale”, ovvero l'elenco dei canali.

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

L'applicazione è pronta, l'unica cosa da considerare qui è controllare l'URL dell'API in helpers/socket.js :

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

Assicurati di cambiarlo in base all'URL e alla PORT per il server back-end che stai utilizzando.

Esegui sia la parte front-end che il server:

Passare alla directory principale ed eseguire:
npm start


Passare a src/server ed eseguire:
npm start

Ora puoi aprire http://localhost:3000 o qualsiasi altra porta che usi per la parte front-end e accedervi.

Tieni presente che questa applicazione ha funzionalità molto semplici. Non c'è autenticazione di accesso: hai solo bisogno di un nickname. E non sarai in grado di creare i tuoi canali: puoi solo passare da quelli statici forniti.

Molto bene!

Hai seguito la nostra guida e ora hai la tua applicazione di chat semplificata simile a Slack.

Sentiti libero di sperimentare ed estendere l'app. Puoi aggiungere cose come:

  • Una funzione "l'utente sta digitando".
  • Una funzione per entrare/creare canali.
  • Autenticazione.
  • Avatar utente.
  • Una capacità di mostrare quali utenti sono attivi online.

Facci sapere come ti piace. E se hai bisogno di ulteriore assistenza, non esitare a contattarci.