- كل المقالات
- بناء التطبيقات على Cloudflare
- دليل Cloudflare Workers: من أول طلب إلى تطبيق يعمل
دليل Cloudflare Workers: من أول طلب إلى تطبيق يعمل
دليل عملي لفهم Cloudflare Workers وبناء واجهة برمجية باستخدام Hono: من تقدير مدة قراءة النصوص إلى النشر وربط الخدمات ومتابعة الأداء والتكلفة.
في دليل DNS، تعرّفنا على إعداد النطاق وربطه بالتطبيق. نكمل هنا من جهة التطبيق نفسه: أين تعمل الشيفرة التي تستقبل الطلب، وكيف نكتبها وننشرها؟
Cloudflare Workers بيئة لتشغيل شيفرة الخادم. يستقبل تطبيقك، الذي نسميه Worker، الطلب وينفذ الشيفرة ثم يعيد الاستجابة. يمكنك بناء واجهة برمجية (API) صغيرة عليه، أو معالجة بيانات نموذج، أو تشغيل الجزء الخاص بالخادم من موقعك. وهذا الموقع نفسه يعمل على Workers.
سنبني باستخدام Hono واجهة تقدّر مدة قراءة النصوص العربية والإنجليزية. ترسل إليها نصا، فتعيد عدد كلماته والوقت المتوقع لقراءته. يمكنك إضافة هذه الأداة إلى محرر مقالات؛ وكل ما تحتاجه هنا هو الشيفرة نفسها، من دون قاعدة بيانات أو خدمة خارجية.
للمتابعة، تحتاج إلى أساسيات JavaScript، وطرفية، وإصدار حديث طويل الدعم (LTS) من Node.js مع npm. سنشرح ما نستخدمه من أنواع TypeScript أثناء العمل. يمكنك تجربة المثال محليا دون حساب Cloudflare؛ ستحتاج إليه عند النشر. أما شراء نطاق فاختياري.
- فهم بيئة التشغيل.
- بناء الواجهة واختبارها.
- النشر وربط النطاق.
- الإعدادات وبيانات الاعتماد والتخزين.
- حدود الاستخدام والتكلفة.
- تتبع الطلبات وتشخيص الأخطاء.
كيف يعمل Worker؟
تتولى Cloudflare إدارة الأجهزة التي تشغّل التطبيق. تنشر أنت الشيفرة وتحدد الموارد التي يمكنها الوصول إليها، ويتلقى الزائر الاستجابة التي تنتجها. تظل شيفرة Worker على الخادم.
تعمل شيفرة JavaScript داخل بيئات V8 معزولة (isolates). قد تعالج البيئة الواحدة عدة طلبات، ثم تستبدلها المنصة ببيئة جديدة. لذلك لا يمكنك الاعتماد على استمرارها أو بقاء ما وضعته في ذاكرتها بين طلب وآخر.
لنفترض أنك أضفت عدادا للزيارات في متغير عام. يبدو أنه يعمل على جهازك: تحدّث الصفحة فيزداد العدد. لكن في بيئة الإنتاج، قد يصل الطلب إلى بيئة معزولة أخرى لها عدادها الخاص، أو إلى بيئة جديدة يبدأ عدادها من الصفر. يشرح دليل بيئة التشغيل هذا النموذج بالتفصيل.
بيئة التشغيل
الشيفرة واحدة، والذاكرة منفصلة
لنفترض أن لدينا عدادا للزيارات في متغير عام، يبدأ من الصفر.
- البيئة المعزولة A2وصل إليها الطلبان الأول والثاني، فارتفع العداد إلى 2.
- البيئة المعزولة B1وصل الطلب الثالث إلى بيئة أخرى، يبدأ عدادها من الصفر.
- بيئة معزولة جديدة1وصل الطلب الرابع بعد استبدال البيئة A. بدأ العداد الجديد من الصفر أيضا.
لهذا، احتفظ ببيانات كل طلب داخل دالة معالجته. يمكن وضع الثوابت خارج الدالة، أما البيانات التي تريد الاحتفاظ بها بين الطلبات فتحتاج إلى تخزين دائم.
توفر Workers واجهات ويب مألوفة مثل Request وResponse وURL وfetch()، لكن من دون صفحة ويب أو شجرة DOM. يستقبل التطبيق طلب HTTP عبر دالة fetch التي يصدّرها، ويعيد كائن Response بوصفه استجابة الطلب:
export default {
async fetch(request, env, ctx): Promise<Response> {
return new Response("The Worker answered.");
},
} satisfies ExportedHandler<CloudflareBindings>;
تستقبل دالة المعالجة ثلاثة كائنات:
request: عنوان الطلب وطريقته وترويساته ومحتواه، كما أرسلها العميل.env: المتغيرات والقيم السرية والموارد التي ربطتها بالتطبيق في الإعدادات، مثل قواعد البيانات.ctx: أدوات لإدارة المهام المرتبطة بالطلب الحالي، ومنها إتاحة وقت قصير لإكمال بعض الأعمال بعد إرسال الاستجابة.
يتيح ExportedHandler<CloudflareBindings> لـ TypeScript التحقق من توقيع الدالة. سنولّد تعريف CloudflareBindings من ملف الإعدادات بعد قليل. هذه الأنواع تساعدنا أثناء التطوير، ثم تُحذف عند البناء؛ ما تشغّله Cloudflare هو JavaScript.
رحلة طلب HTTP
من وصول الطلب إلى إرسال الاستجابة
نتتبع طلبا ناجحا إلى واجهة تقدير مدة القراءة التي سنبنيها.
- العميلPOST /api/reading-timeيرسل العميل النص واللغة بصيغة JSON، ويمكنه تحديد سرعة القراءة أيضا.
- Cloudflareاختيار التطبيقتحدد إعدادات اسم المضيف تطبيق Worker الذي يستقبل الطلب.
- دالة معالجة الطلبالتحقق ← العديختار Hono دالة المعالجة بحسب المسار، فتتحقق من JSON وتعدّ الكلمات.
- العودة إلى العميل200 · application/jsonيتلقى العميل الحالة والترويسات ونتيجة الحساب بصيغة JSON. إذا كانت المدخلات غير صالحة، نعيد 400 عند التحقق منها.
انتبه إلى الفرق بين استخدامين للاسم fetch: الدالة في المثال تستقبل الطلب الوارد، أما استدعاء fetch() داخلها فيرسل طلبا إلى خدمة أخرى. لن نحتاج إلى اتصال خارجي في واجهة تقدير القراءة؛ سنحسب النتيجة مباشرة من النص المرسل.
بهذا تكتمل صلة Workers بما شرحناه عن DNS: يساعد DNS العميل على الوصول إلى الخدمة، ويحدد توجيه HTTP تطبيق Worker المقصود، ثم تختار شيفرتك ما يحدث عند طلب /api/reading-time. إعداد DNS وحده لا ينشئ هذا المسار داخل التطبيق.
هل يمكن استخدام حزم npm؟
نعم، بشرط أن تعمل الحزمة والحزم التي تعتمد عليها ضمن بيئة Workers. تدعم طبقة التوافق مع Node.js كثيرا من وحداته المدمجة، لكن الدعم جزئي لبعض الواجهات، وبعضها يمكن استيراده دون أن تتوفر وظائفه. لذلك اختبر الحزمة في بيئة Workers؛ نجاح تثبيتها وحده لا يكفي.
يُفعّل التوافق مع Node.js افتراضيا عندما يكون تاريخ التوافق 4 آب 2026 أو أحدث، وهو ما ينطبق على مثالنا. قد ترى في أمثلة أقدم الخيار nodejs_compat مضافا صراحة. يشرح دليل التوافق هذا الاختلاف. وتظل تجربة الوظائف التي تحتاجها ضرورية، خاصة إذا كانت الحزمة تعتمد على إمكانات نظام التشغيل.
إنشاء المشروع
رأينا كيف تستقبل دالة واحدة الطلب وتعيد الاستجابة. سنستخدم الآن Hono لتنظيم التطبيق: نعرّف مسارا لكل وظيفة، ونضيف دوال وسيطة (middleware) لفحص الطلبات. تتولى Workers تشغيل هذه الشيفرة ونشرها وإتاحة الموارد المرتبطة بها.
افتح الطرفية في مجلد مشاريعك، ثم نفّذ:
npm create cloudflare@latest -- reading-api
اختر Hello World، ثم Worker only وTypeScript، واختر عدم النشر في هذه الخطوة. تتكفل أداة إنشاء المشروع بتثبيت Wrangler، الأداة التي سنستخدمها للتشغيل المحلي والنشر. ادخل إلى مجلد المشروع وأضف Hono:
cd reading-api
npm install hono
سنفصل العمل بين ملفين: src/analyze.ts لعدّ الكلمات وحساب المدة، وsrc/index.ts لاستقبال طلبات HTTP والرد عليها. لنبدأ بالإعدادات؛ استبدل محتوى wrangler.jsonc بما يلي:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "reading-api",
"main": "src/index.ts",
"compatibility_date": "2026-09-21",
"workers_dev": true,
"vars": {
"SERVICE_NAME": "reading-api",
"DEFAULT_WPM": 200
},
"observability": {
"enabled": true,
"head_sampling_rate": 1
}
}
يحدد name اسم التطبيق، وmain الملف الذي يبدأ منه التنفيذ. يتيح workers_dev الوصول إلى التطبيق عبر عنوان عام بعد النشر. وتفعّل observability حفظ السجلات؛ اخترنا هنا تسجيل جميع الطلبات بوضع head_sampling_rate عند 1. سنعود إلى السجلات لاحقا.
أما تاريخ التوافق فيحدد سلوك بيئة التشغيل. ابدأ بتاريخ اليوم، واختبر التطبيق إذا غيّرت هذا التاريخ لاحقا. إذا كان إصدار Wrangler لا يدعمه، فحدّث Wrangler داخل المشروع. تاريخ التوافق مستقل عن إصدارات حزم npm؛ تلك يحفظها ملف القفل.
حددنا سرعة القراءة الافتراضية في DEFAULT_WPM. اخترنا 200 كلمة في الدقيقة للتجربة، لا بوصفها متوسطا مقاسا للقراءة بالعربية أو الإنجليزية. يستطيع العميل إرسال سرعة مختلفة بين 80 و600 كلمة في الدقيقة. وضعنا هذا النطاق في تطبيقنا؛ وهو ليس من حدود Cloudflare.
ولّد الآن تعريفات TypeScript من الإعدادات:
npx wrangler types --env-interface CloudflareBindings
أعد تشغيل الأمر كلما غيّرت الإعدادات. يستخدم Hono التعريف الناتج للتحقق من أنواع القيم التي نقرأها عبر c.env، ومنها DEFAULT_WPM الرقمية.
جرب أداة تقدير مدة القراءة
قبل كتابة الشيفرة، جرّب النتيجة التي نريد الوصول إليها. ألصق فقرة واختر لغتها، ثم غيّر سرعة القراءة لترى أثرها في المدة. افتح تفاصيل الطلب والاستجابة إذا أردت رؤية شكل JSON الذي ستتعامل معه الواجهة.
جرب نصا من اختيارك
كم تستغرق قراءة هذا النص؟
جرّب نصا عربيا أو إنجليزيا، وغيّر سرعة القراءة لترى أثرها في المدة.
يبقى النص في متصفحك ولا يُرسل إلى خادم. تعتمد المدة على السرعة التي تختارها، وقد يختلف عدّ الكلمات قليلا بين بيئات التشغيل.
عرض تفاصيل الطلب والاستجابة
POST /api/reading-timeمحتوى الطلب
محتوى الاستجابة المتوقع
كيف يفحص الخادم الطلب؟
- POST /api/reading-time؟لا · أعد الاستجابة404 / 405المسار المجهول يعيد 404. وأي طريقة أخرى على مسار التحليل تعيد 405 مع Allow: POST.نعم · تابع
- هل جسم الطلب ضمن حد 64 كيبيبايت؟لا · أعد الاستجابة413ترفض الدالة الوسيطة الطلب إذا تجاوز حجمه الحد، قبل تحليل JSON.نعم · تابع
- هل نوع المحتوى وصيغة JSON والحقول صحيحة؟لا · أعد الاستجابة415 / 400نعيد 415 إذا كان نوع المحتوى غير مقبول، و400 إذا كان الخلل في JSON أو النص أو اللغة أو السرعة.نعم · تابع
- عدّ الكلمات وحساب المدةالاستجابة200تتضمن النتيجة اللغة وعدد الكلمات وسرعة القراءة والمدة بالثواني والدقائق.
يحدد اختيار اللغة قواعد تقسيم النص إلى كلمات؛ ولا يترجم النص أو يتحقق من اللغة المكتوب بها. ويمكنك تجربة نص يجمع العربية والإنجليزية. أما المدة فهي تقديرية، لأن صعوبة النص وإلمام القارئ بموضوعه يؤثران في سرعة قراءته.
عدّ الكلمات وتقدير مدة القراءة
نبدأ بدالة مستقلة عن HTTP: تتحقق من النص واللغة والسرعة، ثم تعدّ الكلمات وتحسب المدة. هذا هو الجزء الذي شغّلته الأداة السابقة داخل متصفحك. أنشئ الملف src/analyze.ts:
export function analyzeReading(input: unknown, defaultWpm = 200) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
return { ok: false as const, error: "Send a JSON object." };
}
const data = input as Record<string, unknown>;
if (typeof data.text !== "string" || !data.text.trim()) {
return { ok: false as const, error: "Add some text to analyze." };
}
if (data.text.length > 10_000) {
return {
ok: false as const,
error: "Keep text within 10,000 UTF-16 code units.",
};
}
if (data.language !== "en" && data.language !== "ar") {
return { ok: false as const, error: "Choose en or ar." };
}
const wordsPerMinute =
data.wordsPerMinute === undefined
? defaultWpm
: data.wordsPerMinute;
if (
typeof wordsPerMinute !== "number" ||
!Number.isInteger(wordsPerMinute) ||
wordsPerMinute < 80 ||
wordsPerMinute > 600
) {
return {
ok: false as const,
error: "Use a whole-number reading speed from 80 to 600.",
};
}
const segmenter = new Intl.Segmenter(data.language, {
granularity: "word",
});
let words = 0;
for (const segment of segmenter.segment(data.text)) {
if (segment.isWordLike) {
words++;
}
}
if (words === 0) {
return {
ok: false as const,
error: "Add text containing at least one word.",
};
}
return {
ok: true as const,
value: {
language: data.language,
words,
wordsPerMinute,
seconds: Math.ceil((words / wordsPerMinute) * 60),
minutes: Math.ceil(words / wordsPerMinute),
},
};
}
نستخدم Intl.Segmenter لتقسيم النص بحسب قواعد اللغة المختارة، ونعدّ المقاطع التي تحمل isWordLike. بذلك لا تدخل المسافات وعلامات الترقيم وحدها في العدد. لا يجري هذا تحليلا صرفيا للعربية؛ فالحروف المتصلة بالكلمة ليست بالضرورة كلمات مستقلة في الحساب. وقد تختلف بعض النتائج قليلا بين بيئات التشغيل، بحسب إصدار بيانات اللغة فيها.
تقبل الدالة قيمة من النوع unknown لأن أنواع TypeScript لا تفحص JSON عند وصوله. نتحقق من البيانات بأنفسنا قبل الحساب. كذلك، يقيس text.length طول النص بوحدات UTF-16: الحد هنا 10,000 وحدة، لا 10,000 بايت أو حرف ظاهر. فالرمز التعبيري الواحد قد يشغل أكثر من وحدة.
نقرّب قيمتي seconds وminutes إلى العدد الصحيح الأعلى، كل قيمة على حدة. مثلا، يعطي حساب خمس كلمات بسرعة 200 كلمة في الدقيقة مدة قدرها 1.5 ثانية. نقرّبها إلى ثانيتين، أو إلى دقيقة واحدة إذا اخترنا عرض المدة بالدقائق. لهذا تعرض الأداة السابقة الثواني، حتى تكون النتيجة مفيدة للنصوص القصيرة.
إضافة المسارات باستخدام Hono
لدينا الآن دالة الحساب. سنستخدمها في تطبيق له مساران: أحدهما للتأكد من أن التطبيق يستجيب، والآخر لاستقبال النص وإعادة النتيجة.
يوفر Hono لكل طلب كائن سياق نسميه c. نقرأ الطلب عبر c.req، ونصل إلى إعدادات Workers ومواردها عبر c.env، ونعيد JSON باستخدام c.json(). أما Variables فيصف القيم التي نحفظها للطلب الحالي عبر c.set()، مثل معرّف الطلب، ولا يعرّف إعدادات البيئة.
استبدل محتوى src/index.ts بما يلي:
import { Hono } from "hono";
import { bodyLimit } from "hono/body-limit";
import { analyzeReading } from "./analyze";
const app = new Hono<{
Bindings: CloudflareBindings;
Variables: {
requestId: string;
};
}>();
app.use("*", async (c, next) => {
const requestId = crypto.randomUUID();
c.set("requestId", requestId);
c.header("X-Request-Id", requestId);
c.header("Cache-Control", "no-store");
await next();
console.log({
event: "reading_api_response",
requestId,
status: c.res.status,
});
});
app.get("/health", (c) => {
return c.json({ status: "ok", service: c.env.SERVICE_NAME });
});
app.post(
"/api/reading-time",
bodyLimit({
maxSize: 64 * 1024,
onError: (c) =>
c.json({ error: "Keep the request body within 64 KiB." }, 413),
}),
async (c) => {
const mediaType = c.req
.header("Content-Type")
?.split(";")[0]
?.trim()
.toLowerCase();
if (mediaType !== "application/json") {
return c.json(
{ error: "Use Content-Type: application/json." },
415,
);
}
let input: unknown;
try {
input = await c.req.json();
} catch (error) {
if (error instanceof SyntaxError) {
return c.json({ error: "Send valid JSON." }, 400);
}
throw error;
}
const defaultWpm = c.env.DEFAULT_WPM;
if (
!Number.isInteger(defaultWpm) ||
defaultWpm < 80 ||
defaultWpm > 600
) {
throw new Error("Invalid DEFAULT_WPM configuration.");
}
const result = analyzeReading(input, defaultWpm);
if (!result.ok) {
return c.json({ error: result.error }, 400);
}
return c.json(result.value);
},
);
app.all("/api/reading-time", (c) => {
c.header("Allow", "POST");
return c.json({ error: "Use POST." }, 405);
});
app.notFound((c) => c.json({ error: "Not found." }, 404));
app.onError((error, c) => {
console.error({
event: "reading_api_error",
requestId: c.get("requestId"),
name: error.name,
});
return c.json({ error: "Internal server error." }, 500);
});
export default app;
تتكون الشيفرة من ثلاثة أجزاء:
- إعداد الطلب وتسجيل نتيجته: تنشئ الدالة الوسيطة معرّفا جديدا للطلب وتضبط
no-store، ثم تسجل حالة الاستجابة بعدawait next(). يبقى النص المرسل خارج السجل. - معالجة المسارات: يتيح
GET /healthالتأكد من أن التطبيق يستجيب، ويستقبلPOST /api/reading-timeالنص لتحليله. - معالجة الأخطاء: نعيد استجابة واضحة للمسارات المجهولة وطرق HTTP غير المدعومة والاستثناءات غير المتوقعة.
تمنع الدالة الوسيطة bodyLimit تجاوز حجم جسم الطلب 64 كيبيبايت، أي 65,536 بايت، قبل تحليل JSON. يشمل الحجم النص وصيغة JSON المحيطة به. تعتمد الدالة على ترويسة Content-Length عند وجودها، وتفحص تدفق البيانات عند غيابها. إذا تجاوز الطلب الحد، تعيد 413 قبل الوصول إلى بقية الفحوص. وهذا حد مستقل عن طول النص الذي فحصناه في دالة الحساب.
بعد فحص الحجم، نشترط نوع المحتوى application/json وإلا نعيد 415. وإذا كانت صيغة JSON أو قيم الحقول غير صالحة، نعيد 400. ولأي طريقة HTTP أخرى على مسار التحليل، تعيد app.all() الحالة 405 مع Allow: POST. المسارات المجهولة تعيد 404. ويستجيب Hono لطلب HEAD على /health دون محتوى، بينما يظل مسار التحليل مخصصا لـ POST.
إذا وقع استثناء غير متوقع داخل التطبيق، تعيد app.onError() الحالة 500 برسالة عامة، وتسجل اسم الخطأ ومعرّف الطلب دون النص المرسل. لكن بعض أخطاء المنصة، مثل استنفاد زمن المعالجة، قد تقع خارج نطاق هذه الدالة. سنناقش حدود التشغيل بعد النشر.
اختبار الواجهة محليا
شغّل التطبيق على جهازك:
npx wrangler dev --local
يشغّل Wrangler الشيفرة على جهازك باستخدام workerd، بيئة تشغيل Workers. يمنع --local استخدام الموارد البعيدة عبر إعدادات الربط، لكنه لا يمنع الشيفرة من إرسال طلبات fetch() إلى خدمات حقيقية. مثالنا لا يرسل مثل هذه الطلبات. يوضح دليل التطوير المحلي هذا الفرق.
افتح نافذة طرفية أخرى وأرسل نصا:
curl -i 'http://localhost:8787/api/reading-time' \
-H 'Content-Type: application/json' \
--data '{"text":"Small APIs can be useful.","language":"en"}'
ينبغي أن تحصل على الحالة 200، وترويستي Cache-Control: no-store وX-Request-Id، والنتيجة التالية:
{
"language": "en",
"words": 5,
"wordsPerMinute": 200,
"seconds": 2,
"minutes": 1
}
لتجربة العربية، أرسل نصا عربيا وغيّر language إلى ar. ويمكنك إضافة "wordsPerMinute": 150 لتغيير سرعة القراءة. بعد نجاح الطلب، جرّب الحالات التي ينبغي أن ترفضها الواجهة:
# Health check: 200
curl -i 'http://localhost:8787/health'
# Empty text: 400
curl -i 'http://localhost:8787/api/reading-time' \
-H 'Content-Type: application/json' \
--data '{"text":"","language":"en"}'
# Invalid JSON: 400
curl -i 'http://localhost:8787/api/reading-time' \
-H 'Content-Type: application/json' \
--data '{'
# Wrong content type: 415
curl -i 'http://localhost:8787/api/reading-time' \
--data 'hello'
# Wrong method: 405, with Allow: POST
curl -i 'http://localhost:8787/api/reading-time'
# Unknown route: 404
curl -i 'http://localhost:8787/missing'
إذا أردت تجربة مقال كامل، فاحفظ محتوى الطلب بصيغة JSON في request.json، واستخدم --data-binary @request.json مع ترويسة نوع المحتوى نفسها. بهذا ترسل نص UTF-8 كما هو، وتتجنب مشكلات علامات الاقتباس في الطرفية. اختبر أيضا لغة غير مدعومة، ونصا لا يحتوي إلا على علامات ترقيم، وسرعة بقيمة كسرية أو خارج النطاق، وجسم طلب يتجاوز 64 كيبيبايت.
المثال متاح دون مصادقة. تطلب no-store عدم تخزين الاستجابة مؤقتا، لكنها لا تحدد من يحق له استدعاء الواجهة. وإذا استدعيتها من واجهة أمامية على أصل مختلف، فستحتاج إلى إعداد CORS للسماح لها بقراءة الاستجابة. تنظم CORS هذا الوصول داخل المتصفح، ولا تتحقق من هوية المتصل.
نشر التطبيق
من التطوير إلى النشر
متى يصبح التطبيق متاحا للناس؟
تعمل في الخطوات الثلاث الأولى على جهازك. عند الخطوة الرابعة يصبح التطبيق متاحا عبر عنوان عام.
- 01 · كتابة الشيفرةsrc/index.ts + wrangler.jsoncيحتوي المشروع على دالة معالجة الطلبات وإعدادات تشغيلها.
- 02 · التشغيل المحليwrangler dev --localجرّب إرسال طلبات HTTP إلى التطبيق على جهازك.
- 03 · فحص حزمة النشرwrangler deploy --dry-runتأكد من إمكانية بناء الحزمة دون نشر التطبيق.
- 04 · النشرwrangler deployانشر التطبيق، ثم جرّب عنوانه الذي يظهر في الطرفية.
بعد تجربة الطلبات، تحقق من أنواع TypeScript وابن حزمة التطبيق للتأكد من جاهزيتها للنشر:
npx tsc --noEmit
npx wrangler deploy --dry-run
يبني الأمر deploy --dry-run الحزمة دون نشرها. لكنه لا يتحقق من صلاحيات بيئة الإنتاج أو إعدادات النطاقات أو الخدمات البعيدة التي قد تضيفها لاحقا.
لنشر المثال، سجّل الدخول ثم نفّذ أمر النشر:
npx wrangler login
npx wrangler deploy
يعرض Wrangler عنوان التطبيق بعد النشر، وغالبا يكون بالشكل https://reading-api.<your-subdomain>.workers.dev. استخدم العنوان الذي ظهر لك في الطرفية: اختبر /health أولا، ثم أعد إرسال أمثلة POST إلى /api/reading-time على العنوان الجديد. يمكنك استخدام هذا العنوان مباشرة دون شراء نطاق.
تؤكد هذه الخطوة أن الطلب يصل إلى التطبيق المنشور بالحساب والإعدادات المقصودة. نجاح التجربة المحلية وحده لا يتحقق من ذلك.
ربط نطاقك الخاص
في مثالنا، ينشئ Worker الاستجابة النهائية بنفسه، فيؤدي دور خادم الأصل. يمكنك ربطه بنطاق مخصص (Custom Domain) ليستقبل الطلبات الموجهة إلى اسم مضيف مثل reading.example.com. إذا كان نطاقك مفعّلا على Cloudflare، فأضف routes إلى المستوى الأعلى من wrangler.jsonc، واستبدل الاسم باسم مضيف تملكه:
{
"routes": [
{
"pattern": "reading.example.com",
"custom_domain": true
}
]
}
أعد النشر بعد تعديل الإعدادات. تتولى Cloudflare إدارة سجل DNS وشهادة الاتصال المشفر للنطاق المخصص. اختر اسم مضيف غير مستخدم؛ فقد يتعارض سجل CNAME موجود مع هذا الإعداد. ويظل اختيار المسار داخل التطبيق مسؤولية Hono، فيصبح عنوان واجهتنا https://reading.example.com/api/reading-time.
أما Route فتستخدمه عندما يكون لديك خادم أصل بالفعل وتريد تشغيل Worker أمامه، كأن يفحص الطلب قبل تمريره إلى الخادم. يطابق Route نمطا في العنوان، ويتطلب سجل DNS مناسبا مع تفعيل وكيل Cloudflare. يشرح دليل التوجيه هذا الاستخدام. في مثالنا، يكفي النطاق المخصص لأن Worker ينتج الاستجابة بنفسه.
الإعدادات وربط الخدمات عبر env
قرأنا اسم الخدمة من c.env.SERVICE_NAME وسرعة القراءة الافتراضية من c.env.DEFAULT_WPM. ويمكن أن يتيح env أيضا قيما سرية أو وصولا إلى موارد مثل قواعد البيانات وحاويات الملفات:
داخل دالة المعالجة
ما الذي يتيحه env لشيفرتك؟
المثال الأول قيمة إعداد استخدمناها في الواجهة. أما البقية فتوضح ما قد تضيفه لاحقا.
c.envالقيم والخدمات المتاحة وفق الإعدادات- c.env.SERVICE_NAMEقيمة إعداداسم الخدمة كما حددناه في vars: reading-api. لا يحتاج إلى إخفاء.
- c.env.PROVIDER_API_KEYقيمة سريةبيانات اعتماد تستخدمها الشيفرة عند الاتصال بخدمة خارجية.
- c.env.DBربط بقاعدة بياناتوصول إلى قاعدة D1 محددة في الإعدادات، مع صلاحية استخدامها.
- c.env.UPLOADSربط بحاوية ملفاتوصول إلى حاوية R2 محددة. تبقى الملفات مخزنة في R2.
نسمي هذا ربطا (binding): تحدد المورد في الإعدادات، فتتيح Workers للشيفرة التعامل معه. مثلا، إذا ربطت قاعدة D1 باسم DB، أمكنك الاستعلام عنها عبر c.env.DB دون تضمين رمز API لحساب Cloudflare في الشيفرة. أضف إعداد الربط ثم أعد توليد الأنواع.
لا تحتاج أداة تقدير القراءة إلى حفظ النصوص. لكن إذا طوّرتها إلى محرر مقالات، فاختر التخزين بحسب البيانات وطريقة استخدامها:
- D1 للسجلات التي تريد الاستعلام عنها باستخدام SQL، مثل المقالات المحفوظة.
- KV للقيم التي تقرؤها بمفاتيحها، عندما يكون التأخر في ظهور التحديثات مقبولا.
- R2 للملفات، مثل المستندات المصدرة.
- Durable Objects لتنسيق العمل على كائن واحد، كأن يعدّل محرران المستند نفسه.
لكل خيار سلوكه وحدوده. حدد ما يحتاجه التطبيق قبل الاختيار؛ حفظ قيمة بين الطلبات ليس المعيار الوحيد. تفيدك مقارنة خيارات التخزين هنا، وسنفصل هذه الخيارات في مقالات لاحقة من السلسلة.
حفظ بيانات الاعتماد بأمان
إذا احتجت لاحقا إلى بيانات اعتماد لخدمة خارجية، فاحفظها كقيمة سرية:
npx wrangler secret put PROVIDER_API_KEY
سيطلب Wrangler إدخال القيمة، وتقرأها الشيفرة بعد ذلك عبر c.env.PROVIDER_API_KEY. أثناء التطوير المحلي، ضعها في .dev.vars وأضف النمطين .dev.vars* و.env* إلى .gitignore.
لا يرفع Wrangler القيم المحلية تلقائيا إلى بيئة الإنتاج؛ أضفها هناك بأمر secret put. يوضح دليل القيم السرية كيفية إعدادها لكل بيئة.
لا نحتاج إلى هذه الخطوة في مثالنا، لأن الأداة لا تتصل بخدمة خارجية.
فصل البيئة التجريبية عن الإنتاج
قبل تجربة تغييرات على تطبيق منشور، قد تحتاج إلى نسخة تجريبية مستقلة عنه. تتيح بيئات Wrangler نشر المشروع نفسه كتطبيقات Workers منفصلة. أضف env إلى الإعدادات الحالية:
{
"env": {
"staging": {
"routes": [],
"vars": {
"SERVICE_NAME": "reading-api-staging",
"DEFAULT_WPM": 200
}
}
}
}
استخدم --env staging عند تشغيل النسخة التجريبية أو نشرها:
npx wrangler dev --local --env staging
npx wrangler deploy --env staging
ينشئ أمر النشر هنا تطبيقا باسم reading-api-staging. تمنع قائمة routes الفارغة استخدام نطاق الإنتاج الذي أضفناه سابقا؛ اختبر النسخة التجريبية على عنوان workers.dev الخاص بها. وتبقى إعدادات المستوى الأعلى خاصة بالتطبيق الأصلي reading-api. لذلك حدد البيئة المقصودة عند تنفيذ أوامر تغيّر التطبيق المنشور.
لا ترث البيئة التجريبية متغيرات المستوى الأعلى أو إعدادات ربط الموارد؛ عرّفها لكل بيئة على حدة. أضف القيم السرية لكل بيئة أيضا، باستخدام --env staging عند الحاجة. وانتبه إلى أن فصل التطبيقين لا يفصل بياناتهما إذا ربطتهما بقاعدة البيانات نفسها.
مشروع واحد، وبيئتان
قاعدة بيانات مستقلة لكل بيئة
إذا أضفت D1 لاحقا، فخصص قاعدة بيانات لكل بيئة. يمكن للتطبيقين استخدام اسم الربط نفسه: c.env.DB.
src/index.tsالبيئة التجريبية
التطبيقreading-api-stagingc.env.DBقاعدة D1يصل c.env.DB إلى قاعدة بيانات للاختبار: reading-api-staging-db.الإنتاج
التطبيقreading-apic.env.DBقاعدة D1يصل c.env.DB إلى قاعدة بيانات الإنتاج: reading-api-production-db.
زمن المعالجة ومدة انتظار الاستجابة
بعد تشغيل التطبيق، كيف تقيس الوقت الذي يستهلكه؟ تخيل طلبا تمر معالجته بثلاث خطوات: التحقق من المدخلات، وانتظار قاعدة بيانات، ثم إعداد الاستجابة. لنفترض أن التحقق يستهلك 3 مللي ثانية من عمل المعالج، والانتظار 200 مللي ثانية، وإعداد الاستجابة 2 مللي ثانية.
استغرقت المعالجة نحو 205 مللي ثانية داخل الدالة، لكن زمن المعالجة الفعلي على المعالج (CPU time) كان 5 مللي ثانية فقط. هذه أرقام توضيحية لفهم الفرق، وليست قياسا لأداء واجهتنا.
جرب زيادة وقت الانتظار
طلب واحد، وقياسان للوقت
غيّر مدة انتظار البيانات، ولاحظ أن زمن المعالجة يبقى ثابتا.
- التحقق3 مللي ثانية معالجةتنفيذ فحوص المدخلات.
- انتظار البيانات200 مللي ثانية انتظاريزيد انتظار الشبكة مدة الطلب، من دون أن يزيد زمن معالجة Worker.
- التنسيق2 مللي ثانية معالجةإعداد الاستجابة.
نحتاج إلى هذا الفرق لفهم حدود الخطة وتكلفتها. بحسب حدود Workers بتاريخ 21 أيلول 2026، تنطبق الحدود التالية على معالجة طلبات HTTP العادية:
| الحد | Workers Free | Workers Paid |
|---|---|---|
| الطلبات الواردة | 100,000 يوميا | الفوترة بحسب الاستخدام |
| زمن المعالجة لكل طلب | 10 مللي ثانية | 30 ثانية افتراضيا، ويمكن رفعه إلى 5 دقائق |
| الذاكرة لكل بيئة معزولة | 128 ميغابايت | 128 ميغابايت |
تتشارك تطبيقات الحساب حصة الخطة المجانية اليومية، وتتجدد عند منتصف الليل بتوقيت UTC. لا يحتسب انتظار الشبكة ضمن زمن المعالجة، لكن ذلك لا يضمن استمرار الطلب إلى ما لا نهاية: إذا قطع العميل الاتصال، فقد تُلغى الأعمال التي لم تنته.
كذلك تتشارك الطلبات داخل البيئة المعزولة ذاكرتها. إذا تعاملت مع محتوى كبير، فاستخدم البث المتدفق (streaming) لتمريره تدريجيا بدلا من تحميله كله في الذاكرة. أما واجهتنا فتعيد كائن JSON صغيرا، وتناسبها c.json() في Hono.
ماذا لو كانت قاعدة البيانات بعيدة؟
تشغّل Cloudflare تطبيق Worker افتراضيا قرب نقطة دخول الطلب إلى شبكتها. يناسب ذلك مثالنا، لأن كل ما نحتاجه موجود في الطلب نفسه. أما إذا كان التطبيق يتصل مرارا بقاعدة بيانات بعيدة، فقد يقضي معظم وقته في انتظارها.
تتيح إعدادات موضع التنفيذ (Placement) تشغيل التطبيق أقرب إلى خدماته الخلفية إذا كان ذلك يقلل زمن الاستجابة الإجمالي. قِس مدة الطلب والاتصالات الخارجية قبل تغيير موضع التنفيذ؛ فالقرب من المستخدم لا يكفي وحده.
هل يمكن إكمال مهمة بعد إرسال الاستجابة؟
إذا كانت الاستجابة تعتمد على نتيجة عملية غير متزامنة، فانتظرها باستخدام await. أما الأعمال القصيرة التي لا تتوقف عليها الاستجابة، فيمكن منحها وقتا إضافيا عبر ctx.waitUntil()، أو c.executionCtx.waitUntil() في Hono.
تصل المهلة الموثقة لطلبات HTTP إلى 30 ثانية بعد إرسال الاستجابة أو انقطاع اتصال العميل. تتشارك جميع مهام waitUntil() التابعة للطلب نفسه هذه المهلة. وإذا بدأت عملية تعيد Promise دون انتظارها أو تمريرها إلى waitUntil()، فقد تتوقف قبل أن تكتمل.
للمهام التي تحتاج إلى تسليم موثوق وإعادة المحاولة عند الفشل، استخدم Queue. وللعمليات التي تتكون من خطوات وفترات انتظار وتحتاج إلى حفظ تقدمها، استخدم Workflow. وفي كل الأحوال، لا ترسل «تم الحفظ» قبل اكتمال الكتابة في قاعدة البيانات؛ نجاحها لاحقا أثناء التجربة المحلية لا يضمن أن يحدث ذلك في الإنتاج.
كم تبلغ التكلفة؟
ابدأ بالخطة المجانية لتجربة الواجهة، ثم قِس استهلاك المعالج وعدد الطلبات لتعرف متى تحتاج إلى خطة مدفوعة.
وقت كتابة المقال، يبدأ الاشتراك في خطة Workers Standard من 5 دولارات أمريكية للحساب شهريا. يشمل ذلك 10 ملايين طلب و30 مليون مللي ثانية من زمن المعالجة. وتبلغ تكلفة الاستخدام الزائد 0.30 دولار لكل مليون طلب، و0.02 دولار لكل مليون مللي ثانية من المعالجة.
شهر افتراضي · بالدولار الأمريكي
كيف نحسب تكلفة 12 مليون طلب؟
عند افتراض 4 مللي ثانية من المعالجة لكل طلب، نستهلك 48 مليون مللي ثانية. نضيف إلى الاشتراك الأساسي تكلفة ما يتجاوز الحصة المشمولة.
احسب تكلفة الحوسبة
خطة Standard المدفوعة · بالدولار الأمريكي · أسعار 21 أيلول 2026. يبدأ الحساب باشتراك 5 دولارات، حتى دون استخدام. الأداة مخصصة للخطة المدفوعة.
أدخل عدد طلبات بين 0 و1,000 مليون، وزمن معالجة بين 0 و300,000 مللي ثانية. صحّح القيمتين لعرض التقدير.
- الاشتراك الأساسييشمل 10 ملايين طلب و30 مليون مللي ثانية من المعالجة.$5.00
- الطلبات الإضافيةمليونا طلب فوق الحصة المشمولة، بسعر 0.30 دولار لكل مليون.$0.60
- المعالجة الإضافية18 مليون مللي ثانية إضافية، بسعر 0.02 دولار لكل مليون.$0.36
يفترض المثال أن الحصة المشمولة بالاشتراك متاحة كاملة لهذا التطبيق، وأن التطبيقات الأخرى في الحساب لم تستهلك منها شيئا. ولا يشمل الضرائب أو السجلات أو التخزين أو الخدمات الأخرى. أما متوسط 4 مللي ثانية لكل طلب فافتراض للحساب، وليس قياسا لأداء المثال.
الاشتراك الأساسي هو بداية الفاتورة، وليس سقفا لها. راقب الاستخدام والسجلات مع زيادة عدد الطلبات؛ فالاستجابة الصغيرة قد تُطلب ملايين المرات.
تتبع الطلب في السجلات
أثناء تشغيل wrangler dev، تظهر سجلات المثال في الطرفية. وبعد النشر، يمكنك متابعة السجلات مباشرة بالأمر:
npx wrangler tail
أرسل طلبا، وقارن قيمة ترويسة الاستجابة X-Request-Id مع requestId في سجل reading_api_response. ولمتابعة البيئة التجريبية، استخدم npx wrangler tail --env staging.
تجد السجلات المحفوظة في صفحة Workers Logs بلوحة التحكم، بعد تفعيل observability. عندما تكون head_sampling_rate مساوية لـ 1، تُختار جميع الطلبات للتسجيل؛ وعند 0.1 تُختار نحو 10% منها. يقلل أخذ العينات حجم السجلات، لكنه يعني أنك قد لا تجد سجلا لطلب محدد.
للسجلات حدود استخدام ومدة احتفاظ وتسعير خاص بها. ويكتب مثالنا حدثا في السجل من داخل الشيفرة، إضافة إلى حدث الاستدعاء الذي تسجله المنصة. لذلك قد ينتج عن الطلب الواحد أكثر من حدث.
إذا واجهت مشكلة، فابدأ بما يظهر لك في الاستجابة أو السجل:
| ما الذي تراه؟ | ما الذي تفحصه أولا؟ |
|---|---|
400 مع رسالة JSON من تطبيقنا | صيغة JSON والنص واللغة وسرعة القراءة |
404 مع رسالة JSON من تطبيقنا | المسار؛ /health أو /api/reading-time |
413 أو 415 | حجم جسم الطلب أو ترويسة Content-Type |
| لا يوجد سجل تطبيق مطابق | العنوان والتطبيق المستهدف والبيئة ونسبة أخذ العينات |
قيمة ربط تساوي undefined | اسم الربط وإعدادات البيئة المختارة |
الخطأ 1102 لتجاوز الموارد | استهلاك المعالج والذاكرة في الاستدعاء الذي فشل |
| زمن معالجة منخفض واستجابة بطيئة | اتصالات الشبكة وتأخر الخدمات الخلفية |
عند حدوث استثناء داخل التطبيق، ابحث عن قيمة X-Request-Id في سجل reading_api_error. ستجد اسم الخطأ، بينما يتلقى العميل رسالة عامة مع الحالة 500. مثلا، القيمة غير الصحيحة لـ DEFAULT_WPM خطأ في إعدادات التطبيق؛ لذلك نعيد 500 بدلا من 400 التي تدل على مشكلة في بيانات العميل. وإذا لم تصل استجابة من التطبيق أصلا، فافحص أخطاء التنفيذ واستهلاك الموارد في المنصة.
العودة إلى إصدار سابق
عند نشر تغيير، من المفيد أن تعرف كيف ترجع عنه. يسجّل إصدار Worker الشيفرة والإعدادات، وتحدد عملية النشر أي إصدار يستقبل الطلبات. اعرض الإصدارات الأخيرة بالأمر:
npx wrangler versions list
إذا تعطلت الواجهة بعد نشر تغيير، فاختر إصدارا سبق نشره وتأكدت من عمله، ثم ارجع إليه:
npx wrangler rollback <VERSION_ID>
استبدل <VERSION_ID> بالمعرّف الفعلي، وأضف --env staging إذا كانت البيئة التجريبية هي المقصودة. أعد اختبارات HTTP على العنوان المنشور بعد الرجوع.
العودة إلى إصدار سابق تعيد الشيفرة، لكنها لا تتراجع عن عمليات الكتابة في قاعدة البيانات ولا تستعيد الموارد المحذوفة. وقد يتعذر الرجوع بعد حذف مورد مرتبط بالتطبيق أو إجراء بعض تغييرات Durable Objects. لذلك، عندما تضيف بيانات دائمة، ضع خطة مستقلة لاستعادتها وتأكد من توافق بنيتها مع الشيفرة التي قد ترجع إليها.
ماذا تبني بعد ذلك؟
أصبح لديك تطبيق يعمل على Workers، ويمكنك تتبع ما يحدث فيه من وصول الطلب إلى ظهور نتيجته في السجل. فصلنا حساب المدة عن معالجة HTTP، واستخدمنا Hono لتنظيم المسارات، ثم جرّبنا الواجهة وتناولنا نشرها ومتابعة تشغيلها.
لتوسيع المثال، جرّب إتاحة تحليل عدة مسودات في طلب واحد. حدد عدد المسودات وحجم الطلب الإجمالي، وقرر ما يحدث إذا كانت إحداها غير صالحة: هل ترفض الدفعة كلها أم تعيد نتيجة مستقلة لكل مسودة؟ طبّق القرار واختبره.
في الجزء المقبل، سنضيف واجهة أمامية ونناقش خيارات استضافة المواقع على Workers، وما يقدمه Pages في هذا الجانب.
إذا كان هذا الدليل سيساعد أحد زملائك في بناء أول واجهة API على Workers، فشاركه معه. وما الجزء الذي احتجت وقتا أطول لفهمه في تجربتك: بيئة التشغيل، أم ربط الخدمات، أم النشر؟ راسلني بتجربتك، أو شارك المقال مع ملاحظاتك على X أو LinkedIn.