Socket.IO Kullanarak Basit React Chat Uygulaması Nasıl Oluşturulur

Yayınlanan: 2022-02-10

Hey, Slack gibi uygulamaların nasıl çalıştığını hiç merak ettiniz mi? Veya böyle bir uygulama oluşturmanın ne kadar zor olacağı hakkında?

Bu yazıda, ReactJS ve SocketIO kullanarak Slack benzeri basit tepki veren bir sohbet uygulamasının nasıl oluşturulacağına dair adım adım bir kılavuz göstereceğiz. Slack'in sunduğu tüm özelliklerin oldukça basitleştirilmiş bir sürümünü oluşturacağız, bu nedenle bu öğreticiyi iyi bir başlangıç ​​örneği olarak kabul edin.

Özgün geliştirme çalışmasına başlamadan önce, hazırlamanız gereken bazı temel şeyler var.

3 Temel Ön Koşul:

  1. Temel JavaScript bilgisine sahip olmanız gerekir.
  2. Cihazınızda NodeJS ve NPM kurulu olmalıdır.
  3. Bir IDE veya tercih edilen herhangi bir metin düzenleyiciye sahip olun.

Bunları ayarladıktan sonra, çok basit 3 özelliği olan bir uygulamaya ulaşmak için gereken adımları uygulayacağız:

  1. Bir takma ad sağlayarak giriş yapın.
  2. Statik olarak sağlanan kanallar arasında geçiş yapın.
  3. Kanallara mesaj gönderin (emojiler dahil).

İşimiz bittiğinde, şuna benzeyen bir uygulamanız olmalıdır:

Sohbet Uygulaması Girişi ve Ana

Her şey hazır mı? Evet!? Gelelim o zaman, olur mu...

1. ReactJS Uygulamasını Başlatın

Öncelikle ReactJS uygulamasını oluşturup başlatmamız gerekiyor. Bunun için create-react-app.

Terminalinizi açın ve çalıştırın:
npx create-react-app simple-react-js-chat-application

Bu, temel ReactJS iskeletiyle yeni bir simple-react-js-chat-application dizini oluşturacaktır. Şu anda temel projenin yapısından geçmeyeceğiz.

2. Bağımlılıkları Yükleyin

Sonraki adım, ön uç istemcimiz için gerekli bağımlılıkları kurmaktır. Terminalinizde:

  • Proje dizinine gidin:
    cd simple-react-js-chat-application
  • Koşmak:
    npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid

Bu, önkoşul bağımlılıklarını yükleyecektir:

  • axios - Kanalları ve mesajları almak için arka uca çağrı yapmak için kullanıyoruz.
  • emoji-mart – Emojiler için React bileşenidir.
  • iskelet-css – Basit, duyarlı bir CSS kalıp plakası.
  • socket.io-client - Sokete bağlanmak için NPM paketi.
  • uuid – benzersiz kullanıcı kimliği kitaplığı
  • node-sass – SCSS kullanacağız.

3. Arka Uç Sunucusunu Oluşturun

Socket.IO'yu kullanmak için olayları ve bazı API uç noktalarını işleyecek, yani kanalları ve mesajları alacak bir sunucu oluşturmamız gerekiyor. Bu durumda, NodeJS'de işlenen mümkün olduğunca basit bir sunucu kullanacağız.

src klasöründe yeni bir dizin sunucusu oluşturarak başlayın. Ardından aşağıdaki dosyaları oluşturmaya başlayın:

Bir Package.json dosyası

package.json dosyası, npm işlemeyi, bağımlılıkları ve geliştirme bağımlılıklarını belirtir. Bu bir JavaScript nesnesi değil, gerçek bir JSON dosyasıdır.

Node.JS'nin ihtiyaç duyduğu ana alanlar ad ve sürümdür. Ad, projenin adını ve sürümünü, yani paket sürümünü içerir.

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

Bir Server.js Dosyası

Bu dosya, arka uç sunucunun sunucu somutlaştırmasını, özel rotaları ve olayları/yayılan dinleyicileri yönettiği mantığı izler.

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

Bir Users.js Dosyası

Bu bileşen, oluşturmakta olduğumuz uygulamanın “kullanıcılarından” sorumludur. Bu durumda sadece bir kullanıcı eklememiz/silmemiz gerekir.

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

Bir Messages.js Dosyası

Bu dosya, mesajlar için işlevsellik ekler – yani onları diziye eklemek ve belirli kanal mesajlarını almak.

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

Bir Channels.js Dosyası

Bu dosya, kanalların mantığını tutar – varsayılanları başlatır ve bir kanala kullanıcı eklemek için bir işlevsellik

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

