Mengembangkan Obrolan React Menggunakan Socket.IO





Selamat siang teman!



Saya ingin berbagi dengan Anda pengalaman saya dalam mengembangkan obrolan sederhana di React menggunakan pustaka Socket.IO .



Diasumsikan bahwa Anda sudah familiar dengan library bernama. Jika Anda tidak terbiasa, berikut adalah panduan terkait dengan contoh membuat tudushka dan chat di vanilla JavaScript .



Ini juga mengasumsikan bahwa Anda setidaknya dangkal akrab dengan Node.js .



Pada artikel ini, saya akan fokus pada kepraktisan penggunaan Socket.IO, React, dan Node.js.



Obrolan kita akan memiliki fitur-fitur utama berikut:



  • Pemilihan kamar
  • Mengirim pesan
  • Hapus pesan oleh pengirim
  • Menyimpan pesan dalam database lokal dalam format JSON
  • Menyimpan nama pengguna dan ID di penyimpanan lokal browser
  • Menampilkan jumlah pengguna aktif
  • Menampilkan daftar pengguna dengan indikator online


Kami juga akan menerapkan kemampuan mengirim emoji .



Jika Anda tertarik, silakan ikuti saya.



Bagi mereka yang hanya tertarik dengan kode: berikut ini tautan ke repositori .



Bak pasir:





Struktur dan ketergantungan proyek



Mari mulai membuat proyek:



mkdir react-chat
cd react-chat

      
      





Buat klien menggunakan Buat Aplikasi React :



yarn create react-app client
# 
npm init react-app client
# 
npx create-react-app client

      
      





Di masa depan, saya akan menggunakan benang : untuk memasang dependensi yarn add = npm i, yarn start = npm start, yarn dev = npm run dev



.



Buka direktori "klien" dan instal dependensi tambahan:



cd client
yarn add socket.io-client react-router-dom styled-components bootstrap react-bootstrap react-icons emoji-mart react-timeago

      
      







Bagian "ketergantungan" dari file "package.json":



{
  "bootstrap": "^4.6.0",
  "emoji-mart": "^3.0.0",
  "react": "^17.0.1",
  "react-bootstrap": "^1.5.0",
  "react-dom": "^17.0.1",
  "react-icons": "^4.2.0",
  "react-router-dom": "^5.2.0",
  "react-scripts": "4.0.1",
  "react-timeago": "^5.2.0",
  "socket.io-client": "^3.1.0",
  "styled-components": "^5.2.1"
}

      
      





Kembali ke direktori root (react-chat), buat direktori "server", buka, inisialisasi proyek dan instal dependensi:



cd ..
mkdir server
cd server
yarn init -yp
yarn add socket.io lowdb supervisor

      
      





  • socket.io - Bagian belakang Socket.IO
  • lowdb - database lokal dalam format JSON
  • supervisor - server pengembangan (alternatif untuk nodemon , yang tidak berfungsi dengan benar dengan versi stabil terbaru Node.js; ini ada hubungannya dengan memulai / menghentikan proses anak yang salah)


Tambahkan perintah "start" untuk memulai server produksi dan perintah "dev" untuk memulai server pengembangan. package.json:



{
  "name": "server",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "lowdb": "^1.0.0",
    "socket.io": "^3.1.0",
    "supervisor": "^0.12.0"
  },
  "scripts": {
    "start": "node index.js",
    "dev": "supervisor index.js"
  }
}

      
      





Kembali ke direktori root (react-chat), inisialisasi proyek dan instal dependensi:



  cd ..
  yarn init -yp
  yarn add nanoid concurrently

      
      





  • nanoid - pengidentifikasi penghasil (akan digunakan baik di klien maupun di server)
  • secara bersamaan - eksekusi dua atau lebih perintah secara bersamaan


react-chat / package.json (perhatikan perintah untuk npm terlihat berbeda; lihat dokumen secara bersamaan):



{
  "name": "react-chat",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "concurrently": "^6.0.0",
    "nanoid": "^3.1.20"
  },
  "scripts": {
    "server": "yarn --cwd server dev",
    "client": "yarn --cwd client start",
    "start": "concurrently \"yarn server\" \"yarn client\""
  }
}

      
      





