Entwickeln eines Reaktions-Chats mit Socket.IO





Guten Tag, Freunde!



Ich möchte Ihnen meine Erfahrungen mit der Entwicklung eines einfachen Chats in React mithilfe der Socket.IO- Bibliothek mitteilen .



Es wird davon ausgegangen, dass Sie mit der genannten Bibliothek vertraut sind. Wenn Sie nicht vertraut sind, finden Sie hier eine verwandte Anleitung mit Beispielen zum Erstellen einer Tudushka und zum Chatten in Vanille-JavaScript .



Es wird auch davon ausgegangen, dass Sie mit Node.js zumindest oberflächlich vertraut sind .



In diesem Artikel werde ich mich auf die praktischen Aspekte der Verwendung von Socket.IO, React und Node.js konzentrieren.



Unser Chat wird die folgenden Hauptfunktionen haben:



  • Zimmerauswahl
  • Nachrichten senden
  • Nachrichten vom Absender löschen
  • Speichern von Nachrichten in einer lokalen Datenbank im JSON-Format
  • Speichern des Benutzernamens und der ID im lokalen Speicher des Browsers
  • Anzeige der Anzahl der aktiven Benutzer
  • Anzeigen einer Benutzerliste mit einem Online-Indikator


Wir werden auch die Möglichkeit implementieren, Emoji zu senden .



Wenn Sie interessiert sind, folgen Sie mir bitte.



Für diejenigen, die nur am Code interessiert sind: Hier ist der Link zum Repository .



Sandkasten:





Projektstruktur und Abhängigkeiten



Beginnen wir mit der Erstellung eines Projekts:



mkdir react-chat
cd react-chat

      
      





Erstellen Sie einen Client mit der Create React App :



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

      
      





In Zukunft werde ich Garn verwenden : um Abhängigkeiten zu installieren yarn add = npm i, yarn start = npm start, yarn dev = npm run dev



.



Wechseln Sie in das Verzeichnis "client" und installieren Sie zusätzliche Abhängigkeiten:



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

      
      







Der Abschnitt "Abhängigkeiten" der Datei "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"
}

      
      





Gehen Sie zurück zum Stammverzeichnis (React-Chat), erstellen Sie das "Server" -Verzeichnis, gehen Sie dorthin, initialisieren Sie das Projekt und installieren Sie die Abhängigkeiten:



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

      
      





  • socket.io - Socket.IO Backend
  • lowdb - lokale Datenbank im JSON-Format
  • Supervisor - Entwicklungsserver (Alternative zu nodemon , das mit der neuesten stabilen Version von Node.js nicht richtig funktioniert; es hat etwas mit falschem Starten / Stoppen von untergeordneten Prozessen zu tun)


Fügen Sie den Befehl "start" hinzu, um den Produktionsserver zu starten, und den Befehl "dev", um den Entwicklungsserver zu starten. 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"
  }
}

      
      





Gehen Sie zurück zum Stammverzeichnis (React-Chat), initialisieren Sie das Projekt und installieren Sie die Abhängigkeiten:



  cd ..
  yarn init -yp
  yarn add nanoid concurrently

      
      





  • nanoidgenerierende Kennungen (werden sowohl auf dem Client als auch auf dem Server verwendet)
  • gleichzeitig - gleichzeitige Ausführung von zwei oder mehr Befehlen


react-chat / package.json (beachten Sie, dass die Befehle für npm anders aussehen; siehe die gleichzeitigen Dokumente):



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

      
      





Großartig, wir sind mit der Bildung der Hauptstruktur des Projekts und der Installation der notwendigen Abhängigkeiten fertig. Beginnen wir mit der Implementierung des Servers.



Serverimplementierung



Die Struktur des "Server" -Verzeichnisses:



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

      
      





In der Datei "index.js" gehen wir wie folgt vor:



  • Erstellen eines HTTP-Servers
  • Wir verbinden Socket.IO damit
  • Wir starten den Server auf Port 5000
  • Registrieren von Ereignishandlern beim Anschließen eines Sockets


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

      
      





In der Datei "handlers / messageHandlers.js" gehen wir wie folgt vor:



  • Einrichten einer lokalen Datenbank im JSON-Format mit lowdb
  • Wir schreiben erste Daten in die Datenbank
  • Funktionen zum Empfangen, Hinzufügen und Löschen von Nachrichten erstellen
  • Wir registrieren die Verarbeitung der entsprechenden Ereignisse:

    • message: get - empfangen Sie Nachrichten
    • message: add - füge eine Nachricht hinzu
    • message: remove - löscht eine Nachricht




