يُتيح Deno 2.9 (الصادر في 25 يونيو 2026) أحدَ أكثر الميزات انتظارًا في بيئة JavaScript: أمرُ deno desktop التجريبي الذي يحوّل أيّ مشروع ويب إلى تطبيق سطح مكتب أصيل قابل للتوزيع. لا Electron، ولا ملفات إعداد Tauri معقّدة — فقط TypeScript.
في هذا الدليل ستبني تطبيق سطح مكتب لتدوين الملاحظات بصيغة Markdown — واجهة ويب تخدمها Deno.serve()، مربوطة بنظام التشغيل عبر واجهة برمجة Deno.BrowserWindow، مع شريط قوائم أصيل وأيقونة في صينية النظام، ومُجمَّعة في ملف ثنائي واحد لنظام macOS وWindows وLinux.
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Deno 2.9 أو أحدث (شغّل
deno upgradeإن كان لديك إصدار أقدم) - معرفة أساسية بـ TypeScript وJavaScript الحديث
- إلمام بـ Fresh أو Astro أو خادم HTTP العادي في Deno
- محرر كود (يُنصح بـ VS Code مع إضافة Deno)
ما ستبنيه
بنهاية هذا الدليل، سيكون لديك:
- تطبيق ملاحظات لسطح المكتب مع واجهة WebView
- خلفية TypeScript تعمل في Deno وتتواصل مع الواجهة عبر
window.bind() - شريط قوائم أصيل مع اختصارات لوحة مفاتيح
- أيقونة صينية النظام مع لوحة منبثقة
- ملف ثنائي واحد قابل للتوزيع على macOS وWindows وLinux
الخطوة 1: تثبيت أو تحديث Deno 2.9
إن لم يكن Deno مثبتًا، شغّل:
curl -fsSL https://deno.land/install.sh | shإن كان لديك Deno مسبقًا، حدّثه إلى 2.9:
deno upgrade
deno --version
# deno 2.9.0يُصنَّف deno desktop كـ تجريبي في Deno 2.9. الواجهة البرمجية تتثبّت بسرعة، لكن قد تطرأ تغييرات طفيفة في الإصدارات المقبلة.
الخطوة 2: إنشاء المشروع
أنشئ مجلد المشروع والملف الرئيسي:
mkdir deno-notes-app
cd deno-notes-appأنشئ deno.json (ملف إعداد مساحة عمل Deno):
{
"name": "deno-notes",
"version": "1.0.0",
"tasks": {
"dev": "deno desktop .",
"build": "deno desktop build --all-targets ."
},
"permissions": {
"read": true,
"write": true,
"net": ["localhost"]
}
}الخطوة 3: بناء نقطة دخول سطح المكتب
نقطة الدخول هي ملف TypeScript الذي يعمل في عملية Deno — لا في المتصفح. تتحكم هنا في النوافذ وقوائم النظام والميزات الأصيلة.
أنشئ main.ts:
// نقطة دخول سطح المكتب — تعمل في Deno، لا في WebView
const win = new Deno.BrowserWindow({
title: "Deno Notes",
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
});
// تشغيل خادم الواجهة
const server = Deno.serve({ port: 8000 }, async (req) => {
const url = new URL(req.url);
const filePath = url.pathname === "/" ? "/index.html" : url.pathname;
try {
const file = await Deno.readFile(`./public${filePath}`);
const ext = filePath.split(".").pop() ?? "txt";
const types: Record<string, string> = {
html: "text/html",
css: "text/css",
js: "application/javascript",
json: "application/json",
};
return new Response(file, {
headers: { "Content-Type": types[ext] ?? "text/plain" },
});
} catch {
return new Response("Not found", { status: 404 });
}
});
// فتح WebView على الخادم المحلي
win.loadURL("http://localhost:8000");
// ربط دوال Deno التي يمكن للواجهة استدعاؤها
win.bind("saveNote", async (id: string, content: string) => {
const path = `./notes/${id}.md`;
await Deno.mkdir("./notes", { recursive: true });
await Deno.writeTextFile(path, content);
return { ok: true };
});
win.bind("loadNote", async (id: string) => {
try {
const content = await Deno.readTextFile(`./notes/${id}.md`);
return { ok: true, content };
} catch {
return { ok: false, content: "" };
}
});
win.bind("listNotes", async () => {
try {
const entries = [];
for await (const entry of Deno.readDir("./notes")) {
if (entry.isFile && entry.name.endsWith(".md")) {
entries.push(entry.name.replace(".md", ""));
}
}
return { ok: true, notes: entries };
} catch {
return { ok: false, notes: [] };
}
});
win.bind("deleteNote", async (id: string) => {
await Deno.remove(`./notes/${id}.md`);
return { ok: true };
});
// إغلاق التطبيق عند إغلاق النافذة
win.addEventListener("close", () => {
server.shutdown();
Deno.exit(0);
});يستخدم Deno.BrowserWindow افتراضيًا WebView الأصيل لنظام التشغيل (WKWebView في macOS وWebView2 في Windows وWebKitGTK في Linux)، مما يُبقي الملفات الثنائية صغيرة. أضف { backend: "chromium" } في الخيارات إن احتجت إلى تطابق دقيق في التصيير عبر الأنظمة.
الخطوة 4: بناء واجهة الويب
أنشئ مجلد public/ والملف public/index.html:
mkdir publicأنشئ public/index.html:
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Deno Notes</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<aside id="sidebar">
<div class="sidebar-header">
<h2>الملاحظات</h2>
<button id="newNote">+ جديد</button>
</div>
<ul id="noteList"></ul>
</aside>
<main id="editor">
<input id="noteTitle" type="text" placeholder="عنوان الملاحظة..." />
<textarea id="noteContent" placeholder="ابدأ الكتابة بصيغة Markdown..."></textarea>
<div class="toolbar">
<button id="saveBtn">حفظ</button>
<button id="deleteBtn">حذف</button>
</div>
</main>
<script src="/app.js"></script>
</body>
</html>أنشئ public/styles.css:
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif;
display: flex;
height: 100vh;
background: #1a1a2e;
color: #e0e0e0;
}
#sidebar {
width: 260px;
background: #16213e;
border-left: 1px solid #0f3460;
display: flex;
flex-direction: column;
order: 1;
}
.sidebar-header {
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #0f3460;
}
#newNote {
background: #533483;
color: white;
border: none;
padding: 0.4rem 0.8rem;
border-radius: 6px;
cursor: pointer;
}
#noteList { list-style: none; overflow-y: auto; flex: 1; }
#noteList li {
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 1px solid #0f3460;
font-size: 0.9rem;
}
#noteList li:hover, #noteList li.active { background: #0f3460; }
#editor {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
gap: 0.75rem;
}
#noteTitle {
font-size: 1.4rem;
font-weight: 600;
background: transparent;
border: none;
border-bottom: 2px solid #533483;
color: #e0e0e0;
padding: 0.5rem 0;
outline: none;
}
#noteContent {
flex: 1;
background: #16213e;
color: #e0e0e0;
border: 1px solid #0f3460;
border-radius: 8px;
padding: 1rem;
font-family: "Fira Code", monospace;
font-size: 0.95rem;
resize: none;
outline: none;
}
.toolbar { display: flex; gap: 0.5rem; }
.toolbar button {
padding: 0.5rem 1.5rem;
border-radius: 6px;
border: none;
cursor: pointer;
font-weight: 600;
}
#saveBtn { background: #533483; color: white; }
#deleteBtn { background: #c0392b; color: white; }أنشئ public/app.js:
let currentNote = null;
async function loadNoteList() {
const result = await bindings.listNotes();
const ul = document.getElementById("noteList");
ul.innerHTML = "";
for (const id of result.notes) {
const li = document.createElement("li");
li.textContent = id;
li.dataset.id = id;
if (id === currentNote) li.classList.add("active");
li.addEventListener("click", () => openNote(id));
ul.appendChild(li);
}
}
async function openNote(id) {
currentNote = id;
const result = await bindings.loadNote(id);
document.getElementById("noteTitle").value = id;
document.getElementById("noteContent").value = result.content;
document.querySelectorAll("#noteList li").forEach((li) => {
li.classList.toggle("active", li.dataset.id === id);
});
}
document.getElementById("newNote").addEventListener("click", () => {
const id = "ملاحظة-" + Date.now();
currentNote = id;
document.getElementById("noteTitle").value = id;
document.getElementById("noteContent").value = "";
});
document.getElementById("saveBtn").addEventListener("click", async () => {
const id = document.getElementById("noteTitle").value.trim();
const content = document.getElementById("noteContent").value;
if (!id) return alert("يرجى إعطاء الملاحظة عنوانًا.");
currentNote = id;
await bindings.saveNote(id, content);
await loadNoteList();
});
document.getElementById("deleteBtn").addEventListener("click", async () => {
if (!currentNote) return;
await bindings.deleteNote(currentNote);
currentNote = null;
document.getElementById("noteTitle").value = "";
document.getElementById("noteContent").value = "";
await loadNoteList();
});
loadNoteList();المتغير bindings يُحقن تلقائيًا بواسطة Deno.BrowserWindow. كل دالة سجّلتها عبر win.bind() في نقطة الدخول تصبح متاحة بالاسم bindings.اسمها() داخل WebView. جميع الاستدعاءات تُعيد Promises.
الخطوة 5: إضافة شريط قوائم أصيل
أضف هذا الكود إلى main.ts بعد استدعاءات win.bind():
win.setMenu({
items: [
{
label: "ملف",
submenu: [
{ id: "new", label: "ملاحظة جديدة", accelerator: "CmdOrCtrl+N" },
{ id: "save", label: "حفظ", accelerator: "CmdOrCtrl+S" },
{ type: "separator" },
{ id: "quit", label: "إنهاء", accelerator: "CmdOrCtrl+Q" },
],
},
{
label: "تحرير",
submenu: [
{ id: "undo", label: "تراجع", role: "undo" },
{ id: "redo", label: "إعادة", role: "redo" },
{ type: "separator" },
{ id: "cut", label: "قص", role: "cut" },
{ id: "copy", label: "نسخ", role: "copy" },
{ id: "paste", label: "لصق", role: "paste" },
],
},
],
});
win.addEventListener("menuclick", (e) => {
const id = e.detail.id;
if (id === "new") win.executeJs("document.getElementById('newNote').click()");
if (id === "save") win.executeJs("document.getElementById('saveBtn').click()");
if (id === "quit") Deno.exit(0);
});عناصر role مثل "cut" و"copy" و"paste" تفوّض لنظام التشغيل إدارة الحافظة والتراجع — دون الحاجة لأي كود JavaScript.
الخطوة 6: إضافة أيقونة صينية النظام
أضف أيقونة في شريط المهام للوصول السريع إلى التطبيق:
const iconPath = new URL("./assets/icon.png", import.meta.url).pathname;
const iconBytes = await Deno.readFile(iconPath);
const tray = new Deno.Tray();
tray.setIcon(iconBytes);
tray.setTooltip("Deno Notes");
tray.setContextMenu({
items: [
{ id: "show", label: "إظهار النافذة" },
{ id: "quit", label: "إنهاء" },
],
});
tray.addEventListener("contextmenuclick", (e) => {
if (e.detail.id === "show") {
win.show();
win.focus();
}
if (e.detail.id === "quit") Deno.exit(0);
});أنشئ مجلد assets/ وضع فيه صورة PNG بحجم 32x32 بكسل.
الخطوة 7: التشغيل في وضع التطوير
deno task dev
# يكافئ: deno desktop .يكتشف Deno نقطة الدخول تلقائيًا من deno.json ويفتح النافذة. إعادة التحميل التلقائي غير مدعومة بعد — أعد تشغيل deno task dev عند تعديل main.ts، لكن التعديلات على public/ تُحمَّل بتحديث الصفحة.
أثناء التطوير، افتح DevTools داخل WebView عبر win.openDevTools() في نقطة الدخول، أو بإضافة الخيار --inspect إلى أمر deno desktop.
الخطوة 8: البناء والتعبئة للتوزيع
ابنِ ملفًا ثنائيًا أصيلًا للنظام الحالي:
deno desktop build .
# المخرجات: ./dist/deno-notes.app (macOS)
# ./dist/deno-notes.exe (Windows)
# ./dist/deno-notes.AppImage (Linux)صدّر لجميع الأنظمة في أمر واحد:
deno desktop build --all-targets .صيَغ محددة:
# صورة قرص macOS
deno desktop build --output dist/DenoNotes.dmg .
# مثبّت Windows
deno desktop build --output dist/DenoNotes.msi .
# حزمة Debian
deno desktop build --output dist/deno-notes.deb .يعمل التصدير المتقاطع من أي نظام — لا تحتاج جهاز macOS لبناء ملف .dmg. يتضمّن Deno سلسلة أدوات المنصة الهدف تلقائيًا.
بنية المشروع النهائية
deno-notes-app/
├── deno.json
├── main.ts # نقطة دخول سطح المكتب (عملية Deno)
├── assets/
│ └── icon.png
├── notes/ # تُنشأ وقت التشغيل
└── public/
├── index.html
├── styles.css
└── app.js
كيف يعمل الجسر
نموذج التواصل بين Deno والواجهة:
┌─────────────────────────────────────────┐
│ عملية Deno (main.ts) │
│ │
│ Deno.BrowserWindow │
│ ├─ win.bind("saveNote", fn) ◄──────────┼──── تسجيل معالج أصيل
│ ├─ win.executeJs("...") ──────────►│ استدعاء JS في WebView
│ └─ win.addEventListener(...) │
│ │
└──────────────────────┬───────────────────┘
│ IPC (آمن)
┌──────────────────────▼───────────────────┐
│ WebView (public/app.js) │
│ │
│ bindings.saveNote(id, content) ────────┼──► استدعاء معالج Deno
│ bindings.loadNote(id) ────────┘
│ │
└──────────────────────────────────────────┘
قناة IPC معزولة — لا يستطيع WebView الوصول المباشر لنظام الملفات. كل عمليات القراءة والكتابة تمر عبر دوال مرتبطة في عملية Deno، مما يوفر حدود أمان واضحة.
مقارنة مع Electron وTauri
| الميزة | Deno Desktop | Electron | Tauri |
|---|---|---|---|
| اللغة | TypeScript | JavaScript | Rust + JS |
| حجم الملف الثنائي | حوالي 15 ميغابايت | من 80 إلى 150 ميغابايت | من 5 إلى 10 ميغابايت |
| الخلفية | Deno (V8) | Node.js (V8) | Rust |
| WebView أصيل | نعم (افتراضي) | لا (Chromium) | نعم |
| Chromium مدمج | اختياري | دائمًا | لا |
| التصدير المتقاطع | مدمج | عبر electron-builder | عبر Tauri CLI |
| توافق NPM | كامل (توافق Node) | كامل | عبر جسر JS |
استكشاف الأخطاء
النافذة تفتح لكن محتواها أبيض فارغ
تأكد أن الخادم يبدأ قبل استدعاء win.loadURL(). أضف تأخيرًا صغيرًا أو انتظر حدث ready من الخادم.
bindings غير معرّف في WebView
تأكد أن win.bind() تُستدعى قبل win.loadURL(). يُحقن الربط في الصفحة عند تحميلها.
فشل البناء بخطأ DENO_DESKTOP_UNSTABLE
يحتاج أمر deno desktop إلى خيار --unstable-desktop في إصدارات Deno التي تسبق 2.9.0.
أيقونة الصينية لا تظهر على Linux
تحتاج إلى تثبيت libayatana-appindicator3-1: sudo apt install libayatana-appindicator3-1.
الخطوات التالية
بعد إتمام هذا التطبيق:
- أضف معاينة Markdown: صيّر النص إلى HTML باستخدام مكتبة Marked من CDN
- استخدم Fresh أو Astro: استبدل
Deno.serve()اليدوي بإطار عمل كامل — يكتشفهdeno desktopتلقائيًا - أضف مصادقة: استخدم
Deno.envفي نقطة الدخول لقراءة الأسرار دون كشفها للواجهة - انشر في متاجر التطبيقات: تعبّأ بصيغة
.pkgلـ macOS App Store أو.msixلـ Microsoft Store مع التوقيع الرقمي - استكشف Deno Windowing: زر windowing.deno.dev للحصول على واجهة نوافذ موسّعة تدعم نوافذ متعددة
خاتمة
يُخفّض أمر deno desktop في Deno 2.9 بشكل كبير العقبات أمام تطوير تطبيقات سطح المكتب متعددة الأنظمة. تحصل على خلفية TypeScript أصيلة، ومنصة الويب الكاملة للواجهة، وحدود أمان واضحة عبر window.bind()، وتصدير متقاطع بدون إعداد — كل ذلك دون العبء الثقيل لـ Electron. الواجهة البرمجية لا تزال تجريبية وتتطور، لكن أسسها باتت قوية بما يكفي لتوصيل منتجات حقيقية.
للاطلاع على آخر مستجدات الواجهة البرمجية، راجع التوثيق الرسمي لـ Deno Desktop.