برنامه نویسی

[Hono] برنامه پیام رسانی ساده با استفاده از Bun و WebSocket

ما اغلب شاهد اجرای WebSocket با استفاده از چارچوب Express و Socket.io هستیم. با این حال، به نظر می‌رسد نمونه‌های کمتری از پیاده‌سازی WebSocket با استفاده از Hono وجود دارد، چارچوبی که شبیه Express است اما سریع‌تر و سبک‌تر است. در این مقاله پیاده سازی یک برنامه پیام رسانی ساده با استفاده از Hono و Bun که یک زمان اجرا جاوا اسکریپت است را معرفی می کنم.

این پروژه ساختار ساده ای دارد و امکان گسترش ویژگی های مختلف مانند استفاده از پایگاه داده و چندین عملکرد مدیریت اتاق وجود دارد. در ابتدا قصد داشتم مقاله ای با محوریت WebSocket بنویسم که در کمتر از 3 دقیقه قابل خواندن باشد، اما جذابیت Hono مرا جذب کرد و حجم مقاله بیش از حد انتظار افزایش یافت. حالا بیایید به جزئیات بپردازیم.

پشته فناوری

Frontend:

Backend:

زمان اجرا جاوا اسکریپت (هم فرانت اند و هم باطن):

ساختار پروژه

├── frontend
│   ├── src
│   │   ├── App.css
│   │   ├── App.tsx
│   │   ├── index.css
│   │   ├── main.tsx
│   │   └── vite-env.d.ts
│   ├── bun.lockb
│   ├── index.html
│   ├── package.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
├── server
│   └── index.ts
├── shared
│   ├── constants.ts
│   └── types.ts
├── bun.lockb
├── package.json
└── tsconfig.json
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • دایرکتوری ریشه و دایرکتوری سرور: برنامه Hono
  • دایرکتوری Frontend: برنامه React
  • دایرکتوری اشتراکی: ثابت ها و انواعی که به طور مشترک بین frontend و backend استفاده می شوند

من فایل های تنظیمات TailwindCSS و غیره را حذف کرده ام. ساختار به گونه ای است که برنامه Hono برنامه React را می پیچد. این یک پیکربندی آسان برای مدیریت در هنگام پیاده‌سازی RPC (تماس رویه از راه دور) بود، که در آن frontend و backend وابستگی‌ها و تعاریف نوع Hono را به اشتراک می‌گذارند.

مخزن:
https://github.com/yutakusuno/bun-hono-react-websocket

UI

نسخه ی نمایشی برنامه

هونو چیست؟

Hono یک فریم ورک اپلیکیشن تحت وب است که بسیار سریع و سبک است. این برنامه بر روی هر زمان اجرا جاوا اسکریپت کار می کند و شامل میان افزار داخلی و کمک کننده است. اجرای آن شبیه Express است و استفاده از آن را بصری می کند. همچنین دارای یک API تمیز و پشتیبانی درجه یک از TypeScript است.

برای جزئیات بیشتر، به https://hono.dev/ مراجعه کنید

Bun چیست؟

Bun یک زمان اجرا جاوا اسکریپت و یک جعبه ابزار همه کاره برای برنامه های جاوا اسکریپت و تایپ اسکریپت است. این در Zig نوشته شده است و به صورت داخلی از JavaScriptCore استفاده می کند، یک موتور JS عملکرد محور که برای سافاری ایجاد شده است. همچنین Node.js و Web API را به صورت بومی پیاده‌سازی می‌کند و تمام ابزارهای لازم برای ساخت برنامه‌های جاوا اسکریپت، از جمله مدیر بسته، اجرای آزمایشی و باندلر را فراهم می‌کند.

برای جزئیات بیشتر به آدرس زیر مراجعه کنید: https://bun.sh/

WebSocket چیست؟

WebSocket پروتکلی است که یک کانال ارتباطی دو طرفه پایدار بین یک مرورگر وب و یک سرور ایجاد می کند. با استفاده از این فناوری، برنامه های کاربردی وب می توانند داده ها را به صورت بلادرنگ با سرور مبادله کنند، بدون اینکه مشتری درخواست HTTP جدیدی را آغاز کند یا صفحه را بارگذاری مجدد کند.