Terminali çalıştırın ve dizine NPM bağımlılıklarını kurun:

 npm run install

NPM'nin bitmesini bekleyin ve sunucu hazır. Çalıştırarak deneyebilirsiniz

 npm start

Sohbet Uygulaması Sunucusu Başlatıldı

4. Ön Uç Parçasını Oluşturun

Sırayla ama önemli olmayan son şey, uygulamanın ön uç kısmını oluşturmaktır. Ön uç, temel özellikleri sağlamak için arka uç sunucusuyla iletişim kuracaktır – örneğin takma ad girme, kanallar arasında geçiş yapma ve mesaj gönderme.

Uygulamanın ilk ekranı ile başlayalım.
src klasörüne gidin ve App.js dosyasını açın. Ardından içeriğini aşağıdakiyle değiştirin:

 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;

Uygulama bileşeni, takma adın durumu ve bir kullanıcının "oturum açmış" olup olmadığına ilişkin mantığı içerir. Ayrıca duruma bağlı olarak LoginDialog veya Chat gibi uygun bileşeni de işler.

Biraz Şekil Ekle

App.css'yi açın ve içeriği şununla değiştirin:

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

Yeni klasör yardımcıları oluşturun ve içine socket.js adında bir dosya yerleştirin.

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

Bu bileşen, arka uç sunucusuyla iletişim kurmak için daha sonra React bileşenlerinde kullanacağımız gerekli yardımcı işlevleri dışa aktarır.

Neredeyse hazırız!
Şimdi takma ad sağlama ve sohbet düzeni için iletişim kutusunu oluşturmaya devam edeceğiz.

Girişi Ayarla

Yeni klasör bileşenleri oluşturun ve uygulama için gerekli bileşenleri oluşturmaya devam edelim.

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;

Bu, uygulama ilk yüklendiğinde açılan oturum açma ekranı bileşenidir. Bu noktada herhangi bir takma ad verilmez. Yalnızca işaretlemeler içerir ve buradaki durumla ilgili hiçbir şey yoktur. Koddan göründüğü gibi, durum ve işleyiciler ona aksesuarlardan geçirilir.

Sohbete Hayat Verin

Birkaç bileşen oluşturacağımız Chat adlı bir klasör daha oluşturmaya devam edin:

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;

Bu, bir kullanıcı "oturum açtığında" sohbet uygulamasının mantığını tutan ana bileşendir. Kanallar, mesajlar ve mesaj gönderme için işleyicileri ve durumları tutar.

Tarzınızı Oluşturun

Bileşen ve alt bileşenleri için stiller ile devam edelim:

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

Sohbet ekranının ana bileşeni, ChatMessages ve MessageForm bileşenlerini içerir.

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;

Mesaj için giriş alanlarının mantığını ve işlemesini içerir – mesaj için emoji seçici ve giriş kutusu.

Kenar Çubuğunu Oluşturun

Uygulama yapısının son bileşeni “kenar çubuğu”nu, yani kanal listesini içerir.

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;

Uygulama hazır, burada dikkate alınması gereken tek şey helpers/socket.js içindeki API URL'sini kontrol etmektir:

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

Kullandığınız arka uç sunucusunun URL'sine ve PORT'a göre değiştirdiğinizden emin olun.

Hem ön uç kısmı hem de sunucuyu çalıştırın:

Kök dizine gidin ve çalıştırın:
npm start


src/server'a gidin ve şunu çalıştırın:
npm start

Artık http://localhost:3000 veya ön uç kısım için kullandığınız başka bir bağlantı noktasını açıp ona erişebilirsiniz.

Bu uygulamanın çok temel özelliklere sahip olduğunu unutmayın. Oturum açma kimlik doğrulaması yoktur - yalnızca bir takma ada ihtiyacınız vardır. Ve kendi kanallarınızı oluşturamayacaksınız – yalnızca sağlanan statik kanallar arasında geçiş yapabilirsiniz.

Aferin!

Rehberimizi izlediniz ve artık kendi basitleştirilmiş Slack benzeri sohbet uygulamanız var.

Uygulamayı denemekten ve genişletmekten çekinmeyin. Şunlar gibi şeyler ekleyebilirsiniz:

  • Bir "kullanıcı yazıyor" özelliği.
  • Kanalları birleştirme/oluşturma özelliği.
  • Kimlik doğrulama.
  • Kullanıcı avatarları.
  • Hangi kullanıcıların çevrimiçi olarak aktif olduğunu gösterme yeteneği.

Nasıl beğendiğinizi bize bildirin. Ve daha fazla yardıma ihtiyacınız olursa, bizimle iletişime geçmekten çekinmeyin.