ما ستبنيه
في هذا الدرس، ستبني تطبيق إدارة مهام متكامل باستخدام Deno 2 وإطار عمل Fresh. بنهاية الدرس، سيكون لديك تطبيق يعمل بالكامل مع:
- صفحات معروضة من الخادم بدون JavaScript افتراضياً
- جزر تفاعلية (Islands) للمكونات الديناميكية
- مسارات API لعمليات CRUD
- Deno KV لتخزين البيانات بشكل دائم
- TypeScript في كل مكان — بدون أي إعدادات
الوقت المطلوب: 45-60 دقيقة
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Deno 2 مثبّت — شغّل
deno --versionللتحقق (يجب أن يكون 2.x+) - معرفة أساسية بـ TypeScript/JavaScript
- إلمام بـ HTML و CSS
- محرر أكواد (يُنصح بـ VS Code مع إضافة Deno)
إذا لم يكن Deno مثبّتاً:
# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iexلماذا Deno 2 + Fresh؟
ما هو Deno 2؟
Deno 2 هو بيئة تشغيل JavaScript و TypeScript من الجيل التالي، أنشأها Ryan Dahl — المبتكر الأصلي لـ Node.js. يُصلح العديد من عيوب تصميم Node مع إضافة:
- دعم TypeScript مدمج — لا حاجة لملف
tsconfig.jsonأو خطوة بناء - آمن افتراضياً — أذونات صريحة للملفات والشبكة والبيئة
- توافق مع npm — استخدم أي حزمة npm مع محدد
npm: - أدوات مدمجة — منسّق، مدقق، مشغل اختبارات، ومقياس أداء
- Deno KV — قاعدة بيانات مفتاح-قيمة مدمجة
ما هو Fresh؟
Fresh هو أشهر إطار عمل ويب متكامل لـ Deno. يتميز بـ:
- بنية الجزر (Islands) — يُرسل JavaScript فقط للمكونات التفاعلية
- بدون خطوة بناء — الكود يعمل مباشرة، مما يتيح نشراً فورياً
- عرض من جانب الخادم — الصفحات تُعرض مسبقاً على الخادم لتحميل سريع
- توجيه قائم على الملفات — المسارات تتطابق مع مسارات الملفات
- Preact تحت الغطاء — مكتبة واجهة مستخدم خفيفة متوافقة مع React
الخطوة 1: إنشاء مشروع Fresh جديد
افتح الطرفية وأنشئ مشروع Fresh جديد:
deno run -A -r https://fresh.deno.dev my-task-managerعند السؤال:
- هل تريد استخدام مكتبة تنسيق؟ ← اختر
Tailwind CSS - هل تريد استخدام VS Code؟ ← اختر
Yes(إذا كنت تستخدم VS Code)
انتقل إلى المشروع:
cd my-task-managerشغّل خادم التطوير للتحقق:
deno task devافتح http://localhost:8000 في المتصفح. يجب أن ترى صفحة ترحيب Fresh.
بنية المشروع
هذا ما أنشأه Fresh:
my-task-manager/
├── components/ # مكونات واجهة مشتركة
├── islands/ # مكونات تفاعلية على جانب العميل
├── routes/ # مسارات قائمة على الملفات ونقاط API
│ ├── _app.tsx # غلاف التطبيق (التخطيط)
│ ├── index.tsx # الصفحة الرئيسية
│ └── api/ # مسارات API
├── static/ # ملفات ثابتة (CSS، صور)
├── deno.json # إعدادات Deno
├── dev.ts # نقطة دخول التطوير
├── main.ts # نقطة دخول الإنتاج
└── fresh.gen.ts # ملف manifest مُولّد تلقائياً
مفهوم أساسي: الملفات في routes/ تُعرض من الخادم افتراضياً. الملفات في islands/ تُحيّى (hydrated) على العميل مع JavaScript. هذه هي بنية الجزر — فقط الأجزاء التفاعلية ترسل JS إلى المتصفح.
الخطوة 2: تعريف نموذج بيانات المهام
أنشئ ملفاً جديداً لأنواع المهام وعمليات البيانات.
أنشئ utils/db.ts:
// utils/db.ts
export interface Task {
id: string;
title: string;
description: string;
completed: boolean;
createdAt: string;
updatedAt: string;
}
// فتح قاعدة بيانات Deno KV
const kv = await Deno.openKv();
export async function getAllTasks(): Promise<Task[]> {
const tasks: Task[] = [];
const entries = kv.list<Task>({ prefix: ["tasks"] });
for await (const entry of entries) {
tasks.push(entry.value);
}
// ترتيب حسب تاريخ الإنشاء، الأحدث أولاً
return tasks.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
}
export async function getTask(id: string): Promise<Task | null> {
const entry = await kv.get<Task>(["tasks", id]);
return entry.value;
}
export async function createTask(
title: string,
description: string
): Promise<Task> {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const task: Task = {
id,
title,
description,
completed: false,
createdAt: now,
updatedAt: now,
};
await kv.set(["tasks", id], task);
return task;
}
export async function updateTask(
id: string,
updates: Partial<Pick<Task, "title" | "description" | "completed">>
): Promise<Task | null> {
const existing = await getTask(id);
if (!existing) return null;
const updated: Task = {
...existing,
...updates,
updatedAt: new Date().toISOString(),
};
await kv.set(["tasks", id], updated);
return updated;
}
export async function deleteTask(id: string): Promise<boolean> {
const existing = await getTask(id);
if (!existing) return false;
await kv.delete(["tasks", id]);
return true;
}لماذا Deno KV؟ إنها مخزن مفتاح-قيمة مدمج لا يحتاج أي إعداد. البيانات تبقى محفوظة بعد إعادة تشغيل الخادم محلياً، وعند النشر على Deno Deploy تصبح قاعدة بيانات موزعة عالمياً.
الخطوة 3: بناء مسارات API
يستخدم Fresh التوجيه القائم على الملفات لنقاط API أيضاً. أنشئ نقاط RESTful لإدارة المهام.
قائمة وإنشاء المهام
أنشئ routes/api/tasks.ts:
// routes/api/tasks.ts
import { Handlers } from "$fresh/server.ts";
import { createTask, getAllTasks } from "../../utils/db.ts";
export const handler: Handlers = {
// GET /api/tasks — عرض جميع المهام
async GET(_req, _ctx) {
const tasks = await getAllTasks();
return new Response(JSON.stringify(tasks), {
headers: { "Content-Type": "application/json" },
});
},
// POST /api/tasks — إنشاء مهمة جديدة
async POST(req, _ctx) {
const body = await req.json();
const { title, description } = body;
if (!title || typeof title !== "string") {
return new Response(
JSON.stringify({ error: "العنوان مطلوب" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const task = await createTask(title, description || "");
return new Response(JSON.stringify(task), {
status: 201,
headers: { "Content-Type": "application/json" },
});
},
};تحديث وحذف مهمة واحدة
أنشئ routes/api/tasks/[id].ts:
// routes/api/tasks/[id].ts
import { Handlers } from "$fresh/server.ts";
import { deleteTask, getTask, updateTask } from "../../../utils/db.ts";
export const handler: Handlers = {
// GET /api/tasks/:id
async GET(_req, ctx) {
const task = await getTask(ctx.params.id);
if (!task) {
return new Response(
JSON.stringify({ error: "المهمة غير موجودة" }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify(task), {
headers: { "Content-Type": "application/json" },
});
},
// PATCH /api/tasks/:id
async PATCH(req, ctx) {
const body = await req.json();
const task = await updateTask(ctx.params.id, body);
if (!task) {
return new Response(
JSON.stringify({ error: "المهمة غير موجودة" }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify(task), {
headers: { "Content-Type": "application/json" },
});
},
// DELETE /api/tasks/:id
async DELETE(_req, ctx) {
const success = await deleteTask(ctx.params.id);
if (!success) {
return new Response(
JSON.stringify({ error: "المهمة غير موجودة" }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
},
};الخطوة 4: إنشاء جزيرة قائمة المهام
الجزر هي طريقة Fresh لإضافة التفاعلية. فقط مكونات الجزر ترسل JavaScript إلى المتصفح — كل شيء آخر يبقى HTML ثابتاً.
أنشئ islands/TaskList.tsx:
// islands/TaskList.tsx
import { useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
interface Task {
id: string;
title: string;
description: string;
completed: boolean;
createdAt: string;
}
export default function TaskList() {
const tasks = useSignal<Task[]>([]);
const newTitle = useSignal("");
const newDescription = useSignal("");
const loading = useSignal(true);
const error = useSignal("");
// جلب المهام عند التحميل
useEffect(() => {
fetchTasks();
}, []);
async function fetchTasks() {
loading.value = true;
try {
const res = await fetch("/api/tasks");
tasks.value = await res.json();
} catch (e) {
error.value = "فشل تحميل المهام";
} finally {
loading.value = false;
}
}
async function addTask(e: Event) {
e.preventDefault();
if (!newTitle.value.trim()) return;
try {
const res = await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: newTitle.value,
description: newDescription.value,
}),
});
if (res.ok) {
newTitle.value = "";
newDescription.value = "";
await fetchTasks();
}
} catch (e) {
error.value = "فشل إضافة المهمة";
}
}
async function toggleTask(id: string, completed: boolean) {
try {
await fetch(`/api/tasks/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ completed: !completed }),
});
await fetchTasks();
} catch (e) {
error.value = "فشل تحديث المهمة";
}
}
async function removeTask(id: string) {
try {
await fetch(`/api/tasks/${id}`, { method: "DELETE" });
await fetchTasks();
} catch (e) {
error.value = "فشل حذف المهمة";
}
}
return (
<div class="max-w-2xl mx-auto p-4">
{/* نموذج إضافة مهمة */}
<form onSubmit={addTask} class="mb-8 bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold mb-4 text-gray-800">إضافة مهمة جديدة</h2>
<input
type="text"
placeholder="عنوان المهمة..."
value={newTitle.value}
onInput={(e) => newTitle.value = (e.target as HTMLInputElement).value}
class="w-full p-3 border border-gray-300 rounded-lg mb-3 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
<textarea
placeholder="الوصف (اختياري)..."
value={newDescription.value}
onInput={(e) => newDescription.value = (e.target as HTMLTextAreaElement).value}
class="w-full p-3 border border-gray-300 rounded-lg mb-3 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={2}
/>
<button
type="submit"
class="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
إضافة المهمة
</button>
</form>
{/* رسالة الخطأ */}
{error.value && (
<div class="bg-red-100 text-red-700 p-3 rounded-lg mb-4">
{error.value}
</div>
)}
{/* حالة التحميل */}
{loading.value && (
<p class="text-center text-gray-500">جاري تحميل المهام...</p>
)}
{/* قائمة المهام */}
{!loading.value && tasks.value.length === 0 && (
<p class="text-center text-gray-500 py-8">
لا توجد مهام بعد. أضف مهمتك الأولى!
</p>
)}
<div class="space-y-3">
{tasks.value.map((task) => (
<div
key={task.id}
class={`bg-white rounded-lg shadow p-4 flex items-start gap-3 transition-opacity ${
task.completed ? "opacity-60" : ""
}`}
>
<button
onClick={() => toggleTask(task.id, task.completed)}
class={`mt-1 w-5 h-5 rounded border-2 flex-shrink-0 flex items-center justify-center ${
task.completed
? "bg-green-500 border-green-500 text-white"
: "border-gray-300 hover:border-blue-500"
}`}
>
{task.completed && "✓"}
</button>
<div class="flex-1 min-w-0">
<h3
class={`font-semibold text-gray-800 ${
task.completed ? "line-through" : ""
}`}
>
{task.title}
</h3>
{task.description && (
<p class="text-gray-500 text-sm mt-1">{task.description}</p>
)}
<p class="text-gray-400 text-xs mt-1">
{new Date(task.createdAt).toLocaleDateString("ar")}
</p>
</div>
<button
onClick={() => removeTask(task.id)}
class="text-red-400 hover:text-red-600 flex-shrink-0 text-lg"
title="حذف المهمة"
>
×
</button>
</div>
))}
</div>
</div>
);
}نقاط مهمة:
useSignalمن Preact Signals توفر حالة تفاعلية — أكثر أداءً منuseState- هذا المكون يعيش في
islands/لذا يُرسل JavaScript إلى العميل - كل شيء آخر في الصفحة يبقى HTML ثابتاً
الخطوة 5: إنشاء الصفحة الرئيسية
الآن اربط الجزيرة بصفحة معروضة من الخادم.
استبدل routes/index.tsx:
// routes/index.tsx
import { Head } from "$fresh/runtime.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
import { getAllTasks } from "../utils/db.ts";
import TaskList from "../islands/TaskList.tsx";
interface PageData {
totalTasks: number;
completedTasks: number;
}
export const handler: Handlers<PageData> = {
async GET(_req, ctx) {
const tasks = await getAllTasks();
return ctx.render({
totalTasks: tasks.length,
completedTasks: tasks.filter((t) => t.completed).length,
});
},
};
export default function Home({ data }: PageProps<PageData>) {
const { totalTasks, completedTasks } = data;
const progress = totalTasks > 0
? Math.round((completedTasks / totalTasks) * 100)
: 0;
return (
<>
<Head>
<title>مدير المهام — مبني بـ Deno 2 و Fresh</title>
</Head>
<div class="min-h-screen bg-gray-100">
{/* الرأس */}
<header class="bg-white shadow-sm">
<div class="max-w-2xl mx-auto px-4 py-6">
<h1 class="text-3xl font-bold text-gray-900">
📋 مدير المهام
</h1>
<p class="text-gray-500 mt-1">
مبني بـ Deno 2 و Fresh — معروض من الخادم مع جزر تفاعلية
</p>
</div>
</header>
{/* شريط الإحصائيات — معروض من الخادم، بدون JS */}
<div class="max-w-2xl mx-auto px-4 mt-6">
<div class="bg-white rounded-lg shadow p-4 flex items-center justify-between">
<div class="flex gap-6 text-sm">
<span class="text-gray-600">
الإجمالي: <strong class="text-gray-900">{totalTasks}</strong>
</span>
<span class="text-gray-600">
مكتملة: <strong class="text-green-600">{completedTasks}</strong>
</span>
<span class="text-gray-600">
متبقية:{" "}
<strong class="text-blue-600">
{totalTasks - completedTasks}
</strong>
</span>
</div>
<div class="flex items-center gap-2">
<div class="w-24 bg-gray-200 rounded-full h-2">
<div
class="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<span class="text-xs text-gray-500">{progress}%</span>
</div>
</div>
</div>
{/* المحتوى الرئيسي */}
<main class="py-6">
<TaskList />
</main>
{/* التذييل */}
<footer class="text-center py-6 text-gray-400 text-sm">
<p>
مدعوم بـ{" "}
<a href="https://deno.com" class="text-blue-500 hover:underline">
Deno 2
</a>{" "}
و{" "}
<a href="https://fresh.deno.dev" class="text-blue-500 hover:underline">
Fresh
</a>
</p>
</footer>
</div>
</>
);
}الخطوة 6: إضافة وسيط للتسجيل
يدعم Fresh الوسائط (Middleware) للوظائف المشتركة. لنضف تسجيل الطلبات.
أنشئ routes/_middleware.ts:
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
export async function handler(req: Request, ctx: FreshContext) {
const start = Date.now();
const url = new URL(req.url);
// معالجة الطلب
const resp = await ctx.next();
const duration = Date.now() - start;
const status = resp.status;
console.log(
`${req.method} ${url.pathname} — ${status} (${duration}ms)`
);
return resp;
}الخطوة 7: التشغيل والاختبار
شغّل خادم التطوير:
deno task devافتح http://localhost:8000 واختبر:
- إضافة مهمة — املأ العنوان والوصف، اضغط "إضافة المهمة"
- تبديل الإكمال — اضغط على مربع الاختيار بجانب المهمة
- حذف مهمة — اضغط زر ×
- تحديث الصفحة — المهام تبقى محفوظة بفضل Deno KV
اختبار API مباشرة
# عرض المهام
curl http://localhost:8000/api/tasks
# إنشاء مهمة
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "تعلم Deno 2", "description": "إكمال درس Fresh"}'
# تبديل حالة مهمة (استبدل TASK_ID)
curl -X PATCH http://localhost:8000/api/tasks/TASK_ID \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# حذف مهمة
curl -X DELETE http://localhost:8000/api/tasks/TASK_IDالخطوة 8: النشر على Deno Deploy
Deno Deploy هي منصة الاستضافة الطبيعية لتطبيقات Fresh. النشر شبه فوري.
الخيار أ: عبر تكامل GitHub
- ارفع مشروعك إلى GitHub
- اذهب إلى dash.deno.com
- أنشئ مشروعاً جديداً
- اربط مستودع GitHub الخاص بك
- عيّن نقطة الدخول إلى
main.ts - انشر — يستغرق ثوانٍ فقط
الخيار ب: عبر سطر الأوامر
# تثبيت deployctl
deno install -Arf jsr:@deno/deployctl
# النشر
deployctl deploy --project=my-task-manager --entrypoint=main.tsDeno KV على Deploy: عند النشر على Deno Deploy، يتم توزيع بيانات KV عالمياً تلقائياً — بدون إعداد قاعدة بيانات أو سلاسل اتصال.
استكشاف الأخطاء
أخطاء "تم رفض الإذن"
Deno آمن افتراضياً. إذا رأيت أخطاء أذونات، تأكد من استخدام علامات --allow-* أو تشغيل مع -A أثناء التطوير:
deno run -A main.ts"الوحدة غير موجودة" لحزم npm
استخدم محدد npm: في الاستيراد:
import express from "npm:express@4";بيانات Deno KV لا تُحفظ
افتراضياً، تُخزّن بيانات KV في ملف محلي. تأكد من وجود أذونات كتابة للمجلد. لمسار مخصص:
const kv = await Deno.openKv("./data/kv.db");ما تعلمته
في هذا الدرس، بنيت تطبيقاً متكاملاً وتعلمت:
- بنية مشروع Fresh — المسارات، الجزر، المكونات، والأدوات المساعدة
- بنية الجزر — إرسال JavaScript فقط حيث يُحتاج
- العرض من جانب الخادم — صفحات معروضة على الخادم لتحميل فوري
- مسارات API — نقاط RESTful مع توجيه قائم على الملفات
- Deno KV — تخزين بيانات دائم بدون إعدادات
- الوسائط — وظائف مشتركة مثل التسجيل
- النشر — الدفع إلى الإنتاج مع Deno Deploy
الخطوات التالية
- إضافة المصادقة — استخدم Deno KV OAuth لتسجيل الدخول عبر GitHub/Google
- إضافة فئات المهام — وسّع نموذج البيانات بعلامات وفلاتر
- إضافة تحديثات فورية — استخدم Server-Sent Events (SSE) لمزامنة المهام مباشرة
- استكشاف إضافات Fresh — تحقق من fresh.deno.dev/docs/concepts/plugins
- اقرأ وثائق Deno — استكشف docs.deno.com لميزات بيئة التشغيل
الخلاصة
يقدم Deno 2 و Fresh نهجاً بسيطاً ومنعشاً لتطوير الويب المتكامل. بنية الجزر تضمن أن تطبيقاتك سريعة افتراضياً، TypeScript يعمل بدون إعدادات، و Deno KV يلغي الحاجة لإعداد قاعدة بيانات خارجية. إذا سئمت من سلاسل أدوات البناء المعقدة والأُطر الثقيلة، فإن Fresh يستحق التجربة الجدية لمشروعك القادم.