STOMP הוא פרוטוקול פשוט וחזק באמת לשדרגת מסרים, שמיועד על ידי שרתים פופולריים כמו RabbitMQ, ActiveMQ ו-Apollo. שימוש בSTOMP מעל לWebSocket הוא פרוטוקול פשוט וקל, וזו היא הבחירה פופולרית לשדרגת מסרים מהדף האינטרנטי מפני שפרוטוקולים כמו AMQP מוגבלים על ידי הדפים האינטרנטיים האלה שאינם מאפשרים קישורי TCP.

כדי להשתמש בSTOMP מעל לWebSocket, ניתן להשתמש ב@stomp/stompjs, אך לו יש צירופי הגשה מסובכים וAPI מורכב שמוכן לשימוש במקרים מותאמים יותר מיוחדים. למרבה המזל, יש גם את @stompjs/rx-stomp הידוע פחות, שמעניק ממשק נחמד דרך אובסביבלים של RxJS. אובסביבלים אינם ייעדים רק ל-Angular, והם מתאימים די טוב לאופן בו עובדת React. זהו ממשק נחמד כשמערבבים עבודות מסויימות וערוצים עם מקורות מסרים שונים.

המדריך הזה עומד על דרך די דומה לגרסה הראשונית ב-Angular, אך מבנה המולך וסגנון הקוד מותאמים יותר לסגנון פונקציונלי של React.

הערה: המדריך הזה נכתב ב-TypeScript מוקפד, אך הקוד ב-JavaScript כמעט אינו שונה מפני שיש רק 5 הצגות סוגיות. עבור הגרסה ב-JS, ניתן להימנע מהייוצאים הסוגיים לסוגיות וההגדרות.

תוכן הטבלה

מטרות

כאן, אנחנו נבנה אפליקציית מסרים פשוטה שמראה אספקטים של RxStomp בין רכיבים שונים. באופן כללי, אנחנו רוצים לקבל:

  • קישור קדמת רעיון ב-React שמחובר עם RxStomp לשרת STOMP.

  • מצב חיבור חי שמבוסס על החיבור שלנו לשרת STOMP.

  • מנגנון פוב/סב לכל נושא מתאפשרי.

  • פיצול ההגיון של RxStomp למרכיבים רבים כדי להראות איך להפריד בין ההגיון והאחריות.

  • שירוג החיים של מערכת החיבור/הרשמה RxStomp עם החיים של איברי הרשת React בכדי לוודא שאין דליקות או מעקבים שלא נסגרו.

דרישות קדם

  • צריך להיות לך שרת STOMP שמפעיל כך שיישום React יוכל להתחבר אליו. פה, אנחנו נשתמש ב RabbitMQ עם ההרחבה rabbitmq_web_stomp.

  • גירסה האחרונה של React. במדריך הלמידה נשתמש ב v18, אף על פי שגירסאות יותר קדומות עלולות לעבוד גם כן.

  • קצת מומחיות בעזרת אובסבליבים תגובה גם לעזרך.

שרת STOMP מתחיל עם RabbitMQ

אם אתה רוצה להשתמש ב RabbitMQ גם כן (לא בהחלט חובה), הנה המדריכים לשימוש בשרתים שונים להתקנה. כדי להוסיף את ההרחבה, תצטרך לראות:

$ rabbitmq-plugins enable rabbitmq_web_stomp

Dockerfile דומה ל<di

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

טבלת התחלה של ריקסט לרקע של המדריך

למדריך זה, אנחנו נשתמש בתבנית Vite react-ts. החלק המרכזי של היישומנו יהיה במולך App, ואנחנו ניצור מוליך ילדים עבור פונקציות ספציפיות של STOMP.

איך להתקין RxStomp

אנחנו נהשתמש באפליקציית npm @stomp/rx-stomp:

$ npm i @stomp/rx-stomp rxjs

זה יתקין את הגירסה 2.0.0

הערה: המדריך הזה עדיין פועל בלי לספק ברורת מידע את rxjs בגלל שהיא תלויה אחת, אבל זה מנהג טוב להיות ברור על כך.

