[Hono] برنامه پیام رسانی ساده با استفاده از Bun و WebSocket
![[Hono] برنامه پیام رسانی ساده با استفاده از Bun و WebSocket [Hono] برنامه پیام رسانی ساده با استفاده از Bun و WebSocket](https://nabfollower.com/blog/wp-content/uploads/2024/06/Hono-برنامه-پیام-رسانی-ساده-با-استفاده-از-Bun-و-780x470.png)
ما اغلب شاهد اجرای 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 بین مشتری و سرور را نشان می دهد. ابتدا، کلاینت از طریق یک درخواست 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
.
این به ما این امکان را میدهد تا با ترکیب کردن، یک 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 هستم.
در مورد آن است. کد نویسی مبارک!