كيفية إنشاء تطبيق محادثة بسيط في React باستخدام Socket.IO
نشرت: 2022-02-10مرحبًا ، هل تساءلت يومًا عن كيفية عمل تطبيقات مثل Slack؟ أو ما مدى صعوبة إنشاء تطبيق كهذا؟
في هذه المقالة ، سنعرض لك دليلًا تفصيليًا حول كيفية إنشاء تطبيق دردشة بسيط يشبه Slack باستخدام ReactJS و SocketIO. سننشئ إصدارًا مبسطًا إلى حد ما لجميع الميزات التي يجب أن يقدمها Slack ، لذا خذ هذا البرنامج التعليمي كمثال جيد للمبتدئين.
قبل أن ندخل في أعمال التطوير الجوهرية ، هناك بعض الأساسيات التي تحتاجها لتكون جاهزًا.
المتطلبات الأساسية الثلاثة:
- ستحتاج إلى معرفة أساسية بجافا سكريبت.
- يجب أن يكون لديك NodeJS و NPM مثبتين على جهازك.
- لديك IDE ، أو أي محرر نصوص مفضل.
بمجرد الانتهاء من هذه المجموعة ، سنتابع الخطوات لتحقيق تطبيق يحتوي على 3 ميزات بسيطة للغاية:
- تسجيل الدخول من خلال تقديم اسم مستعار.
- التبديل بين القنوات المتوفرة بشكل ثابت.
- إرسال رسائل إلى القنوات (بما في ذلك الرموز التعبيرية).
عندما ننتهي ، يجب أن يكون لديك تطبيق يشبه هذا:
هل لديك كل شيء جاهز؟ نعم!؟ دعنا نصل إليها إذن ، هل علينا ...
1. تهيئة تطبيق ReactJS
أولاً ، نحتاج إلى إنشاء تطبيق ReactJS وتهيئته. لذلك سنستخدم create-react-app.
افتح المحطة الطرفية وقم بتشغيل:
npx create-react-app simple-react-js-chat-application
سيؤدي هذا إلى إنشاء دليل جديد simple-response-js-chat-application بهيكل ReactJS الأساسي. لن نمر في هيكل المشروع الأساسي في الوقت الحالي.
2. تثبيت التبعيات
الخطوة التالية هي تثبيت التبعيات المطلوبة لعميل الواجهة الأمامية. في محطتك:
- انتقل إلى دليل المشروع:
cd simple-react-js-chat-application
- يركض:
npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid
سيؤدي هذا إلى تثبيت تبعيات المتطلبات الأساسية:
- أكسيوس - نحن نستخدمه لإجراء مكالمات إلى النهاية الخلفية لجلب القنوات والرسائل.
- emoji-mart - إنه مكون React للرموز التعبيرية.
- skeleton-css - نموذج CSS سريع الاستجابة بسيط.
- socket.io-client - حزمة NPM للاتصال بالمقبس.
- uuid - مكتبة معرف المستخدم الفريدة
- node-sass - سنستخدم SCSS.
3. إنشاء الخادم الخلفي
لاستخدام Socket.IO نحتاج إلى إنشاء خادم يتعامل مع الأحداث وبعض نقاط نهاية API - أي استرداد القنوات والرسائل. في هذه الحالة ، سنستخدم خادمًا بسيطًا قدر الإمكان يتم التعامل معه في NodeJS.
ابدأ بإنشاء خادم دليل جديد في مجلد src . ثم ابدأ في إنشاء الملفات التالية:
ملف Package.json
يحدد ملف package.json معالجة npm والتبعيات والتبعيات. إنه ملف JSON حقيقي ، وليس كائن JavaScript.
الحقول الرئيسية التي يحتاجها Node.JS نفسها هي الاسم والإصدار. الاسم يحمل اسم المشروع وإصداره - إصدار الحزمة.
{ "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" } }
ملف Server.js
يتبع هذا الملف المنطق القائل بأن الخادم الخلفي يتعامل مع إنشاء مثيل للخادم والمسارات المخصصة والأحداث / المنصات المنبعثة.
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}`));
ملف Users.js
هذا المكون مسؤول عن "مستخدمي" التطبيق الذي نقوم ببنائه. في هذه الحالة ، علينا فقط إضافة / حذف مستخدم.
const users = {}; const addUser = (nickname, socketId) => { users[nickname] = socketId; } const removeUser = (nickname) => { if(users.hasOwnProperty(nickname)) { delete users[nickname]; } } module.exports = { users, addUser, removeUser };
ملف Messages.js
يضيف هذا الملف وظيفة الرسائل - أي إضافتها إلى المصفوفة والحصول على رسائل قناة محددة.
const messages = []; const addMessage = (data) => { messages.push(data); return data; }; const getChannelMessages = (channel) => messages.filter((message) => message.channel === channel); module.exports = { addMessage, getChannelMessages };
ملف Channels.js
يحتوي هذا الملف على منطق القنوات - تهيئة القنوات الافتراضية ووظيفة إضافة مستخدمين إلى القناة
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 };
قم بتشغيل المحطة وفي الدليل قم بتثبيت تبعيات NPM:
npm run install
انتظر حتى انتهاء NPM والخادم جاهز. يمكنك تجربتها عن طريق الجري
npm start
4. قم بإنشاء جزء الواجهة الأمامية
آخر شيء يجب تحقيقه ، ولكن ليس من الأهمية بمكان ، هو إنشاء الجزء الأمامي من التطبيق. ستتواصل الواجهة الأمامية مع الخادم الخلفي لتوفير الميزات الأساسية - أي إدخال الأسماء المستعارة والتبديل بين القنوات وإرسال الرسائل.
لنبدأ بالشاشة الأولية للتطبيق.
انتقل إلى مجلد src وافتح App.js. ثم استبدل محتواه بالمحتوى أدناه:
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;
يحتوي مكون التطبيق على منطق حالة اللقب وما إذا كان المستخدم قد "قام بتسجيل الدخول". كما أنه يعرض المكون المناسب سواء كان حوار تسجيل الدخول أو الدردشة - والذي يعتمد على الحالة.
أضف بعض الشكل
افتح App.css واستبدل المحتوى بـ:
.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; }
قم بإنشاء مساعدين مجلدات جديدة وضع ملفًا بداخله اسم socket.js .
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; };
يقوم هذا المكون بتصدير الوظائف المساعدة الضرورية التي سنستخدمها لاحقًا في مكونات React للتواصل مع الخادم الخلفي.
نحن جاهزون تقريبًا!
سنستمر الآن في إنشاء مربع حوار لتوفير الاسم المستعار وتخطيط الدردشة.
قم بإعداد تسجيل الدخول
قم بإنشاء مكونات مجلد جديدة ودعنا نواصل إنشاء المكونات الضرورية للتطبيق.
تسجيل الدخول
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;
هذا هو مكون شاشة تسجيل الدخول الذي يفتح عند تحميل التطبيق في البداية. في هذه المرحلة لم يتم تقديم اسم مستعار. يحتوي فقط على هوامش الربح ولا شيء متعلق بالحالة هنا. كما يبدو من الكود ، يتم تمرير الحالة والمعالجات عبر الدعائم إليها.
اجعل الدردشة تنبض بالحياة
استمر في إنشاء مجلد آخر - الدردشة ، حيث سننشئ عدة مكونات:
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;
هذا هو المكون الرئيسي الذي يدعم منطق تطبيق الدردشة ، عندما يتم "تسجيل دخول" المستخدم. إنه يحمل المعالجات والحالات للقنوات والرسائل وإرسال الرسائل.
اسلوبها
دعنا نواصل استخدام الأسلوب الخاص بالمكون ومكوناته الفرعية:
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;
يحتوي المكون الرئيسي لشاشة الدردشة على مكونات ChatMessages و 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;
يحتوي على منطق وعرض حقول الإدخال للرسالة - منتقي الرموز التعبيرية ومربع الإدخال للرسالة.
قم بإنشاء الشريط الجانبي
يحتوي المكون الأخير من بنية التطبيق على "الشريط الجانبي" - أي قائمة القنوات.
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;
التطبيق جاهز ، الشيء الوحيد الذي يجب مراعاته هنا هو التحقق من عنوان URL الخاص بواجهة برمجة التطبيقات في helpers / socket.js :
const SOCKET_URL = "http://localhost:8080";
تأكد من تغييره وفقًا لعنوان URL و PORT لخادم النهاية الذي تستخدمه.
قم بتشغيل كل من الجزء الأمامي والخادم:
انتقل إلى الدليل الجذر وقم بتشغيل:
npm start
انتقل إلى src / server وقم بتشغيل:
npm start
يمكنك الآن فتح http: // localhost: 3000 أو أي منفذ آخر تستخدمه للجزء الأمامي والوصول إليه.
ضع في اعتبارك أن هذا التطبيق يحتوي على ميزات أساسية للغاية. لا توجد مصادقة لتسجيل الدخول - ما عليك سوى اسم مستعار. ولن تتمكن من إنشاء قنواتك الخاصة - يمكنك فقط التبديل بين القنوات الثابتة المتوفرة.
أتقنه!
لقد اتبعت دليلنا والآن لديك تطبيق دردشة مبسط يشبه Slack.
لا تتردد في تجربة التطبيق وتوسيعه. يمكنك إضافة أشياء مثل:
- ميزة "المستخدم يكتب".
- ميزة للانضمام / إنشاء القنوات.
- المصادقة.
- الصور الرمزية للمستخدم.
- القدرة على إظهار المستخدمين النشطين عبر الإنترنت.
واسمحوا لنا أن نعرف كيف كنت ترغب في ذلك. وإذا كنت بحاجة إلى مزيد من المساعدة ، فلا تتردد في الاتصال بنا.