STOMP هو Protocol بسيط للغاية ولكن قوي لإرسال الرسائل يتم تنفيذه من قبل خوادم معروفة مثل RabbitMQ و ActiveMQ و Apollo. إستخدام STOMP بالوسائط التي تتم مع الWebSocket هو Protocol بسيط ومن ثم يصبح خيار شعبي لإرسال الرسائل من خلال المتصفح الويب لأن بروتوكولات مثل AMQP قائمة بالحدود من قبل المتصفحات التي لا تسمح بالاتصالات الTCP.

للاستخدام من خلال WebSocket لل STOMP يمكنك استخدام @stomp/stompjs ولكن هذا مع دالات معقدة و API معقد يهدف لأحداث تخصصية أكثر. بحسن الحظ ، يوجد أيضًا الأقل معروف @stompjs/rx-stomp الذي يوفر واجهة جيدة عبر RxJS المتواجدين. المتواجدين ليس خاصة ب Angular وهم مناسبون جدًا مع كيفية عمل React. إنه واجهة جيدة عندما تكون تركيب أعمال التواصل المعقدة والأنابيب التي تتضمن العديد من مصادر الرسائل المختلفة.

يتبع الدرس التالي مسارًا شبيهًا بالإصدار الأولي في Angular ولكن الهيكل المكوني ونمط الكود موجه نحو أسلوب الوظائف في React.

ملاحظة: هذا الدرس مكتوب ب TypeScript الفائق من الدقة ولكن البرمجيات الجاهزة تشابه جدًا لأن لدينا فقط 5 تعريفات للأنواع. للبرمجيات الجاهزة لل JavaScript يمكنك تخطي الأوامر التعريفية والتعريفات.

جدول محتويات

الأهداف

هنا، سنبني تطبيق تراسل مبسط يظهر جوانب مختلفة لـ RxStomp عبر مكونات مختلفة. على النطاق العام نريد أن نحصل على:

  • واجهة React متصلة مع RxStomp إلى مخزن STOMP.

  • عرض حالة الاتصال الحية وفقاً لإتصالنا بمخزن STOMP.

  • مادة Pub/Sub لأي موضوع قابل للتكوين.

  • توزيع منطق RxStomp عبر مكونات متعددة لإظهار كيفية فصل المنطق والمسؤولية.

  • توافق دوران اتصال RxStomp / اشتراكات الدوران مع دوران مكونات React لضمان عدم وجود تسربات أو مراقبات غير مغلقة.

الأحداث الملزمة

  • يجب أن يكون لديك مخزن STOMP الذي يمكن للتطبيق الReact الاتصال به. في هذه الحالة ، سنستخدم RabbitMQ مع تمديد rabbitmq_web_stomp.
  • أحدث 版本 React. سيستخدم هذا التورية v18 على الإطلاق ، على الرغم من أن 版本 أقدم قد تعمل أيضًا بشكل جيد.
  • سيساعد أي معرفة بسيطة بشأن المشاهدات المراقبة أيضًا.

محرك STOMP المبدأ مع RabbitMQ

إذا أردت استخدام RabbitMQ أيضًا (غير ملزم بالضبط) ، هذه هي دروس التثبيت لأنظمة التشغيل المختلفة. لإضافة التمديد، سيتوجب عمل:

$ rabbitmq-plugins enable rabbitmq_web_stomp

إذا كان بإمكانك استخدام Docker ، سيتوجب عمل ملف Docker مماث

FROM rabbitmq:3.8.8-alpine

run rabbitmq-plugins enable --offline rabbitmq_web_stomp

EXPOSE 15674

قالب مبتدأ React

في هذا التوريتال، سنستخدم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 Hook. هذا التعامل يدير تفعيل الاتصال مع دورة حياة المكون.

// 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 enum RxStompState يمثل أحد الحالات الممكنة للاتصال مع منصةنا. هدفنا المقبل هو عرض حالة الاتصال في واجهتنا البرمجية.

دعونا نبني مكون جديد لهذا الغرض يسمى 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$ للترابط بحرفية ما نسميه 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 جديد. أولاً ، يمكننا إنشاء المكون مع حقل اسم المستخدم و رسالة مخصص مرتبط بالمدخلات.

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

دعنا نضمنه في التطبيق الخاص بنا مع تبديل للانضمام إلى غرفة الدردشة:

// 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.ts
interface ChatMessage {
  userName: string,
  message: string
}

ملاحظة: إذا لم تكن تستخدم TypeScript ، فيمكنك تخطي إضافة هذا التعريف للنوع.

بعد ذلك ، دعنا نستخدم JSON لتسلسل الرسالة وإرسال الرسائل إلى خادم STOMP الخاص بنا باستخدام .نشر مع موضوع الوجهة والـ 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>
    </>
  )
}

لاختبارها ، حاول النقر على زر إرسال رسالة عدة مرات وانظر إذا كان التسلسل يعمل بشكل جيد. بينما لن تتمكن من رؤية أي تغييرات مرئية حتى الآن ، يجب أن تظهر سجلات الوحدة النمطية ذلك:

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

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