عملیات WebSocket از طریق فرآیند زیر انجام می شود:

شروع دست دادن: مشتری درخواست HTTP زیر را به سرور ارسال می کند و درخواست ارتقاء از HTTP به WebSocket را می کند.

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پاسخ سرور: اگر سرور از WebSocket پشتیبانی کند و با ارتقا موافقت کند، سرور با دست دادن خود پاسخ می دهد و تغییر به پروتکل WebSocket را تأیید می کند.

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

انتقال داده: در حین حفظ یک اتصال باز، سرویس گیرنده و سرور می توانند داده ها را مبادله کنند.

نمودار WebSocket

این نمودار جریان اصلی ارتباط WebSocket بین مشتری و سرور را نشان می دهد. ابتدا، کلاینت از طریق یک درخواست HTTP از سرور درخواست ارتقاء به WebSocket را می کند. سپس، سرور یک پاسخ HTTP را برمی‌گرداند و سوئیچ پروتکل را تأیید می‌کند. این اتصال را باز می کند و کلاینت و سرور می توانند داده ها را مبادله کنند.

من می خواهم عمیق تر در این زمینه کاوش کنم تا آن را بهتر درک کنم، اما فعلا همین است.

نمونه های درخواست و پاسخ برای دست دادن از RFC نقل شده است.
https://datatracker.ietf.org/doc/html/rfc6455#section-1.2

پیاده سازی

با استخراج قطعات از این مخزن بر روی پیاده سازی های کلیدی تمرکز خواهم کرد. من می خواهم تا حد امکان توضیح دهم، از جمله واردات وابستگی ها و تعریف متغیرها، بنابراین بخش هایی وجود دارد که پیاده سازی واقعی در صورت نیاز بازنویسی شده است. اگر می خواهید جزئیات را درک کنید، لطفاً کد منبع مخزن را به طور موازی بخوانید و این مقاله را به عنوان مکمل در نظر بگیرید.

می توانید برنامه تکمیل شده را در اینجا بررسی کنید: https://github.com/yutakusuno/bun-hono-react-websocket

Backend: پیکربندی WebSocket

server/index.ts

import { Hono } from 'hono';
import { createBunWebSocket } from 'hono/bun';

const app = new Hono();
const { upgradeWebSocket, websocket } = createBunWebSocket();
const server = Bun.serve({
  fetch: app.fetch,
  port: 3000,
  websocket,
});

export default app;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هنگام راه اندازی یک سرور HTTP با Bun، استفاده از آن توصیه می شود Bun.serve. نمونه ای از کلاس Hono را به این کنترل کننده واکشی ارسال کنید و پورت سرور باطن را مشخص کنید. سوکت وب وارد شده از createBunWebSocket یک میان افزار Hono، مدیریت کننده WebSocket است که برای Bun پیاده سازی شده است.

Backend: اتصال و قطع اتصال WebSocket

server/index.ts

import type { ServerWebSocket } from 'bun';

const topic = 'anonymous-chat-room';

app.get(
  '/ws',
  upgradeWebSocket((_) => ({
    onOpen(_, ws) {
      const rawWs = ws.raw as ServerWebSocket;
      rawWs.subscribe(topic);
      console.log(`WebSocket server opened and subscribed to topic '${topic}'`);
    },
    onClose(_, ws) {
      const rawWs = ws.raw as ServerWebSocket;
      rawWs.unsubscribe(topic);
      console.log(
        `WebSocket server closed and unsubscribed from topic '${topic}'`
      );
    },
  }))
);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

رفتار زمانی که یک اتصال WebSocket باز و بسته می شود، تعریف می شود. هنگامی که یک اتصال باز می شود، شروع به اشتراک در یک موضوع خاص می کند synonymous-chat-room، و هنگامی که یک اتصال بسته می شود، به آن اشتراک پایان می دهد. در این برنامه، وقتی یک کاربر ناشناس صفحه ای را باز می کند، یک ارتباط WebSocket برقرار می کند و شروع به اشتراک در موضوع می کند. این بار محدودیتی برای تعداد افرادی که می توانند در یک موضوع مشترک شوند وجود ندارد.