Hebat, kita selesai dengan pembentukan struktur utama proyek dan pemasangan dependensi yang diperlukan. Mari mulai menerapkan server.



Implementasi server



Struktur direktori "server":



|--server
  |--db -    
  |--handlers
    |--messageHandlers.js
    |--userHandlers.js
  |--index.js
  ...

      
      





Di file "index.js", kami melakukan hal berikut:



  • Membangun server HTTP
  • Kami menghubungkan Socket.IO ke sana
  • Kami memulai server pada port 5000
  • Mendaftarkan Penangan Peristiwa Saat Menghubungkan Soket


index.js:



//  HTTP-
const server = require('http').createServer()
//    Socket.IO
const io = require('socket.io')(server, {
  cors: {
    origin: '*'
  }
})

const log = console.log

//   
const registerMessageHandlers = require('./handlers/messageHandlers')
const registerUserHandlers = require('./handlers/userHandlers')

//        (,   =  )
const onConnection = (socket) => {
  //     
  log('User connected')

  //       ""
  const { roomId } = socket.handshake.query
  //       
  socket.roomId = roomId

  //    (  )
  socket.join(roomId)

  //  
  //     
  registerMessageHandlers(io, socket)
  registerUserHandlers(io, socket)

  //   -
  socket.on('disconnect', () => {
    //  
    log('User disconnected')
    //  
    socket.leave(roomId)
  })
}

//  
io.on('connection', onConnection)

//  
const PORT = process.env.PORT || 5000
server.listen(PORT, () => {
  console.log(`Server ready. Port: ${PORT}`)
})

      
      





Di file "handlers / messageHandlers.js" kita melakukan hal berikut:



  • Menyiapkan database lokal dalam format JSON menggunakan lowdb
  • Kami menulis data awal ke database
  • Membuat fungsi untuk menerima, menambah dan menghapus pesan
  • Kami mendaftarkan pemrosesan acara terkait:

    • pesan: terima - terima pesan
    • pesan: tambah - tambahkan pesan
    • pesan: hapus - hapus pesan




Pesan adalah objek dengan properti berikut:



  • messageId (string) - pengenal pesan
  • userId (string) - ID pengguna
  • senderName (string) - nama pengirim
  • messageText (string) - teks pesan
  • createAt (date) - tanggal pembuatan


handlers / messageHandlers.js:



const { nanoid } = require('nanoid')
//  
const low = require('lowdb')
const FileSync = require('lowdb/adapters/FileSync')
//     "db"   "messages.json"
const adapter = new FileSync('db/messages.json')
const db = low(adapter)

//     
db.defaults({
  messages: [
    {
      messageId: '1',
      userId: '1',
      senderName: 'Bob',
      messageText: 'What are you doing here?',
      createdAt: '2021-01-14'
    },
    {
      messageId: '2',
      userId: '2',
      senderName: 'Alice',
      messageText: 'Go back to work!',
      createdAt: '2021-02-15'
    }
  ]
}).write()

module.exports = (io, socket) => {
  //     
  const getMessages = () => {
    //    
    const messages = db.get('messages').value()
    //   ,   
    //  - , , 
    io.in(socket.roomId).emit('messages', messages)
  }

  //   
  //    
  const addMessage = (message) => {
    db.get('messages')
      .push({
        //     nanoid, 8 -  id
        messageId: nanoid(8),
        createdAt: new Date(),
        ...message
      })
      .write()

    //     
    getMessages()
  }

  //   
  //   id 
  const removeMessage = (messageId) => {
    db.get('messages').remove({ messageId }).write()

    getMessages()
  }

  //  
  socket.on('message:get', getMessages)
  socket.on('message:add', addMessage)
  socket.on('message:remove', removeMessage)
}

      
      





Di file "handlers / userHandlers.js" kami melakukan hal berikut:



  • Buat struktur yang dinormalisasi dengan pengguna
  • Kami membuat fungsi untuk mendapatkan, menambah, dan menghapus pengguna
  • Kami mendaftarkan pemrosesan acara terkait:

    • pengguna: dapatkan - dapatkan pengguna
    • pengguna: tambah - tambahkan pengguna
    • pengguna: tinggalkan - hapus pengguna