Nachrichten sind Objekte mit den folgenden Eigenschaften:



  • messageId (string) - Nachrichtenkennung
  • userId (string) - Benutzer-ID
  • Absendername (Zeichenfolge) - Absendername
  • messageText (string) - Nachrichtentext
  • createdAt (Datum) - Erstellungsdatum


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

      
      





In der Datei "handlers / userHandlers.js" gehen wir wie folgt vor:



  • Erstellen Sie eine normalisierte Struktur mit Benutzern
  • Wir erstellen Funktionen zum Abrufen, Hinzufügen und Entfernen von Benutzern
  • Wir registrieren die Verarbeitung der entsprechenden Ereignisse:

    • user: get - get users
    • Benutzer: Hinzufügen - Benutzer hinzufügen
    • Benutzer: verlassen - Benutzer löschen




Wir könnten auch lowdb verwenden, um mit der Liste der Benutzer zu arbeiten. Sie können dies tun, wenn Sie möchten. Ich werde mich mit Ihrer Erlaubnis auf das Objekt beschränken.



Die normalisierte Struktur (Objekt) der Benutzer hat das folgende Format:



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

      
      





Tatsächlich löschen wir keine Benutzer, sondern übertragen ihren Status auf offline (Zuweisen der Eigenschaft "online" zu "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)
}

      
      





Wir starten den Server, um seine Leistung zu überprüfen:



yarn dev

      
      





Wenn die Meldung „Server bereit. Port: 5000 "und eine Datei" messages.json "mit Anfangsdaten wurden im Verzeichnis" db "angezeigt. Dies bedeutet, dass der Server wie erwartet funktioniert und Sie mit der Implementierung des Client-Teils fortfahren können.



Client-Implementierung



Mit dem Kunden ist alles etwas komplizierter. Die Struktur des "Client" -Verzeichnisses:



|--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)
  ...

      
      





Wie der Name schon sagt, enthält das Verzeichnis "components" Anwendungskomponenten (Teile der Benutzeroberfläche, Module) und das Verzeichnis "hooks" Benutzer-Hooks ("custom"), deren Hauptverzeichnis useChat () ist.



Dateien "package.json" in den Komponentenverzeichnissen haben ein einzelnes Feld "main" mit dem Wert des Pfads zur JS-Datei, zum Beispiel:



{
  "main": "./Home"
}

      
      





Auf diese Weise können Sie eine Komponente aus einem Verzeichnis importieren, ohne einen Dateinamen anzugeben. Beispiel:



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

      
      





Die Dateien "components / index.js" und "hooks / index.js" werden zum Aggregieren und erneuten Exportieren von Komponenten bzw. Hooks verwendet.



components / 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'

      
      





Auf diese Weise können Sie Komponenten und Hooks erneut nach Verzeichnis und gleichzeitig importieren. Durch Aggregation und Reexport werden benannte Komponentenexporte verwendet (in der React-Dokumentation wird die Verwendung des Standardexports empfohlen).



Die Datei jsconfig.json sieht folgendermaßen aus:



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

      
      





Dies "teilt" dem Compiler mit, dass der Import von Modulen aus dem Verzeichnis "src" beginnt, sodass beispielsweise Komponenten wie folgt importiert werden können:



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

      
      





Beginnen wir mit benutzerdefinierten Haken.



Sie können vorgefertigte Lösungen verwenden. Hier sind zum Beispiel die Hooks, die von der React-Use- Bibliothek angeboten werden :



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

      
      





Mit dem useLocalStorage () - Hook können Sie Werte im lokalen Speicher des Browsers speichern (schreiben und abrufen). Wir werden es verwenden, um den Benutzernamen und die Benutzer-ID zwischen Browsersitzungen zu speichern. Wir möchten den Benutzer nicht jedes Mal zwingen, seinen Namen einzugeben, aber die ID wird benötigt, um die Nachrichten zu bestimmen, die diesem Benutzer gehören. Der Hook erhält einen Namen für den Schlüssel und optional einen Anfangswert.



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

      
      





Der Hook "useBeforeUnload ()" wird verwendet, um eine Nachricht anzuzeigen oder eine Funktion auszuführen, wenn die Seite (Browser-Registerkarte) neu geladen oder geschlossen wird. Wir werden es verwenden, um ein "Benutzer: Verlassen" -Ereignis an den Server zu senden, um den Status des Benutzers umzuschalten. Ein Versuch, den Versand des angegebenen Ereignisses mithilfe des vom Hook "useEffect ()" zurückgegebenen Rückrufs zu implementieren, war nicht erfolgreich. Der Hook akzeptiert einen Parameter, ein Grundelement oder eine Funktion.



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
  }, [])
}

      
      





Der useChat () - Hook ist der Haupthook für unsere Anwendung. Es wird einfacher, wenn ich es Zeile für Zeile kommentiere.



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

      
      





Standardmäßig werden alle Clientanforderungen an localhost: 3000 (den Port, auf dem der Entwicklungsserver ausgeführt wird) gesendet. Um Anforderungen an den Port umzuleiten, auf dem der "Server" -Server ausgeführt wird, muss ein Proxy durchgeführt werden. Fügen Sie dazu der Datei "src / package.json" die folgende Zeile hinzu:



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

      
      





Es bleibt die Implementierung der Anwendungskomponenten.



Die Home-Komponente ist das erste, was der Benutzer sieht, wenn er die Anwendung startet. Es enthält ein Formular, in dem der Benutzer aufgefordert wird, seinen Namen einzugeben und einen Raum auszuwählen. In der Realität hat der Benutzer im Fall eines Zimmers keine Wahl, es steht nur eine Option (kostenlos) zur Verfügung. Die zweite (deaktivierte) Option (Job) ist die Möglichkeit, die Anwendung zu skalieren. Die Anzeige der Schaltfläche zum Starten eines Chats hängt vom Feld mit dem Benutzernamen ab (wenn dieses Feld leer ist, wird die Schaltfläche nicht angezeigt). Die Schaltfläche ist eigentlich ein Link zur Chat-Seite.



Komponenten / 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>
  )
}

      
      





Die UserList-Komponente ist, wie der Name schon sagt, eine Liste von Benutzern. Es enthält ein Akkordeon, die Liste selbst und Indikatoren für die Online-Präsenz der Benutzer.



Komponenten / 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>
  )
}

      
      





Die MessageForm-Komponente ist ein Standardformular zum Senden von Nachrichten. Picker ist eine Emoji-Komponente, die von der Emoji-Mart-Bibliothek bereitgestellt wird. Diese Komponente wird per Knopfdruck ein- / ausgeblendet.



Komponenten / 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} />}
    </>
  )
}

      
      





Die MessageListItem-Komponente ist ein Nachrichtenlistenelement. TimeAgo ist eine Komponente zum Formatieren von Datum und Uhrzeit. Es nimmt ein Datum und gibt eine Zeichenfolge wie "vor 1 Monat" zurück. Diese Zeile wird in Echtzeit aktualisiert. Nur der Benutzer, der sie gesendet hat, kann Nachrichten löschen.



Komponenten / 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>
  )
}

      
      





Die Komponente "MessageList" ist eine Liste von Nachrichten. Es verwendet die Komponente "MessageListItem".



Komponenten / 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>
    </>
  )
}

      
      





Die App-Komponente ist die Hauptkomponente der Anwendung. Es definiert Routen und stellt die Schnittstelle zusammen.



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>
)

      
      





Schließlich ist die Datei "src / index.js" der JavaScript-Einstiegspunkt für Webpack. Es übernimmt das globale Styling und Rendern der App-Komponente.



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
)

      
      





Nun, wir haben die Entwicklung unserer kleinen Anwendung abgeschlossen.



Es ist Zeit sicherzustellen, dass es funktioniert. Führen Sie dazu im Stammverzeichnis des Projekts (React-Chat) den Befehl "Garnstart" aus. Danach sollte auf der sich öffnenden Browser-Registerkarte Folgendes angezeigt werden:















Anstelle einer Schlussfolgerung



Wenn Sie die Anwendung verbessern möchten, finden Sie hier einige Ideen:



  • Fügen Sie eine Datenbank für Benutzer hinzu (mit derselben Lowdb)
  • Fügen Sie einen zweiten Raum hinzu - dafür reicht es aus, eine separate Verarbeitung von Nachrichtenlisten auf dem Server zu implementieren
  • ( ) —
  • MongoDB Cloud Mongoose; Express
  • : (, , ..) — react-filepond, — multer; WebRTC
  • Exotischer: Fügen Sie dem Text Voice-Over hinzu und übersetzen Sie Sprachnachrichten in Text - Sie können hierfür das React-Speech-Kit verwenden


Einige dieser Ideen sind in meinen Plänen zur Verbesserung des Chats enthalten.



Vielen Dank für Ihre Aufmerksamkeit und einen schönen Tag.



All Articles