איך לנהל קישורים והפרדות עם שרת STOMP

עכשיו, בואו נפתח App.tsx ונייצר את הלקוח הRxStomp שלנו. מפני שהלקוח אינו מצב שישנה עבור הנירופין, אנחנו נארך אותו במקרן useRef.

// src/App.tsx
import { useRef } from 'react'
import { RxStomp } from '@stomp/rx-stomp'

import './App.css'

function App() {
  const rxStompRef = useRef(new RxStomp())
  const rxStomp = rxStompRef.current

  return (
    <>
      <h1>Hello RxStomp!</h1>
    </>
  )
}

export default App

מניחים שהפורטים הבסיסיים ופרטי ההסיסמה יהיו ברצף הברת המצבים, אז בדיוק אחר כך נגדל קצת את ההגדרות של הקישור שלנו.

// src/App.tsx

import { RxStomp } from '@stomp/rx-stomp'
import type { RxStompConfig } from '@stomp/rx-stomp'
...
const rxStompConfig: RxStompConfig = {
  brokerURL: 'ws://localhost:15674/ws',
  connectHeaders: {
    login: 'guest',
    passcode: 'guest',
  },
  debug: (msg) => {
    console.log(new Date(), msg)
  },
  heartbeatIncoming: 0,
  heartbeatOutgoing: 20000,
  reconnectDelay: 200,
}

function App() {
  ...

עבור חוויה פיתוח טובה יותר, אנחנו רשמנו את כל ההודעות עם שעות זמן להפרץ להדיקת המקור והגדרנו קצבים השעות הנמוכים. ההגדרות שלך ביישום היישומך צריכה להיות שונה בהרבה, אז הבה נסתכל על הדיקסים RxStompConfig עבו

הבא, נעביר את התצורה ל-rxStomp בתוך הוק useEffect. זה מנהל את הפעלת החיבור לצד מחזור החיים של הרכיב.

// src/App.tsx
...
function App() {
  const rxStompRef = useRef(new RxStomp())
  const rxStomp = rxStompRef.current

  useEffect(() => {
    rxStomp.configure(rxStompConfig)
    rxStomp.activate()

    return () => { 
      rxStomp.deactivate() 
    }
  })
  ...

אף על פי שאין שינוי חזותי באפליקציה שלנו, בדיקת הלוגים צריכה להראות לוגים של חיבור ופינג. הנה דוגמה למה זה צריך להיראות:

Date ... >>> CONNECT
login:guest
passcode:guest
accept-version:1.2,1.1,1.0
heart-beat:20000,0

Date ... Received data 
Date ... <<< CONNECTED
version:1.2
heart-beat:0,20000
session:session-EJqaGQijDXqlfc0eZomOqQ
server:RabbitMQ/4.0.2
content-length:0

Date ... connected to server RabbitMQ/4.0.2 
Date ... send PING every 20000ms 
Date ... <<< PONG 
Date ... >>> PING

הערה: בדרך כלל, אם תראו לוגים כפולים, זה עשוי להיות סימן שפונקציונליות הכיבוי או הביטול הרשמה לא יושמה כראוי. React מציג כל רכיב פעמיים בסביבת פיתוח כדי לעזור לאנשים לתפוס באגים אלה באמצעות React.StrictMode

איך לפקח על סטטוס החיבור

ל-RxStomp יש RxStompState enum שמייצג את המצבים האפשריים של החיבור עם הברוקר שלנו. המטרה הבאה שלנו היא להציג את סטטוס החיבור בממשק המשתמש שלנו.

בואו ניצור רכיב חדש לזה שנקרא Status.tsx:

// src/Status.tsx
import { useState } from 'react'

export default function Status() {
  const [connectionStatus, setConnectionStatus] = useState('')

  return (
    <>
      <h2>Connection Status: {connectionStatus}</h2>
    </>
  )
}

נוכל להשתמש ב-rxStomp.connectionState$ observable כדי לקשור את המחרוזת connectionStatus שלנו. בדומה למה שעשינו עם useEffect, נשתמש בפעולת ההסרה כדי ל-unsubscribe().

// src/Status.tsx
import { RxStompState } from '@stomp/rx-stomp'
import { useEffect, useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'


export default function Status(props: { rxStomp: RxStomp }) {
  const [connectionStatus, setConnectionStatus] = useState('')

  useEffect(() => {
    const statusSubscription = props.rxStomp.connectionState$.subscribe((state) => {
      setConnectionStatus(RxStompState[state])
    })

    return () => {
      statusSubscription.unsubscribe()
    }
  }, [])

  return (
    <>
      <h2>Connection Status: {connectionStatus}</h2>
    </>
  )
}

כדי לצפות בו, נכלול אותו באפליקציה שלנו:

// src/App.tsx
import Status from './Status'
...
  return (
    <>
      <h1>Hello RxStomp!</h1>

      <Status rxStomp={rxStomp}/>
    </>
  )

בשלב זה, אמור להיות לך אינדיקטור חזותי שעובד על המסך. נסה לשחק על ידי הורדת שרת ה-STOMP ולראות אם הלוגים פועלים כמצופה.

איך לשלוח הודעות

בואו ניצור חדר צ'אט פשוט כדי להציג זרימת הודעות מקצה לקצה עם הברוקר.

נוכל למקם את הפונקציונליות ברכיב חדש בשם Chatroom. תחילה, ניצור את הרכיב עם שדות מותאמים אישית username ו-message המקושרים לקלטים.

// src/Chatroom.tsx
import { useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'

export default function Chatroom(props: {rxStomp: RxStomp}) {
  const [message, setMessage] = useState('')
  const [userName, setUserName] = useState(`user${Math.floor(Math.random() * 1000)}`)

  return (
    <>
      <h2>Chatroom</h2>

      <label htmlFor='username'>Username: </label>
      <input
        type='text'
        name='username'
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />

      <label htmlFor='message'>Message: </label>

      <input
        type='text'
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        name='message'
      />
    </>
  )    
}

נכלול זאת בתוך ה-App שלנו עם מתג להצטרפות לחדר הצ'אט:

// src/App.tsx
import { useEffect, useState, useRef } from 'react'
import Chatroom from './Chatroom'
...
function App() {
  const [joinedChatroom, setJoinedChatroom] = useState(false)
  ...
  return (
    <>
      <h1>Hello RxStomp!</h1>

      <Status rxStomp={rxStomp}/>

      {!joinedChatroom && (
        <button onClick={() => setJoinedChatroom(true)}>
          Join chatroom!
        </button>
      )}

      {joinedChatroom && (
        <>
          <button onClick={() => setJoinedChatroom(false)}>
            Leave chatroom!
          </button>

          <Chatroom rxStomp={rxStomp}/>
        </>
      )}

    </>
  )

הגיע הזמן לשלוח הודעות בפועל. STOMP הכי מתאים לשליחת הודעות מבוססות טקסט (נתונים בינאריים גם אפשריים). נגדיר את מבנה הנתונים שאנחנו שולחים בקובץ types חדש:

// types.ts
interface ChatMessage {
  userName: string,
  message: string
}

הערה: אם אינך משתמש ב-TypeScript, אתה יכול לדלג על הוספת הגדרת הסוג הזה.

הבא, נשתמש ב-JSON כדי לסדר את ההודעה ונשלח הודעות לשרת ה-STOMP שלנו באמצעות .publish עם נושא יעד ו-body JSON שלנו.

// src/Chatroom.tsx
import type { ChatMessage } from './types'
...
const CHATROOM_NAME = '/topic/test'

export default function Chatroom(props: {rxStomp: RxStomp}) {
  ...
  function sendMessage(chatMessage: ChatMessage) {
    const body = JSON.stringify({ ...chatMessage })
    props.rxStomp.publish({ destination: CHATROOM_NAME, body })
    console.log(`Sent ${body}`)
    setMessage('')
  }

  return (
    <>
      <h2>Chatroom</h2>

      <label htmlFor="username">Username: </label>
      <input
        type="text"
        name="username"
        value={userName}
        onChange={(e) => setUserName(e.target.value)}
      />

      <label htmlFor="message">Message: </label>

      <input
        type="text"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        name="message"
      />

      <button onClick={() => sendMessage({userName, message})}>Send Message</button>
    </>
  )
}

כדי לבדוק זאת, נסה ללחוץ על כפתור Send Message כמה פעמים ולראות אם הסידור עובד כראוי. למרות שלא תוכל לראות שינויים חזותיים עדיין, הלוגים בקונסול צריכים להציג זאת:

Date ... >>> SEND
destination:/topic/test
content-length:45

Sent {"userName":"user722","message":"1234567890"}

איך לקבל הודעות

ניצור רכיב חדש להצגת רשימת ההודעות מכל המשתמשים. בינתיים, נשתמש באותו סוג, נעביר את שם הנושא כמאפיין, ונציג הכל כרשימה. כל זה נכנס לרכיב חדש בשם MessageList.

// src/MessageDisplay.tsx
import { useEffect, useState } from 'react'
import type { RxStomp } from '@stomp/rx-stomp'
import type { ChatMessage } from './types'

export default function MessageDisplay(props: {rxStomp: RxStomp, topic: string}) {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
    {userName: 'admin', message: `Welcome to ${props.topic} room!`}
  ])

  return(
  <>
  <h2>Chat Messages</h2>
  <ul>
    {chatMessages.map((chatMessage, index) => 
      <li key={index}>
        <strong>{chatMessage.userName}</strong>: {chatMessage.message}
      </li>
    )}
  </ul>
  </>
  )
}

הגיע הזמן לחבר הכל יחד!

בדומה לניהול המנוי עם רכיב ה-Status, אנו מגדירים את המנוי בעת הטעינה, ומבטלים אותו בעת הסרת הרכיב.

באמצעות pipe ו-map של RxJS, אנחנו יכולים לפענח את ה-JSON שלנו בחזרה ל-ChatMessage. התכנון המודולרי מאפשר לך להגדיר צינור מורכב יותר לפי הצורך באמצעות אופרטורים של RxJS.

// src/MessageDisplay.tsx
...
import { map } from 'rxjs'

export default function MessageDisplay(props: {rxStomp: RxStomp, topic: string}) {
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([
    {userName: 'admin', message: `Welcome to ${props.topic} room!`}
  ])

  useEffect(() => {
    const subscription = props.rxStomp
      .watch(props.topic)
      .pipe(map((message) => JSON.parse(message.body)))
      .subscribe((message) => setChatMessages((chatMessages) => [...chatMessages, message]))

    return () => {
      subscription.unsubscribe()
    }
  }, [])

  ...

בשלב זה, ממשק המשתמש של הצ'אט אמור להציג הודעות בצורה נכונה, ואתה יכול לנסות לפתוח מספר כרטיסיות כמשתמשים שונים.

דבר נוסף שכדאי לנסות כאן הוא לכבות את שרת ה-STOMP, לשלוח כמה הודעות, ולהפעיל אותו מחדש. ההודעות אמורות להיכנס לתור מקומי ולהישלח ברגע שהשרת מוכן לפעולה. מגניב!

סיכום

בהדרכה זו:

  • התקנו @stomp/rx-stomp לחוויית פיתוח נעימה.

  • הגדרנו RxStompConfig כדי להגדיר את הלקוח שלנו עם פרטי החיבור, רישום דיבאג והגדרות טיימר.

  • השתמש ב-rxStomp.activate ו-rxStomp.deactivate לניהול מחזור החיים הראשי של הלקוח.

  • ניטור מצב המנוי באמצעות ה-rxStomp.connectionState$ observable.

  • פרסום הודעות באמצעות rxStomp.publish עם יעדים וגופי הודעות ניתנים להגדרה.

  • יצירת observable לנושא מסוים באמצעות rxStomp.watch.

  • שימוש ביומני קונסול וב-React components כדי לראות את הספרייה בפעולה, ולאמת פונקציונליות וסובלנות לתקלות.

תוכל למצוא את הקוד הסופי ב-Gitlab: https://gitlab.com/harsh183/rxstomp-react-tutorial. ניתן להשתמש בו גם כתבנית התחלה ולדווח על כל בעיה שתצוץ.