اشیاء بادوام CloudFlare و Nuxt: ساختن یک برنامه چت در زمان واقعی

از زمان شروع کار در Nuxflare ، این بیشترین درخواست موضوع را گرفته است.
اشیاء با دوام Cloudflare چیست؟ چگونه می توانیم با NUXT از آنها استفاده کنیم؟
اشیاء بادوام قدرتمند و به طرز شگفت آور مقرون به صرفه هستند ، اما در ابتدا می توانند کمی مرموز به نظر برسند. من فکر کردم که آنها را در یک سری پست ها تجزیه می کنم که توضیح می دهد چگونه می توان از آنها بیشترین بهره را برد.
در اینجا پیش نمایش آنچه را که می خواهیم بسازیم آورده شده است:
آن را به صورت زنده امتحان کنید: https://websockets-demo.nuxflare.com
از repo github دیدن کنید: https://github.com/nuxflare/dabor-websockets
بیایید شروع کنیم
من از Bun برای خنک DX استفاده می کنم ، اما می توانید همراه با NPM ، PNPM یا نخ را دنبال کنید – هرچه راحت تر باشید.
ابتدا بیایید یک پروژه جدید Nuxt ایجاد کنیم:
bunx nuxi@latest init durable-websockets -t v4-compat
ما از حالت سازگاری Nuxt 4 استفاده می کنیم ، که به ما امکان می دهد از ساختار پروژه Nuxt 4 استفاده کنیم و هنگام انتشار برای آن آماده شویم.
حال ، بیایید وابستگی ها را نصب کنیم:
bunx nuxi@latest module add ui vueuse # Nuxt UI v3, @vueuse/core
bun i -D nuxflare @cloudflare/workers-types
ما از Nuxflare برای استقرار دو چیز به CloudFlare استفاده خواهیم کرد:
- برنامه اصلی Nuxt به کارگران CloudFlare.
- سرور WebSOCKETS با اشیاء بادوام.
در حالی که شما قوطی این آموزش را بدون Nuxflare دنبال کنید ، واقعاً دلیلی برای استفاده از آن وجود ندارد. حتی اگر راحت می نویسید wrangler.toml
پیکربندی خودتان ، Nuxflare ایجاد و از بین بردن منابع و استقرار در محیط های مختلف را آسان می کند. شما هنوز هم کنترل کامل را برای شخصی سازی پیکربندی Wrangler خود در هر صورت مورد نظر خود حفظ می کنید.
تنظیم nuxflare
بیایید Nuxflare را آغاز کنیم:
bun nuxflare init
Nuxflare چندین سؤال در مورد نام پروژه خود ، دامنه های سفارشی (برای استقرار Dev and Prod) و تنظیم اقدامات GitHub از شما می پرسد. همه چیز اختیاری است.
من استفاده می کنم websockets-demo.nuxflare.com
به عنوان دامنه تولید و رها کردن دامنه توسعه برای استفاده از پیش فرض کارگران CloudFlare به طور پیش فرض.
برای اقدامات GitHub ، من از پیش تعیین شده “Deployments Conseplements” استفاده می کنم ، این بدان معنی است که به روزرسانی های کد در main
شاخه در nuxflare/durable-websockets
مخزن به طور خودکار به websockets-demo.nuxflare.com
بشر
Nuxflare CLI همچنین شما را وادار به ایجاد و پیکربندی A می کند CLOUDFLARE_API_TOKEN
برای استقرار منابع به CloudFlare.
پس از اتمام ، Nuxflare وابستگی های توسعه مانند را نصب می کند sst
(sst.dev) و wrangler
، که برای مدیریت منابع CloudFlare لازم است. همچنین یک پوشه جدید به نام متوجه خواهید شد nuxflare
و پرونده ای به نام sst.config.ts
بشر
احساس غرق شدن؟ نگران آن نباشید در بیشتر موارد ، شما می توانید کاملاً نادیده بگیرید nuxflare
پوشه-جایی است که همه زیرساخت ها به عنوان “جادوگر” کدگذاری می شوند. فقط برای کنترل منبع همه چیز را مرتکب شوید.
پیکربندی زیرساخت ها
حال بیایید با به روزرسانی زیرساخت های برنامه ما را پیکربندی کنیم run()
در داخل کار کنید sst.config.ts
:
async run() {
const { Nuxt } = await import("./nuxflare/nuxt");
const { Worker } = await import("./nuxflare/worker");
const domain =
$app.stage === "production"
? prodDomain || undefined
: devDomain
? `${$app.stage}.${devDomain}`
: undefined;
// Create WebSockets worker
const { websocketsUrl } = await Worker({
name: "WebSockets",
dir: "./websockets",
main: "index.ts",
durableObjects: [{ className: "WebSockets", bindingName: "WEBSOCKETS" }],
});
// Create Nuxt app and pass the WebSockets URL
Nuxt("App", {
dir: ".",
domain,
outputDir: ".output",
extraVars: {
NUXT_PUBLIC_WEBSOCKETS_URL: websocketsUrl,
},
});
}
همانطور که گفته شد ، ما دو مؤلفه ایجاد می کنیم:
- سرور WebSOCKETS با اشیاء بادوام.
- برنامه Nuxt که به سرور WebSockets متصل می شود.
برای برنامه Nuxt ، ما مشخص می کنیم dir: "."
، و Nuxflare آن را با کارگران CloudFlare ساخته و مستقر خواهد کرد. برای سرور WebSockets ، ما یک را مشخص می کنیم websockets
دایرکتوری با index.ts
به عنوان ورودی کارگر.
درک اشیاء بادوام
بنابراین ، معامله با اشیاء بادوام چیست؟
یک شیء بادوام در اصل یک کلاس است. شما می توانید چندین نمونه از آن ایجاد کنید ، و هر نمونه وضعیت خود را مدیریت می کند و اتصالات WebSocket خود را کنترل می کند.
آنچه اشیاء بادوام را خاص می کند این است که آنها به طور خودکار در نزدیکی کاربر قرار می گیرند. به عنوان مثال ، در برنامه چت ما ، هنگامی که کاربر یک اتاق جدید ایجاد می کند ، یک نمونه شیء با دوام جدید را در یک سرور CloudFlare نزدیک به آن کاربر ایجاد می کند. وقتی دیگران به همان اتاق چت (از هر نقطه دنیا) می پیوندند ، به همان نمونه شیء بادوام متصل می شوند.
این قدرتمند است زیرا می توانید بدون نگرانی در مورد استقرار سرور ، روی نوشتن منطق زمان واقعی خود تمرکز کنید ، در حالی که کاربران هنوز بهترین تاخیر ممکن را دارند.
توجه: یک شی با دوام – حداقل در حال حاضر – یک بار که ایجاد شده است جابجا نمی شوند. اگر اولین کاربر که یک نمونه شیء بادوام ایجاد می کند از نظر جغرافیایی از کاربران بعدی فاصله دارد ، ممکن است این کاربران تاخیر زیر حد را تجربه کنند. حل این محدودیت با رویکردهای جایگزین نیز دشوار است.
بنابراین ، بله ، یک شیء بادوام یک کلاس است ، و ما به ما می گوییم "WebSockets"
بشر اما اسم الزام آور چیست؟
Cloudflare این چیز را دارد: هر آنچه از طریق یک کارگر استفاده می شود باید از طریق “الزام آور” عبور کند – خواه KV ، D1 ، صف ها یا اشیاء با دوام باشد. برای هر ویژگی ، به یک نام الزام آور نیاز دارید.
ما می گوییم: اگر کارگر نیاز به استفاده از آن دارد "WebSockets"
شیء بادوام ، باید این کار را از طریق اتصال به نام انجام دهد "WEBSOCKETS"
(env.WEBSOCKETS
).
ایجاد کلاس اشیاء بادوام ما
اکنون که اصول را درک می کنیم ، بیایید کلاس اشیاء بادوام خود را بنویسیم:
export class WebSockets extends DurableObject {}
شیء بادوام ما نیاز دارد:
- درخواست های اتصال WebSocket را کنترل کنید (وقتی یک کارگر یک نمونه شیء بادوام را مشخص می کند که باید یک اتصال WebSocket را اداره کند).
- پیام های ورودی وب سایت را پردازش کنید (وقتی کاربر در گپ پیام ارسال می کند).
- هنگامی که اتصال WebSocket بسته می شود (حالت تمیز کردن برای آن اتصال) را کنترل کنید.
- افشای
publish
برای پخش پیام ها به کاربران خاص که از طریق WebSockets متصل هستند ، کار می کند.
رسیدگی به درخواست های WebSocket
برای شماره 1 ، بیایید بنویسیم fetch
عملکرد. ما کامل را دریافت خواهیم کرد request
شیء از کارگر. از آنجا ، ما باید بررسی کنیم که آیا این یک درخواست WebSockets است یا خیر ، و آیا اینطور است ، ما یک جفت WebSockets ایجاد می کنیم. یک جفت WebSocket مانند Walkie-Talkies است: برای هر مشتری که اتصال WebSockets را با سرور باز می کند ، سرور یک گره را نگه می دارد و دیگری را به مشتری می دهد. سپس می توانیم از گره روی سرور برای ارسال و دریافت پیام استفاده کنیم.
export class WebSockets extends DurableObject {
override async fetch(request: Request): Promise<Response> {
if (request.headers.get("upgrade") === "websocket") {
try {
const { room, userId } = extractRoomAndUser(request);
const protocols =
request.headers
.get("sec-websocket-protocol")
?.split(",")
.map((x) => x.trim()) || [];
protocols.shift(); // remove the room:userId from protocols
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
if (server) {
server.serializeAttachment({
room,
userId,
});
this.ctx.acceptWebSocket(server, [room, userId]);
}
const res = new Response(null, { status: 101, webSocket: client });
if (protocols.length > 0) {
res.headers.set("sec-websocket-protocol", protocols[0] as string);
}
return res;
} catch (err) {
console.error("Error in websocket fetch:", err);
return new Response(null, { status: 400 });
}
}
return new Response(null);
}
}
هنوز چند مورد دیگر برای باز کردن وجود دارد. بیایید یکی یکی برویم.
احراز هویت با عنوان پروتکل WebSocket
بیایید در مورد هک Header Protocols WebSocket که برای تأیید اعتبار استفاده می کنیم صحبت کنیم.
WebSockets به طور طبیعی از داده های تأیید اعتبار در دست اول مانند هدرهای HTTP پشتیبانی نمی کند. با این حال ، یک راه حل وجود دارد: هدر پروتکل WebSocket می تواند پروتکل های سفارشی را به عنوان مقادیر جدا از کاما شامل شود.
ما در حال رمزگذاری اتاق و شناسه کاربری خود در Base64 هستیم و آن را به عنوان اولین پروتکل منتقل می کنیم. سرور این اطلاعات را برای تعیین اینکه کاربر می خواهد به آن بپیوندد و آنها را در گپ شناسایی کند ، استخراج می کند.
// NOTE: in a real-world scenario, the token should instead be JWT or similar
// from which we could extract and validate room/user/topic and such
// or, the info can even be stored inside a KV
function extractRoomAndUser(request: Request): {
room: string;
userId: string;
} {
const protocolHeader = request.headers.get("sec-websocket-protocol");
if (!protocolHeader) {
throw new Error("Missing sec-websocket-protocol header");
}
const [encoded] = protocolHeader.split(",").map((x) => x.trim());
if (!encoded) {
throw new Error("Invalid sec-websocket-protocol format");
}
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
const [room, userId] = decoded.split(":");
if (!room || !userId) {
throw new Error("Room and User ID must be separated by a colon");
}
return { room, userId };
}
برای اطلاعات بیشتر در مورد تأیید هویت WebSocket این پست Overflow Stack را بخوانید: https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api
برای اهداف این آموزش ، ما هیچ “اعتبارسنجی” روی سرور انجام نمی دهیم تا اطمینان حاصل کنیم که کاربر کسی است که آنها می گویند. این در تولید کار نمی کند که احتمالاً باید از یک نشانه JWT یا مشابه استفاده کنیم که می تواند در سرور تأیید شود.
WebSocket Hibernation و Serialize/Deserialize پیوست
با اشیاء با دوام ، شما برای زمان فعال بودن هزینه می کنید.
این می تواند با وب سایت ها گران شود زیرا اگر کاربران به یک اتاق گپ وصل شوند اما به طور فعال گپ نمی زنند ، شیء بادوام بیکار است و در حالی که وضعیت اتصال WebSocket را حفظ می کند ، هزینه شما را هزینه می کند.
خوشبختانه ، CloudFlare WebSocket را خواب زمستانی ارائه می دهد. این بدان معناست که اگر یک شیء بادوام دارای اتصالات فعال WebSocket باشد اما پیام های پردازش را ندارد ، می تواند “خواب زمستانی” باشد و شما برای زمان غیرفعال پرداخت نمی کنید. اتصال WebSocket با مشتری باز است و در صورت فعالیت ، شیء بادوام برای رسیدگی به آن “احیا شده” است.
اشیاء بادوام به شما امکان می دهد مقدار کمی از حالت (2KB) را برای هر وب سایت که هنگام بازگشت آنلاین بازیابی می شود ، ذخیره کنید. این باید حاوی حداقل اطلاعات برای شناسایی مشتری باشد. برای ذخیره اطلاعات بیشتر ، باید از API ذخیره سازی اشیاء با دوام استفاده کنید ، که به شما امکان می دهد از یک فروشگاه KV تمام عیار برای پایداری استفاده کنید.
رسیدگی به رویدادهای WebSocket
برای دست زدن به پیام های WebSocket:
- ما ابتدا مشتری را بر اساس شیئی که به هر مشتری وصل می کنیم ، شناسایی می کنیم.
- ما از دو نوع پیام های دریافتی پشتیبانی می کنیم: یکی که کاربر نام آنها را اعلام می کند (یا تغییر می دهد) و دیگری که کاربر می تواند پیام های گپ را ارسال کند.
export class WebSockets extends DurableObject {
// ...
override async webSocketMessage(
ws: WebSocket,
message: ArrayBuffer | string,
) {
const { room, userId } = ws.deserializeAttachment();
// Validate message type and size
// [...]
try {
const parsed = JSON.parse(message) as WebSocketMessage;
if (parsed.type === "chat") {
if (
typeof parsed.text !== "string" ||
parsed.text.trim().length === 0
) {
throw new Error("Invalid chat message");
}
const userName =
(await this.ctx.storage.get<string>(`name:${userId}`)) || userId;
this.publish(room, {
type: "chat",
userId,
userName,
text: parsed.text,
time: new Date().toISOString(),
});
} else if (parsed.type === "name") {
if (
typeof parsed.name !== "string" ||
parsed.name.trim().length === 0
) {
throw new Error("Invalid name");
}
await this.ctx.storage.put(`name:${userId}`, parsed.name.trim());
this.publish(room, {
type: "name",
userId,
name: parsed.name.trim(),
time: new Date().toISOString(),
});
} else {
throw new Error("Unknown message type");
}
} catch (err) {
console.error("Message processing error:", err);
ws.close(1003, "Invalid message format");
}
}
override async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
_wasClean: boolean,
) {
const { userId } = ws.deserializeAttachment();
await this.ctx.storage.delete(`name:${userId}`);
ws.close(code, reason);
}
}
من از API ذخیره سازی اشیاء بادوام برای ذخیره نام کاربری با استفاده از API استفاده می کنم await this.ctx.storage.put
وت await this.ctx.storage.get
بشر
وقتی اتصال WebSocket بسته شد ، KV را برای آن کاربر پاک می کنیم.
تابع انتشار
برای ارسال پیام به کاربران متصل به اتاق ، ما از آن استفاده می کنیم publish
عملکرد.
ما از طریق تمام وب سایت های متصل به نمونه شیء بادوام خود حلقه می کنیم و بار پیام را برای آنها ارسال می کنیم.
export class WebSockets extends DurableObject {
async publish(room: string, data: any) {
try {
const websockets = this.ctx.getWebSockets();
if (websockets.length < 1) {
return;
}
for (const ws of websockets) {
const state = ws.deserializeAttachment() || {};
if (state.room === room) {
ws.send(JSON.stringify(data));
}
}
return null;
} catch (err) {
console.error("publish err", err);
}
}
}
توجه: بررسی اتاق در اینجا زائد است زیرا ما به هر حال برای هر اتاق یک نمونه شیء بادوام ایجاد می کنیم. اما افزونگی عمدی است در صورتی که بعداً منطق ایجاد نمونه های شیء با دوام را تغییر دهیم.
نوشتن کارگر ما
اکنون که اشیاء بادوام ما آماده مدیریت و مدیریت اتصالات WebSocket هستند ، بیایید کد کارگران CloudFlare را که از این اشیاء بادوام استفاده می کند ، بنویسیم:
export default class Worker extends WorkerEntrypoint {
override async fetch(request: Request) {
const binding = (this.env as any)
.WEBSOCKETS as DurableObjectNamespace<WebSockets>;
try {
const { room } = extractRoomAndUser(request);
const stub = binding.get(binding.idFromName(room)); // infer durable object instance from room name
return stub.fetch(request);
} catch (err) {
console.error("Error in worker fetch:", err);
return new Response(null, { status: 400 });
}
}
async publish(room: string, data: any) {
const binding = (this.env as any)
.WEBSOCKETS as DurableObjectNamespace<WebSockets>;
const stub = binding.get(binding.idFromName(room)); // infer durable object instance from room name
await stub.publish(room, data);
return new Response(null);
}
}
ما استفاده می کنیم WEBSOCKETS
به عنوان نام الزام آور برای یافتن کلاس اشیاء بادوام.
اشیاء با دوام ندارند create
روش ایجاد یک نمونه. بنابراین ما فقط استفاده می کنیم get
بشر اگر برای اولین بار به آن دسترسی داشته باشد ، یک شیء بادوام در حال حرکت است.
در idFromName
تابع یک رشته برای تولید یک شناسه برای مثال می گیرد به گونه ای که شناسه مشابه را برای همان رشته (و شناسه های مختلف برای رشته های مختلف) تولید می کند. همانطور که می خواستیم ، از نام اتاق برای شناسایی منحصر به فرد اشیاء بادوام استفاده می کنیم.
ما همچنین در معرض publish
روش برای کارگر که چیزها را به آن منتقل می کند this.publish
روش در شیء بادوام. این امر مفید است زیرا ما می توانیم این روش را از برخی دیگر از کارگر با یک سرویس دهنده تماس بگیریم و این به ما راهی برای ارسال پیام های “سرور” بداهه برای کاربران می دهد.
نوشتن جبهه با useWebSocket
در مخزن ، ما یک UI چت اساسی با استفاده از Nuxt UI V3 و Tailwind داریم. بخش مهم در Messages.vue
مؤلفه ای که به سرور WebSockets وصل می شویم:
const protocol = btoa(`${chatRoom}:${currentUser.id}`);
const { send } = useWebSocket(useRuntimeConfig().public.websocketsUrl, {
protocols: [protocol.replaceAll("=", ""), "chat"],
onConnected: () => {
send(
JSON.stringify({
type: "name",
name: currentUser.name,
}),
);
},
onMessage: (_ws, event) => {
const data = JSON.parse(event.data);
if (data.type === "chat") {
messages.value.push({
sender: {
id: data.userId,
name: data.userName,
},
content: data.text,
timestamp: data.time,
});
}
},
});
ما استفاده می کنیم useWebSocket
از @vueuse/core
زیرا به طور خودکار ارتباطات و ضربان قلب را برای ما انجام می دهد.
برای websocketsUrl
، ما خود را داریم runtimeConfig
:
export default defineNuxtConfig({
// [...]
runtimeConfig: {
public: {
websocketsUrl: "ws://localhost:8787", // default used for dev
},
},
});
ما یک مقدار پیش فرض برای توسعه را مشخص می کنیم ، اما ما هنگام استقرار به کارگران CloudFlare در sst.config.ts
:
// [...]
Nuxt("App", {
dir: ".",
domain,
outputDir: ".output",
extraVars: {
NUXT_PUBLIC_WEBSOCKETS_URL: websocketsUrl,
},
});
من در حال حاضر در حال کار بر روی یک به روزرسانی هستم تا توسعه را برای Nuxflare آسانتر کند. این کار با استفاده از اشیاء بادوام و اتصالات سرویس در حالت DEV ساده می شود. چالش این است که شما به یک سرور Dev Server جداگانه برای این ویژگی ها نیاز دارید. حالت dev برای D1 ، KV ، R2 و غیره.
@nuxt-hub/core
وتnitro-cloudflare-dev
بشر
استفاده از برنامه ما
برای استقرار ، به سادگی اجرا کنید:
bun nuxflare deploy --stage hello # Use whatever stage you like
همچنین می توانید یک مرحله را به طور کامل حذف کنید:
bun nuxflare remove --stage hello
و همین است! اکنون یک برنامه چت در زمان واقعی دارید که روی کارگران CloudFlare با اشیاء بادوام و Nuxt اجرا می شود. خیلی باحال ، درست است؟
این تنها سطح کاری را که می توانید با اشیاء بادوام انجام دهید ، خراشیده می کند. در پست های آینده ، من عمیق تر به چیزهای پیشرفته تر شیرجه می زنم.
اگر سؤالی دارید یا اگر چیز خاصی در مورد اشیاء بادوام وجود دارد ، دوست دارید من را بپوشانم (x ، discord)
قبل از رفتن …
سلام ، اگر این را تا کنون خوانده اید ، حدس می زنم که این را مفید دانستید.
من کار کرده ام Nuxflare Pro – این یک کیت کامل Nuxt + Cloudflare استارت است که باعث صرفه جویی در زمان تنظیم می شود.
- این یک است خرید یک بار با دسترسی به طول عمر به همه محصولات Nuxflare در آینده
- شما مستقیماً از کار من پشتیبانی می کنید تا در منبع باز و ایجاد محتوا (مانند این پست) بسازید (مانند این پست)
- شما به هر آنچه که برای اکوسیستم Nuxt و CloudFlare ایجاد می کنم ، زودتر دسترسی خواهید یافت
صحبت از آن … من در حال حاضر در حال ساخت Nuxflare Landing هستم – یک سازنده صفحه فرود بهینه سازی شده AI که یکپارچه با محتوای Nuxt UI و Nuxt کار می کند. به عنوان یک عضو حرفه ای ، هنگام راه اندازی آن ، آن را کاملاً رایگان دریافت خواهید کرد.
Nuxflare Pro را بررسی کنید
اصلاً هیچ فشاری وجود ندارد – و از خواندن به هر صورت متشکرم.