اشتراک در موضوعات به طور مستقیم در پیاده سازی Hono پشتیبانی نمی شود، بنابراین باید از Bun استفاده کنیم ServerWebSocket. به طور خاص، این امر با گسترش دادن به دست می آید ws که در onOpen. با نگاهی به کد منبع Hono، ws.raw WebSocket سرور Bun است و با استفاده از آن می توانیم WebSocket بومی Bun را مدیریت کرده و اشتراک موضوع را پیاده سازی کنیم. نام هر موضوعی را به عنوان یک رشته برای اشتراک و لغو اشتراک ارسال کنید.

پیاده سازی Bun WebSocket توسط Hono:
https://github.com/honojs/hono/blob/main/src/adapter/bun/websocket.ts

اسناد رسمی برای Bun WebSocket:
https://bun.sh/docs/api/websockets

Backend: /messages Endpoint

server/index.ts

import { zValidator } from '@hono/zod-validator';

const messagesRoute = app
  .get('/messages', (c) => {
    return c.json(messages);
  })
  .post(
    '/messages',
    zValidator('form', MessageFormSchema, (result, c) => {
      if (!result.success) {
        return c.json({ ok: false }, 400);
      }
    }),
    async (c) => {
      const param = c.req.valid('form');
      const currentDateTime = new Date();
      const message: Message = {
        id: Number(currentDateTime),
        date: currentDateTime.toLocaleString(),
        ...param,
      };
      const data: DataToSend = {
        action: publishActions.UPDATE_CHAT,
        message: message,
      };

      server.publish(topic, JSON.stringify(data));

      return c.json({ ok: true });
    }
  )
  .delete('/messages/:id', (c) => {
    // Logic of message deletion
  });

export type AppType = typeof messagesRoute;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این منبع پیام از روش های GET، POST، DELETE پشتیبانی می کند. متد GET تاریخچه پیام کاربران مشترک در یک موضوع را بازیابی می کند، روش POST یک پیام جدید ایجاد می کند و روش DELETE یک پیام خاص را حذف می کند. در اینجا، من بر توضیح POST /messages تمرکز خواهم کرد.

نمونه سرور هنگام ایجاد سرور Bun برای استفاده در این نقطه پایانی ایجاد شد. با تماس publish() بر روی server به عنوان مثال، ما می توانیم برای همه مشتریانی که در یک موضوع مشترک هستند، پخش کنیم. طبق سند رسمی امکان پخش برای همه مشترکین تاپیک به استثنای سوکتی که به نام publish()، اما این بار استفاده نمی شود.

همچنین، مانند Express، می‌توانیم قبل از پردازش منطق منبع، میان‌افزار را با عبور از یک handler در نقطه پایانی پیاده‌سازی کنیم. این بخشی از zValidator. این به ما این امکان را می دهد که رسیدگی کنیم param به روشی ایمن از نظر نوع

که در type AppType، نوع API تعریف شده در messagesRoute صادر می شود. این در پیاده سازی RPC در فرانت اند که بعداً توضیح داده شد، استفاده می شود. با به اشتراک گذاشتن مشخصات API با مشتری، می‌توانیم به یک اجرای API ایمن از نوع دست یابیم.

shared/types.ts

import { z } from 'zod';

