Cara Membuat Aplikasi React Chat Sederhana Menggunakan Socket.IO

Diterbitkan: 2022-02-10

Hei, pernahkah Anda bertanya-tanya bagaimana aplikasi seperti Slack bekerja? Atau tentang betapa sulitnya membuat aplikasi seperti itu?

Pada artikel ini, kami akan menunjukkan kepada Anda panduan langkah demi langkah tentang cara membuat aplikasi obrolan reaksi sederhana seperti Slack menggunakan ReactJS dan SocketIO. Kami akan membuat versi yang agak disederhanakan dari semua fitur yang ditawarkan Slack, jadi ambillah tutorial ini sebagai contoh awal yang baik.

Sebelum kita masuk ke pekerjaan pengembangan seluk beluk, ada beberapa hal penting yang harus Anda siapkan.

3 Prasyarat Utama:

  1. Anda harus memiliki pengetahuan JavaScript Dasar.
  2. Anda harus menginstal NodeJS dan NPM di perangkat Anda.
  3. Miliki IDE, atau editor teks apa pun yang lebih disukai.

Setelah Anda mengaturnya, kami akan melalui langkah-langkah untuk mencapai aplikasi yang memiliki 3 fitur yang sangat sederhana:

  1. Login dengan memberikan nama panggilan.
  2. Beralih di antara saluran yang disediakan secara statis.
  3. Kirim pesan ke saluran (termasuk emoji).

Setelah selesai, Anda harus memiliki aplikasi yang terlihat seperti ini:

Masuk dan Utama Aplikasi Obrolan

Apakah Anda memiliki segalanya siap? Ya!? Mari kita ke sana, akankah kita…

1. Inisialisasi Aplikasi ReactJS

Pertama, kita perlu membuat dan menginisialisasi aplikasi ReactJS. Untuk itu kita akan menggunakan create-react-app.

Buka terminal Anda dan jalankan:
npx create-react-app simple-react-js-chat-application

Ini akan membuat direktori baru aplikasi simple-react-js-chat dengan kerangka dasar ReactJS. Kami tidak akan membahas struktur proyek dasar saat ini.

2. Instal Ketergantungan

Langkah selanjutnya adalah menginstal dependensi yang diperlukan untuk klien front-end kami. Di terminal Anda:

  • Pergi ke direktori proyek:
    cd simple-react-js-chat-application
  • Lari:
    npm install axios emoji-mart node-sass skeleton-css socket.io-client uuid

Ini akan menginstal dependensi prasyarat:

  • axios – Kami menggunakannya untuk melakukan panggilan ke back-end untuk mengambil saluran dan pesan.
  • emoji-mart – Ini adalah komponen Bereaksi untuk emoji.
  • skeleton-css – Sebuah boilerplate CSS responsif sederhana.
  • socket.io-client – ​​paket NPM untuk menghubungkan ke soket.
  • uuid – perpustakaan id pengguna yang unik
  • node-sass – Kami akan menggunakan SCSS.

3. Buat Server Back-End

Untuk menggunakan Socket.IO kita perlu membuat server yang akan menangani acara dan beberapa titik akhir API – yaitu mengambil saluran dan pesan. Dalam hal ini, kami akan menggunakan server sesederhana mungkin yang ditangani di NodeJS.

Mulailah dengan membuat server direktori baru di folder src . Kemudian mulailah membuat file-file berikut:

File Package.json

File package.json menentukan penanganan npm, dependensi, dan dependensi dev. Ini adalah file JSON yang sebenarnya, bukan objek JavaScript.

Bidang utama yang dibutuhkan Node.JS sendiri adalah nama dan versi. Nama tersebut memuat nama dan versi proyek – versi paket.

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

File Server.js

File ini mengikuti logika bahwa server back-end menangani instantiasi server, rute khusus, dan pendengar peristiwa/pancaran.

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

File Users.js

Komponen ini bertanggung jawab atas “pengguna” aplikasi yang sedang kita bangun. Dalam hal ini, kita hanya perlu menambah/menghapus pengguna.

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

File Messages.js

File ini menambahkan fungsionalitas untuk pesan – yaitu menambahkannya ke larik dan mendapatkan pesan saluran tertentu.

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

File Channels.js

File ini menyimpan logika untuk saluran – menginisialisasi saluran default dan fungsionalitas untuk menambahkan pengguna ke saluran

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