Kami juga dapat menggunakan lowdb untuk bekerja dengan daftar pengguna. Anda dapat melakukan ini jika Anda suka. Saya, dengan izin Anda, akan membatasi diri pada objek tersebut.



Struktur (objek) pengguna yang dinormalisasi memiliki format berikut:



{
  id (string) - : {
    username (string) -  ,
    online (boolean) -     
  }
}

      
      





Faktanya, kami tidak menghapus pengguna, tetapi mentransfer status mereka ke offline (menetapkan properti "online" ke "false").



handlers / userHandlers.js:



//  
//  
const users = {
  1: { username: 'Alice', online: false },
  2: { username: 'Bob', online: false }
}

module.exports = (io, socket) => {
  //     
  //  "roomId"  ,
  //       ,
  //      
  const getUsers = () => {
    io.in(socket.roomId).emit('users', users)
  }

  //   
  //         id
  const addUser = ({ username, userId }) => {
    // ,     
    if (!users[userId]) {
      //   ,    
      users[userId] = { username, online: true }
    } else {
      //  ,     
      users[userId].online = true
    }
    //     
    getUsers()
  }

  //   
  const removeUser = (userId) => {
    //        ,
    //     (O(1))    
    //      () 
    //  redux, ,  immer,     
    users[userId].online = false
    getUsers()
  }

  //  
  socket.on('user:get', getUsers)
  socket.on('user:add', addUser)
  socket.on('user:leave', removeUser)
}

      
      





Kami memulai server untuk memeriksa kinerjanya:



yarn dev

      
      





Jika kita melihat pesan “Server siap. Porta: 5000 ", dan file" messages.json "dengan data awal muncul di direktori" db ", yang berarti bahwa server bekerja seperti yang diharapkan, dan Anda dapat melanjutkan ke implementasi bagian klien.



Implementasi klien



Dengan klien, semuanya menjadi lebih rumit. Struktur direktori "klien":



|--client
  |--public
    |--index.html
  |--src
    |--components
      |--ChatRoom
        |--MessageForm
          |--MessageForm.js
          |--package.json
        |--MessageList
          |--MessageList.js
          |--MessageListItem.js
          |--package.json
        |--UserList
          |--UserList.js
          |--package.json
        |--ChatRoom.js
        |--package.json
      |--Home
        |--Home.js
        |--package.json
      |--index.js
    |--hooks
      |--useBeforeUnload.js
      |--useChat.js
      |--useLocalStorage.js
    App.js
    index.js
  |--jsconfig.json (  src)
  ...

      
      





Sesuai dengan namanya, direktori "components" berisi komponen aplikasi (bagian dari antarmuka pengguna, modul), dan direktori "hooks" berisi hook pengguna ("custom"), yang utamanya adalah useChat ().



File "package.json" di direktori komponen memiliki satu kolom "main" dengan nilai jalur ke file JS, misalnya:



{
  "main": "./Home"
}

      
      





Ini memungkinkan Anda untuk mengimpor komponen dari direktori tanpa menentukan nama file, misalnya:



import { Home } from './Home'
// 
import { Home } from './Home/Home'

      
      





File "components / index.js" dan "hooks / index.js" masing-masing digunakan untuk menggabungkan dan mengekspor ulang komponen dan hook.



komponen / index.js:



export { Home } from './Home'
export { ChatRoom } from './ChatRoom'

      
      





hooks / index.js:



export { useChat } from './useChat'
export { useLocalStorage } from './useLocalStorage'
export { useBeforeUnload } from './useBeforeUnload'

      
      





Ini sekali lagi memungkinkan Anda untuk mengimpor komponen dan kait berdasarkan direktori dan pada saat yang bersamaan. Agregasi dan re-ekspor menyebabkan penggunaan ekspor komponen bernama (dokumentasi React merekomendasikan penggunaan ekspor default).



File jsconfig.json terlihat seperti ini:



{
  "compilerOptions": {
    "baseUrl": "src"
  }
}

      
      





Ini "memberitahu" kompiler bahwa impor modul dimulai dari direktori "src", jadi komponen, misalnya, dapat diimpor seperti ini:



//      
import { Home, ChatRoom } from 'components'
// 
import { Home, ChatRoom } from './components'

      
      





Mari kita mulai dengan melihat pengait khusus.



Anda dapat menggunakan solusi yang sudah jadi. Misalnya, berikut adalah hook yang ditawarkan oleh library react-use :



# 
yarn add react-use
# 
import { useLocalStorage } from 'react-use'
import { useBeforeUnload } from 'react-use'

      
      





Hook useLocalStorage () memungkinkan Anda untuk menyimpan (menulis dan mengambil) nilai di penyimpanan lokal browser. Kami akan menggunakannya untuk menyimpan nama pengguna dan ID pengguna di antara sesi browser. Kami tidak ingin memaksa pengguna untuk memasukkan namanya setiap saat, tetapi ID diperlukan untuk menentukan pesan milik pengguna ini. Hook mengambil nama untuk kunci tersebut dan, secara opsional, nilai awal.



hooks / useLocalstorage.js:



import { useState, useEffect } from 'react'

export const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    const item = window.localStorage.getItem(key)
    return item ? JSON.parse(item) : initialValue
  })

  useEffect(() => {
    const item = JSON.stringify(value)
    window.localStorage.setItem(key, item)
    //  ,        key,   useEffect,   ,  
    //     useEffect
    // eslint-disable-next-line
  }, [value])

  return [value, setValue]
}

      
      





Hook "useBeforeUnload ()" digunakan untuk menampilkan pesan atau menjalankan fungsi saat halaman (tab browser) dimuat ulang atau ditutup. Kami akan menggunakannya untuk mengirim acara "pengguna: tinggalkan" ke server untuk mengubah status pengguna. Upaya untuk mengimplementasikan pengiriman peristiwa yang ditentukan menggunakan callback yang dikembalikan oleh hook "useEffect ()" tidak berhasil. Pengait mengambil satu parameter, primitif atau fungsi.



hooks / useBeforeUnload.js:



import { useEffect } from 'react'

export const useBeforeUnload = (value) => {
  const handleBeforeunload = (e) => {
    let returnValue
    if (typeof value === 'function') {
      returnValue = value(e)
    } else {
      returnValue = value
    }
    if (returnValue) {
      e.preventDefault()
      e.returnValue = returnValue
    }
    return returnValue
  }

  useEffect(() => {
    window.addEventListener('beforeunload', handleBeforeunload)
    return () => window.removeEventListener('beforeunload', handleBeforeunload)
    // eslint-disable-next-line
  }, [])
}

      
      





Hook useChat () adalah hook utama untuk aplikasi kita. Akan lebih mudah jika saya mengomentarinya baris demi baris.



hooks / useChat.js:



import { useEffect, useRef, useState } from 'react'
//   IO
import io from 'socket.io-client'
import { nanoid } from 'nanoid'
//  
import { useLocalStorage, useBeforeUnload } from 'hooks'

//  
//    -  
const SERVER_URL = 'http://localhost:5000'

//    
export const useChat = (roomId) => {
  //    
  const [users, setUsers] = useState([])
  //    
  const [messages, setMessages] = useState([])

  //        
  const [userId] = useLocalStorage('userId', nanoid(8))
  //      
  const [username] = useLocalStorage('username')

  // useRef()        DOM-,
  //             
  const socketRef = useRef(null)

  useEffect(() => {
    //   ,    
    //          ""
    // socket.handshake.query.roomId
    socketRef.current = io(SERVER_URL, {
      query: { roomId }
    })

    //    ,
    //         id 
    socketRef.current.emit('user:add', { username, userId })

    //    
    socketRef.current.on('users', (users) => {
      //   
      setUsers(users)
    })

    //     
    socketRef.current.emit('message:get')

    //   
    socketRef.current.on('messages', (messages) => {
      // ,      ,
      //    "userId"     id ,
      //       "currentUser"   "true",
      // ,    
      const newMessages = messages.map((msg) =>
        msg.userId === userId ? { ...msg, currentUser: true } : msg
      )
      //   
      setMessages(newMessages)
    })

    return () => {
      //      
      socketRef.current.disconnect()
    }
  }, [roomId, userId, username])

  //   
  //        
  const sendMessage = ({ messageText, senderName }) => {
    //    id     
    socketRef.current.emit('message:add', {
      userId,
      messageText,
      senderName
    })
  }

  //     id
  const removeMessage = (id) => {
    socketRef.current.emit('message:remove', id)
  }

  //     "user:leave"   
  useBeforeUnload(() => {
    socketRef.current.emit('user:leave', userId)
  })

  //   ,       
  return { users, messages, sendMessage, removeMessage }
}

      
      