كيفية استقبال الرسائل

为了创建一个新的组件来显示所有用户的消息列表, bạn可以参照以下步骤和代码示例。

### MessageList 组件

#### 概述
`MessageList` 组件用于显示来自所有用户的消息列表。我们将使用 `react-chat-elements` 或 `cometchat` 库来实现这个功能。

#### 例子使用 `react-chat-elements`

“`typescript
// src/MessageDisplay.tsx
import React, { useEffect, useState } from ‘react’;
import { MessageList } from ‘react-chat-elements’;

const MessageListComponent = ({ topicName }) => {
const [messages, setMessages] = useState([]);
const messageListReferance = React.createRef();

// 模拟从服务器获取消息数据
const fetchMessages = () => {
// 这里替换为你的实际数据获取逻辑
const mockMessages = [
{
position: ‘left’,
type: ‘text’,
text: ‘Hello!’,
date: new Date(),
},
{
position: ‘right’,
type: ‘text’,
text: ‘Hi!’,
date: new Date(),
},
];
setMessages(mockMessages);
};

useEffect(() => {
fetchMessages();
}, []);

return (

console.log(‘Message clicked:’, message)}
onOpen={(message) => console.log(‘Message opened:’, message)}
onDownload={(message) => console.log(‘Message downloaded:’, message)}
onScroll={(event) => console.log(‘Message list scrolled:’, event)}
/>

);
};

export default MessageListComponent;
“`

#### 例子使用 `cometchat`

“`typescript
// src/MessageDisplay.tsx
import React, { useState, useEffect } from ‘react’;
import { CometChat } from ‘@cometchat/chat-sdk-javascript’;
import { CometChatMessageList } from ‘@cometchat/chat-uikit-react’;

const MessageListComponent = ({ topicName }) => {
const [chatUser, setChatUser] = useState(null);

useEffect(() => {
CometChat.getUser(“uid”).then((user) => {
setChatUser(user);
});
}, []);

return (

{chatUser && (

)}

);
};

export default MessageListComponent;
“`

### 使用 RxJS 管理订阅

如果你需要使用 RxJS 来管理订阅和数据处理,你可以如下所示:

“`typescript
// src/MessageDisplay.tsx
import React, { useEffect, useState } from ‘react’;
import { MessageList } from ‘react-chat-elements’;
import { pipe, map } from ‘rxjs’;
import { from } from ‘rxjs’;
import { ChatMessage } from ‘./models’; // 假设你有一个 ChatMessage 模型

const MessageListComponent = ({ topicName }) => {
const [messages, setMessages] = useState([]);
const messageListReferance = React.createRef();

// 模拟从服务器获取消息数据
const fetchMessages = () => {
// 这里替换为你的实际数据获取逻辑
const mockMessages = [
{ text: ‘Hello!’, date: new Date() },
{ text: ‘Hi!’, date: new Date() },
];

// 使用 RxJS 处理数据
from(mockMessages).pipe(
map((message) => ({ …message, position: ‘left’, type: ‘text’ }))
).subscribe((message) => {
setMessages((prevMessages) => […prevMessages, message]);
});
};

useEffect(() => {
fetchMessages();
}, []);

return (

console.log(‘Message clicked:’, message)}
onOpen={(message) => console.log(‘Message opened:’, message)}
onDownload={(message) => console.log(‘Message downloaded:’, message)}
onScroll={(event) => console.log(‘Message list scrolled:’, event)}
/>

);
};

export default MessageListComponent;
“`

### 总结

在这个示例中,我们创建了一个 `MessageList` 组件,使用 `react-chat-elements` 或 `cometchat` 库来显示消息列表。我们也展示了如何使用 RxJS 管理订阅和数据处理。

#### 使用 react-chat-elements

– 导入 `MessageList` 组件。
– 设置消息数据源。
– 使用 `dataSource` 属性传递消息数据。

#### 使用 cometchat

– 导入 `CometChatMessageList` 组件。
– 获取用户信息并设置消息请求构建器。
– 使用 `user` 和 `messagesRequestBuilder` 属性传递必要信息。

#### 使用 RxJS

– 导入必要的 RxJS 操作符。
– 使用 `pipe` 和 `map` 操作符处理数据。
– 订阅数据流并更新状态。


// 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، نقوم بتعيين الاشتراك عند التحميل وفسخ الاشتراك عند إلغاء التحميل.

باستخدام RxJS pipe و map، يمكننا تسلسل 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$.
  • يتم نشر الرسائل باستخدام rxStomp.publish مع أماكن وجهة النشر القابلة للتكوين وجسم الرسائل.
  • تم إنشاء مشاهد قابل للتحكم للموضوع المعين rxStomp.watch.
  • تم استخدام المعايير الكونزيل والمكونات الرياكت لرؤية المكتبة في عمل وتأكد من الوظائف ومراقبة القدرة على المقاومة.

يمكنك إيجاد البرمجيات النهائية على Git