Jalankan terminal dan di direktori instal dependensi NPM:

 npm run install

Tunggu NPM selesai dan server siap. Anda dapat mencobanya dengan menjalankan

 npm start

Server Aplikasi Obrolan Dimulai

4. Buat Bagian Front-End

Hal terakhir yang harus dilakukan, tetapi tidak terlalu penting, untuk dicapai adalah membuat bagian front-end dari aplikasi. Front-end akan berkomunikasi dengan server back-end untuk menyediakan fitur inti – yaitu memasukkan nama panggilan, beralih antar saluran dan mengirim pesan.

Mari kita mulai dengan layar awal aplikasi.
Arahkan ke folder src dan buka App.js . Kemudian ganti isinya dengan yang di bawah ini:

 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;

Komponen Aplikasi berisi logika untuk status nama panggilan dan apakah pengguna "masuk". Itu juga membuat komponen yang sesuai baik itu LoginDialog atau Obrolan - yang tergantung pada negara bagian.

Tambahkan Beberapa Bentuk

Buka App.css dan ganti konten dengan:

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

Buat pembantu folder baru dan tempatkan file dengan nama socket.js di dalamnya.

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

Komponen ini mengekspor fungsi pembantu yang diperlukan yang akan kita gunakan nanti di komponen React untuk berkomunikasi dengan server back-end.

Kami hampir siap!
Sekarang kita akan melanjutkan dengan membuat dialog untuk pemberian nickname dan layout chat.

Mengatur Login

Buat komponen folder baru dan mari lanjutkan dengan membuat komponen yang diperlukan untuk aplikasi.

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;

Ini adalah komponen layar masuk yang terbuka saat aplikasi pertama kali dimuat. Pada titik ini tidak ada nama panggilan yang diberikan. Ini hanya berisi markup dan tidak ada yang terkait dengan status di sini. Seperti yang terlihat dari kode, status dan penangan diteruskan melalui alat peraga ke sana.

Hidupkan Obrolan

Lanjutkan dengan membuat satu folder lagi – Obrolan , di mana kita akan membuat beberapa komponen:

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;

Ini adalah komponen utama yang memegang logika untuk aplikasi obrolan, ketika pengguna "masuk". Ini memegang penangan dan status untuk saluran, pesan, dan pengiriman pesan.

Gaya!

Mari kita lanjutkan dengan penataan untuk komponen dan komponen turunannya:

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

Pesan Obrolan.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;

Komponen utama untuk layar obrolan berisi komponen ChatMessages dan 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;

Berisi logika dan rendering bidang input untuk pesan – pemilih emoji dan kotak input untuk pesan.

Buat Bilah Sisi

Komponen terakhir dari struktur aplikasi berisi “bilah sisi” – yaitu daftar saluran.

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;

Aplikasi sudah siap, satu-satunya hal yang perlu dipertimbangkan di sini adalah memeriksa URL API di helpers/socket.js :

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

Pastikan untuk mengubahnya sesuai dengan URL dan PORT untuk server back-end yang Anda gunakan.

Jalankan bagian front-end dan server:

Arahkan ke direktori root dan jalankan:
npm start


Arahkan ke src/server dan jalankan:
npm start

Sekarang Anda dapat membuka http://localhost:3000 atau port lain yang Anda gunakan untuk bagian front-end dan mengaksesnya.

Perlu diingat bahwa aplikasi ini memiliki fitur yang sangat mendasar. Tidak ada otentikasi log-in – Anda hanya perlu nama panggilan. Dan Anda tidak akan dapat membuat saluran sendiri – Anda hanya dapat beralih di antara saluran statis yang disediakan.

Sudah selesai dilakukan dengan baik!

Anda mengikuti panduan kami dan sekarang Anda memiliki aplikasi obrolan seperti Slack yang disederhanakan.

Jangan ragu untuk bereksperimen dan memperluas aplikasi. Anda dapat menambahkan hal-hal seperti:

  • Fitur "pengguna sedang mengetik".
  • Fitur untuk bergabung/membuat saluran.
  • Autentikasi.
  • Avatar pengguna.
  • Kemampuan untuk menunjukkan pengguna mana yang aktif secara online.

Beri tahu kami bagaimana Anda menyukainya. Dan jika Anda membutuhkan bantuan lebih lanjut, jangan ragu untuk menghubungi kami.