Secara default, semua permintaan klien dikirim ke localhost: 3000 (port tempat server pengembangan berjalan). Untuk mengalihkan permintaan ke port tempat server "server" berjalan, proxy harus dilakukan. Untuk melakukannya, tambahkan baris berikut ke file "src / package.json":



"proxy": "http://localhost:5000"

      
      





Tetap menerapkan komponen aplikasi.



Komponen Home adalah hal pertama yang dilihat pengguna saat mereka meluncurkan aplikasi. Ini berisi formulir di mana pengguna diminta untuk memasukkan namanya dan memilih kamar. Pada kenyataannya, untuk sebuah ruangan, pengguna tidak memiliki pilihan, hanya satu pilihan (gratis) yang tersedia. Opsi (pekerjaan) kedua (nonaktif) adalah kemampuan untuk menskalakan aplikasi. Tampilan tombol untuk memulai chat bergantung pada field dengan nama pengguna (bila field ini kosong, tombol tidak ditampilkan). Tombol tersebut sebenarnya adalah tautan ke halaman obrolan.



komponen / Home.js:



import { useState, useRef } from 'react'
//    react-router-dom
import { Link } from 'react-router-dom'
//  
import { useLocalStorage } from 'hooks'
//    react-bootstrap
import { Form, Button } from 'react-bootstrap'

export function Home() {
  //        
  //     
  const [username, setUsername] = useLocalStorage('username', 'John')
  //    
  const [roomId, setRoomId] = useState('free')
  const linkRef = useRef(null)

  //    
  const handleChangeName = (e) => {
    setUsername(e.target.value)
  }

  //   
  const handleChangeRoom = (e) => {
    setRoomId(e.target.value)
  }

  //   
  const handleSubmit = (e) => {
    e.preventDefault()
    //   
    linkRef.current.click()
  }

  const trimmed = username.trim()

  return (
    <Form
      className='mt-5'
      style={{ maxWidth: '320px', margin: '0 auto' }}
      onSubmit={handleSubmit}
    >
      <Form.Group>
        <Form.Label>Name:</Form.Label>
        <Form.Control value={username} onChange={handleChangeName} />
      </Form.Group>
      <Form.Group>
        <Form.Label>Room:</Form.Label>
        <Form.Control as='select' value={roomId} onChange={handleChangeRoom}>
          <option value='free'>Free</option>
          <option value='job' disabled>
            Job
          </option>
        </Form.Control>
      </Form.Group>
      {trimmed && (
        <Button variant='success' as={Link} to={`/${roomId}`} ref={linkRef}>
          Chat
        </Button>
      )}
    </Form>
  )
}

      
      





Komponen UserList, seperti namanya, adalah daftar pengguna. Ini berisi akordeon, daftar itu sendiri, dan indikator kehadiran online pengguna.



komponen / UserList.js:



// 
import { Accordion, Card, Button, Badge } from 'react-bootstrap'
//  -   
import { RiRadioButtonLine } from 'react-icons/ri'

//      -  
export const UserList = ({ users }) => {
  //    
  const usersArr = Object.entries(users)
  //    ( )
  // [ ['1', { username: 'Alice', online: false }], ['2', {username: 'Bob', online: false}] ]

  //   
  const activeUsers = Object.values(users)
    //   
    // [ {username: 'Alice', online: false}, {username: 'Bob', online: false} ]
    .filter((u) => u.online).length

  return (
    <Accordion className='mt-4'>
      <Card>
        <Card.Header bg='none'>
          <Accordion.Toggle
            as={Button}
            variant='info'
            eventKey='0'
            style={{ textDecoration: 'none' }}
          >
            Active users{' '}
            <Badge variant='light' className='ml-1'>
              {activeUsers}
            </Badge>
          </Accordion.Toggle>
        </Card.Header>
        {usersArr.map(([userId, obj]) => (
          <Accordion.Collapse eventKey='0' key={userId}>
            <Card.Body>
              <RiRadioButtonLine
                className={`mb-1 ${
                  obj.online ? 'text-success' : 'text-secondary'
                }`}
                size='0.8em'
              />{' '}
              {obj.username}
            </Card.Body>
          </Accordion.Collapse>
        ))}
      </Card>
    </Accordion>
  )
}

      
      





Komponen MessageForm adalah formulir standar untuk mengirim pesan. Picker adalah komponen emoji yang disediakan oleh perpustakaan emoji-mart. Komponen ini ditampilkan / disembunyikan dengan menekan sebuah tombol.



komponen / MessageForm.js:



import { useState } from 'react'
// 
import { Form, Button } from 'react-bootstrap'
// 
import { Picker } from 'emoji-mart'
// 
import { FiSend } from 'react-icons/fi'
import { GrEmoji } from 'react-icons/gr'

//        
export const MessageForm = ({ username, sendMessage }) => {
  //     
  const [text, setText] = useState('')
  //   
  const [showEmoji, setShowEmoji] = useState(false)

  //   
  const handleChangeText = (e) => {
    setText(e.target.value)
  }

  //  / 
  const handleEmojiShow = () => {
    setShowEmoji((v) => !v)
  }

  //   
  //    ,     
  const handleEmojiSelect = (e) => {
    setText((text) => (text += e.native))
  }

  //   
  const handleSendMessage = (e) => {
    e.preventDefault()
    const trimmed = text.trim()
    if (trimmed) {
      sendMessage({ messageText: text, senderName: username })
      setText('')
    }
  }

  return (
    <>
      <Form onSubmit={handleSendMessage}>
        <Form.Group className='d-flex'>
          <Button variant='primary' type='button' onClick={handleEmojiShow}>
            <GrEmoji />
          </Button>
          <Form.Control
            value={text}
            onChange={handleChangeText}
            type='text'
            placeholder='Message...'
          />
          <Button variant='success' type='submit'>
            <FiSend />
          </Button>
        </Form.Group>
      </Form>
      {/*  */}
      {showEmoji && <Picker onSelect={handleEmojiSelect} emojiSize={20} />}
    </>
  )
}

      
      





Komponen MessageListItem adalah item daftar pesan. TimeAgo adalah komponen untuk memformat tanggal dan waktu. Dibutuhkan tanggal dan mengembalikan string seperti "1 bulan yang lalu". Baris ini diperbarui secara real time. Hanya pengguna yang mengirimnya yang dapat menghapus pesan.



komponen / MessageListItem.js:



//    
import TimeAgo from 'react-timeago'
// 
import { ListGroup, Card, Button } from 'react-bootstrap'
// 
import { AiOutlineDelete } from 'react-icons/ai'

//         
export const MessageListItem = ({ msg, removeMessage }) => {
  //   
  const handleRemoveMessage = (id) => {
    removeMessage(id)
  }

  const { messageId, messageText, senderName, createdAt, currentUser } = msg
  return (
    <ListGroup.Item
      className={`d-flex ${currentUser ? 'justify-content-end' : ''}`}
    >
      <Card
        bg={`${currentUser ? 'primary' : 'secondary'}`}
        text='light'
        style={{ width: '55%' }}
      >
        <Card.Header className='d-flex justify-content-between align-items-center'>
          {/*  TimeAgo    */}
          <Card.Text as={TimeAgo} date={createdAt} className='small' />
          <Card.Text>{senderName}</Card.Text>
        </Card.Header>
        <Card.Body className='d-flex justify-content-between align-items-center'>
          <Card.Text>{messageText}</Card.Text>
          {/*        */}
          {currentUser && (
            <Button
              variant='none'
              className='text-warning'
              onClick={() => handleRemoveMessage(messageId)}
            >
              <AiOutlineDelete />
            </Button>
          )}
        </Card.Body>
      </Card>
    </ListGroup.Item>
  )
}

      
      





Komponen "MessageList" adalah daftar pesan. Ini menggunakan komponen "MessageListItem".



komponen / MessageList.js:



import { useRef, useEffect } from 'react'
// 
import { ListGroup } from 'react-bootstrap'
// 
import { MessageListItem } from './MessageListItem'

//    (inline styles)
const listStyles = {
  height: '80vh',
  border: '1px solid rgba(0,0,0,.4)',
  borderRadius: '4px',
  overflow: 'auto'
}

//         
//          "MessageListItem"
export const MessageList = ({ messages, removeMessage }) => {
  //  ""          
  const messagesEndRef = useRef(null)

  //  ,     
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({
      behavior: 'smooth'
    })
  }, [messages])

  return (
    <>
      <ListGroup variant='flush' style={listStyles}>
        {messages.map((msg) => (
          <MessageListItem
            key={msg.messageId}
            msg={msg}
            removeMessage={removeMessage}
          />
        ))}
        <span ref={messagesEndRef}></span>
      </ListGroup>
    </>
  )
}

      
      





Komponen Aplikasi adalah komponen utama aplikasi. Ini mendefinisikan rute dan merakit antarmuka.



src / App.js:



//  
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
// 
import { Container } from 'react-bootstrap'
// 
import { Home, ChatRoom } from 'components'

// 
const routes = [
  { path: '/', name: 'Home', Component: Home },
  { path: '/:roomId', name: 'ChatRoom', Component: ChatRoom }
]

export const App = () => (
  <Router>
    <Container style={{ maxWidth: '512px' }}>
      <h1 className='mt-2 text-center'>React Chat App</h1>
      <Switch>
        {routes.map(({ path, Component }) => (
          <Route key={path} path={path} exact>
            <Component />
          </Route>
        ))}
      </Switch>
    </Container>
  </Router>
)

      
      





Terakhir, file "src / index.js" adalah titik masuk JavaScript untuk Webpack. Itu melakukan gaya global dan rendering komponen App.



src / index.js:



import React from 'react'
import { render } from 'react-dom'
import { createGlobalStyle } from 'styled-components'
// 
import 'bootstrap/dist/css/bootstrap.min.css'
import 'emoji-mart/css/emoji-mart.css'
// 
import { App } from './App'
//   "" 
const GlobalStyles = createGlobalStyle`
.card-header {
  padding: 0.25em 0.5em;
}
.card-body {
  padding: 0.25em 0.5em;
}
.card-text {
  margin: 0;
}
`

const root = document.getElementById('root')
render(
  <>
    <GlobalStyles />
    <App />
  </>,
  root
)

      
      





Nah, kami telah selesai mengembangkan aplikasi kecil kami.



Saatnya memastikan itu berhasil. Untuk melakukan ini, di direktori root proyek (react-chat), jalankan perintah "mulai benang". Setelah itu, di tab browser yang terbuka, Anda akan melihat sesuatu seperti ini:















Bukan sebuah kesimpulan



Jika Anda ingin meningkatkan aplikasinya, berikut beberapa ide:



  • Tambahkan DB untuk pengguna (menggunakan lowdb yang sama)
  • Tambahkan ruang kedua - untuk ini, cukup menerapkan pemrosesan daftar pesan yang terpisah di server
  • ( ) —
  • MongoDB Cloud Mongoose; Express
  • : (, , ..) — react-filepond, — multer; WebRTC
  • Dari yang lebih eksotis: tambahkan suara ke teks dan terjemahkan pesan suara menjadi teks - Anda dapat menggunakan react-speech-kit untuk ini


Beberapa ide ini termasuk dalam rencana saya untuk meningkatkan obrolan.



Terima kasih atas perhatiannya dan semoga harimu menyenangkan.



All Articles