export const MessageFormSchema = z.object({
  userId: z.string().min(1),
  text: z.string().trim().min(1),
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

من از zod برای MessageFormSchema. با ترکیب TypeScript و zod می‌توانیم اعتبارسنجی دقیق‌تری را اجرا کنیم.

Frontend: راه اندازی WebSocket

frontend/src/App.tsx

const [messages, setMessages] = useState<Message[]>([]);

useEffect(() => {
  const socket = new WebSocket('ws://localhost:3000/ws');

  socket.onopen = (event) => {
    console.log('WebSocket client opened', event);
  };

  socket.onmessage = (event) => {
    try {
      const data: DataToSend = JSON.parse(event.data.toString());
      switch (data.action) {
        case publishActions.UPDATE_CHAT:
          setMessages((prev) => [...prev, data.message]);
          break;
        case publishActions.DELETE_CHAT:
          setMessages((prev) =>
            prev.filter((message) => message.id !== data.message.id)
          );
          break;
        default:
          console.error('Unknown data:', data);
      }
    } catch (_) {
      console.log('Message from server:', event.data);
    }
  };
  socket.onclose = (event) => {
    console.log('WebSocket client closed', event);
  };

  return () => {
    socket.close();
  };
}, []);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

من یک سرویس گیرنده WebSocket ایجاد می کنم و آن را به سرور متصل می کنم. من رفتار را برای زمانی که اتصال WebSocket باز می شود، زمانی که یک پیام دریافت می شود و زمانی که اتصال بسته می شود، تعریف می کنم. که در socket.onmessage، من از یک دستور switch برای انشعاب بر اساس نوع عمل دریافت شده از backend استفاده می کنم. این به ما امکان می دهد تا موارد استفاده مختلف را مدیریت کنیم.

// shared/constants.ts
export const publishActions = {
  UPDATE_CHAT: 'UPDATE_CHAT',
  DELETE_CHAT: 'DELETE_CHAT',
} as const;

// shared/types.ts
type PublishAction = (typeof publishActions)[keyof typeof publishActions];

export type Message = { id: number; date: string } & MessageFormValues;

export type DataToSend = {
  action: PublishAction;
  message: Message;
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در اینجا ثابت ها و تعاریف نوع استفاده شده برای WebSocket و مدیریت پیام آورده شده است. این PublishAction type مجموع مقادیر the را استنباط می کند publishActions شی و نوع شمارش را استخراج می کند.

Frontend: ارسال پیام

frontend/src/App.tsx

import { hc } from 'hono/client';
import type { AppType } from '@server/index';

const honoClient = hc<AppType>("http://localhost:3000");

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  try {
    const validatedValues = MessageFormSchema.parse(formValues);
    const response = await honoClient.messages.$post({
      form: validatedValues,
    });
    if (!response.ok) {
      throw new Error('Failed to send message');
    }

  } catch (error) {
    // Error Handling Logic
  }
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

که در handleSubmit، من منطق ارسال پیام جدید را تعریف می کنم. هنگامی که متن ارسال می شود، مقدار ورودی را تأیید می کند و پیام جدید را به عنوان یک درخواست POST به سرور ارسال می کند.

نکته قابل توجه این است که من از مشتری Hono به جای fetch API استفاده می کنم. من مشخص می کنم AppType در backend و URL backend به صادر می شود hc، و یک type-safe تعریف کنید honoClient. این امکان اجرای RPC را فراهم می کند. گیف زیر نمایشی از TypeScript است که هنگام ارسال مقدار نادرست نوع داده، خطای کامپایل ایجاد می کند. $post.

RPC Gif

این به ما این امکان را می‌دهد تا با ترکیب کردن، یک API ایمن تایپ را پیاده‌سازی کنیم zod و RPC

<form
  method="post"
  onSubmit={handleSubmit}
  className="flex items-center space-x-2"
>
  <input name="userId" defaultValue={formValues.userId} hidden />
  <input
    name="text"
    value={formValues.text}
    onChange={handleInputChange}
    className="flex-grow p-2 border border-gray-800 rounded-md bg-gray-800 text-white"
  />
  <button
    type="submit"
    className="px-4 py-2 bg-blue-500 text-white rounded-md"
  >
    Send
  </button>
</form>
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پیاده سازی UI فقط برای بخش فرم ارسال پیام استخراج می شود. این پیکربندی ساده است، قرار دادن یک پیام input برچسب و یک دکمه ارسال پیام در داخل form برچسب زدن هنگامی که دکمه ارسال فشار داده می شود، handleSubmit راه اندازی می شود و یک درخواست به باطن ارسال می شود.

این مقدمه اجرای اصلی است. شخصاً متوجه شدم که راه‌اندازی WebSocket با Hono و Bun دشوار نیست و احساس کردم سطح دشواری آن معادل پیاده‌سازی WebSocket با Socket.io در Express است. من قصد داشتم یک پست در WebSocket بنویسم، اما پیاده سازی یک API ایمن نوع با استفاده از RPC با Hono نیز تجربه توسعه بسیار خوبی را ارائه داد، بنابراین پست طولانی شد. من مشتاقانه منتظر به روز رسانی های آینده Hono هستم.

در مورد آن است. کد نویسی مبارک!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا