Comment créer une application de chat React simple à l'aide de Socket.IO
Publié: 2022-02-10Hé, vous êtes-vous déjà demandé comment fonctionnent des applications comme Slack ? Ou à quel point il serait difficile de créer une application comme celle-là ?
Dans cet article, nous allons vous montrer un guide étape par étape sur la façon de créer une application de chat de réaction simple semblable à Slack en utilisant ReactJS et SocketIO. Nous allons construire une version plutôt simplifiée de toutes les fonctionnalités que Slack a à offrir, alors prenez ce tutoriel comme un bon exemple de départ.
Avant d'entrer dans les détails du travail de développement, vous devez préparer certains éléments essentiels.
Les 3 prérequis clés :
- Vous auriez besoin d'avoir des connaissances de base en JavaScript.
- NodeJS et NPM doivent être installés sur votre appareil.
- Avoir un IDE, ou tout éditeur de texte préférable.
Une fois ceux-ci définis, nous passerons en revue les étapes pour réaliser une application qui possède 3 fonctionnalités très simples :
- Connectez-vous en fournissant un pseudo.
- Basculez entre les canaux fournis statiquement.
- Envoyez des messages aux chaînes (y compris les emoji).
Lorsque nous aurons terminé, vous devriez avoir une application qui ressemble à ceci :
Avez-vous tout prêt? Oui!? Allons-y alors, allons-nous…
1. Initialiser l'application ReactJS
Tout d'abord, nous devons créer et initialiser l'application ReactJS. Pour cela, nous utiliserons create-react-app.
Ouvrez votre terminal et lancez :
npx create-react-app simple-react-js-chat-application
Cela créera un nouveau répertoire simple-react-js-chat-application avec le squelette ReactJS de base. Nous n'aborderons pas la structure du projet de base pour le moment.
2. Installer les dépendances
L'étape suivante consiste à installer les dépendances nécessaires pour notre client frontal. A votre borne :
- Accédez au répertoire du projet :
cd simple-react-js-chat-application
- Cours:
npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid
Cela installera les dépendances des prérequis :
- axios - Nous l'utilisons pour passer des appels au back-end afin de récupérer des canaux et des messages.
- emoji-mart - C'est un composant React pour les emojis.
- squelette-css - Un passe-partout CSS réactif simple.
- socket.io-client – package NPM pour la connexion au socket.
- uuid - bibliothèque d'identifiants d'utilisateur uniques
- node-sass – Nous utiliserons SCSS.
3. Créez le serveur principal
Pour utiliser Socket.IO, nous devons créer un serveur qui gérera les événements et certains points de terminaison de l'API, c'est-à-dire récupérer les canaux et les messages. Dans ce cas, nous utiliserons un serveur aussi simple que possible géré dans NodeJS.
Commencez par créer un nouveau serveur d'annuaire dans le dossier src . Commencez ensuite à créer les fichiers suivants :
Un fichier Package.json
Le fichier package.json spécifie la gestion npm, les dépendances et les dépendances dev. C'est un vrai fichier JSON, pas un objet JavaScript.
Les principaux champs dont Node.JS lui-même a besoin sont le nom et la version. Le nom contient le nom et la version du projet - la version du package.
{ "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 fichier Server.js
Ce fichier suit la logique selon laquelle le serveur principal gère l'instanciation du serveur, les routes personnalisées et les événements/écouteurs d'émission.
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 fichier Users.js
Ce composant est responsable des "utilisateurs" de l'application que nous construisons. Dans ce cas, nous n'avons qu'à ajouter/supprimer un utilisateur.
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 fichier Messages.js
Ce fichier ajoute la fonctionnalité pour les messages - c'est-à-dire en les ajoutant au tableau et en obtenant des messages de canal spécifiques.
const messages = []; const addMessage = (data) => { messages.push(data); return data; }; const getChannelMessages = (channel) => messages.filter((message) => message.channel === channel); module.exports = { addMessage, getChannelMessages };
Un fichier Channels.js
Ce fichier contient la logique des canaux - initialisant ceux par défaut et une fonctionnalité pour ajouter des utilisateurs à 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 };
Exécutez le terminal et dans le répertoire installez les dépendances NPM :
npm run install
Attendez que NPM se termine et que le serveur soit prêt. Vous pouvez l'essayer en courant
npm start
4. Créer la partie frontale
La dernière chose à faire dans l'ordre, mais pas dans l'importance, est de créer la partie frontale de l'application. Le serveur frontal communiquera avec le serveur principal pour fournir les fonctionnalités de base, c'est-à-dire saisir des surnoms, basculer entre les canaux et envoyer des messages.
Commençons par l'écran initial de l'application.
Accédez au dossier src et ouvrez App.js . Remplacez ensuite son contenu par celui ci-dessous :
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;
Le composant App contient la logique de l'état du surnom et indique si un utilisateur est « connecté ». Il rend également le composant approprié, que ce soit le LoginDialog ou le Chat - qui dépend de l'état.
Ajouter une forme
Ouvrez App.css et remplacez le contenu par :
.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; }
Créez de nouveaux assistants de dossier et placez un fichier avec le nom socket.js à l'intérieur.
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; };
Ce composant exporte les fonctions d'assistance nécessaires que nous utiliserons plus tard dans les composants React pour communiquer avec le serveur principal.
Nous sommes presque prêts !
Nous allons maintenant continuer avec la création de la boîte de dialogue pour fournir un surnom et la mise en page du chat.
Configurer la connexion
Créez de nouveaux composants de dossier et continuons avec la création des composants nécessaires à l'application.
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;
Il s'agit du composant de l'écran de connexion qui s'ouvre lors du chargement initial de l'application. À ce stade, aucun surnom n'est fourni. Il ne contient que des annotations et rien en rapport avec l'état ici. Comme il ressort du code, l'état et les gestionnaires lui sont transmis via des accessoires.
Donnez vie au chat
Continuez en créant un autre dossier - Chat , dans lequel nous allons créer plusieurs composants :
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;
C'est le composant principal qui maintient la logique de l'application de chat, lorsqu'un utilisateur est « connecté ». Il contient les gestionnaires et les états des canaux, des messages et de l'envoi de messages.
Donnez du style
Continuons avec les styles du composant et de ses composants enfants :
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;
Le composant principal de l'écran de chat contient les composants ChatMessages et 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;
Contient la logique et le rendu des champs de saisie du message - le sélecteur d'emoji et la zone de saisie du message.
Créer la barre latérale
Le dernier composant de la structure de l'application contient la "barre latérale" - c'est-à-dire la liste des chaînes.
Canaux.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'application est prête, la seule chose à considérer ici est de vérifier l'URL de l'API dans helpers/socket.js :
const SOCKET_URL = "http://localhost:8080";
Assurez-vous de le modifier en fonction de l'URL et du PORT du serveur principal que vous utilisez.
Exécutez à la fois la partie frontale et le serveur :
Accédez au répertoire racine et exécutez :
npm start
Accédez à src/server et exécutez :
npm start
Vous pouvez maintenant ouvrir http://localhost:3000 ou tout autre port que vous utilisez pour la partie frontale et y accéder.
Gardez à l'esprit que cette application a des fonctionnalités très basiques. Il n'y a pas d'authentification de connexion - vous n'avez besoin que d'un surnom. Et vous ne pourrez pas créer vos propres chaînes - vous ne pouvez basculer qu'entre les chaînes statiques fournies.
Bien joué!
Vous avez suivi notre guide et vous avez maintenant votre propre application de chat simplifiée de type Slack.
N'hésitez pas à expérimenter et à étendre l'application. Vous pouvez ajouter des éléments tels que :
- Une fonction "l'utilisateur tape".
- Une fonctionnalité pour rejoindre/créer des canaux.
- Authentification.
- Avatars d'utilisateurs.
- Une capacité à montrer quels utilisateurs sont actifs en ligne.
Dites-nous ce que vous en pensez. Et si vous avez besoin d'aide supplémentaire, n'hésitez pas à nous contacter.