htmx و Alpine.js: بناء تطبيقات ويب تفاعلية بدون أطر عمل JavaScript ثقيلة

هل سئمت من إرسال ميغابايتات من JavaScript لمستخدميك؟ htmx و Alpine.js يتيحان لك بناء تطبيقات ويب تفاعلية للغاية باستخدام سمات HTML والحد الأدنى من JavaScript. في هذا الدليل، ستبني مدير مهام يعمل في الوقت الفعلي مع البحث والتحرير المباشر والانتقالات السلسة — كل ذلك بدون أي أداة بناء أو bundler.
أهداف التعلم
بنهاية هذا الدليل، ستكون قادرًا على:
- فهم نهج الوسائط الفائقة (Hypermedia) مع htmx
- إضافة تفاعلية من جانب العميل مع Alpine.js
- بناء عمليات CRUD بدون كتابة استدعاءات fetch
- دمج htmx و Alpine.js لمجموعة أدوات قوية وخفيفة
- نشر تطبيق مدير مهام كامل
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 18+ مثبت (سنستخدم Express كخادم خلفي)
- معرفة أساسية بـ HTML و CSS
- إلمام بمفاهيم واجهات REST API
- محرر أكواد (يُنصح بـ VS Code)
لا تحتاج أي معرفة بـ React أو Vue أو Angular — هذا هو بيت القصيد!
ما ستبنيه
مدير مهام كامل الوظائف يتضمن:
- بحث في الوقت الفعلي عبر htmx
- تحرير المهام مباشرة بدون إعادة تحميل الصفحة
- تغيير الحالة مع Alpine.js
- انتقالات CSS سلسة عند تبديل المحتوى
- عرض من جانب الخادم مع استجابات HTML جزئية
الخطوة 1: فهم نهج Hypermedia
تعمل تطبيقات SPA التقليدية هكذا: يقوم المتصفح بتنزيل حزمة JavaScript، ثم يجلب JSON من API ويعرض الواجهة من جانب العميل. htmx يقلب هذا النموذج — الخادم يُعيد أجزاء HTML، و htmx يدرجها في DOM.
SPA التقليدية:
المتصفح ← حزمة JS ← جلب JSON ← عرض DOM
نهج htmx:
المتصفح ← نقر/إدخال ← htmx يرسل الطلب ← الخادم يعيد HTML ← htmx يحدّث DOM
هذا يعني:
- JavaScript أقل يُرسل للعميل
- لا حاجة لإدارة الحالة من جانب العميل
- الخادم يتحكم في الواجهة — استخدم أي لغة خلفية
- تحسين تدريجي — يعمل جزئيًا بدون JS
Alpine.js يكمّل htmx بإدارة حالة الواجهة المحلية — القوائم المنسدلة، النوافذ المنبثقة، أزرار التبديل — أشياء لا تحتاج رحلة ذهاب وإياب للخادم.
الخطوة 2: إعداد المشروع
أنشئ مجلد مشروع جديد وقم بتهيئته:
mkdir htmx-task-manager && cd htmx-task-manager
npm init -y
npm install express ejsأنشئ هيكل المشروع:
mkdir -p views/partials public/cssيجب أن يبدو مجلدك هكذا:
htmx-task-manager/
├── views/
│ ├── partials/
│ │ ├── task-list.ejs
│ │ ├── task-item.ejs
│ │ └── task-form.ejs
│ └── index.ejs
├── public/
│ └── css/
│ └── styles.css
├── server.js
└── package.json
الخطوة 3: إعداد خادم Express
أنشئ server.js مع المنطق الخلفي:
const express = require("express");
const app = express();
app.set("view engine", "ejs");
app.use(express.static("public"));
app.use(express.urlencoded({ extended: true }));
// تخزين المهام في الذاكرة
let tasks = [
{ id: 1, title: "تعلم أساسيات htmx", status: "done", priority: "high" },
{ id: 2, title: "استكشاف Alpine.js", status: "in-progress", priority: "medium" },
{ id: 3, title: "بناء مدير المهام", status: "todo", priority: "high" },
{ id: 4, title: "إضافة ميزة البحث", status: "todo", priority: "low" },
];
let nextId = 5;
// الصفحة الرئيسية
app.get("/", (req, res) => {
res.render("index", { tasks });
});
// البحث في المهام — يعيد جزء HTML
app.get("/tasks/search", (req, res) => {
const query = req.query.q?.toLowerCase() || "";
const filtered = tasks.filter((t) =>
t.title.toLowerCase().includes(query)
);
res.render("partials/task-list", { tasks: filtered });
});
// إنشاء مهمة — يعيد HTML المهمة الجديدة
app.post("/tasks", (req, res) => {
const task = {
id: nextId++,
title: req.body.title,
status: "todo",
priority: req.body.priority || "medium",
};
tasks.push(task);
res.render("partials/task-item", { task });
});
// تحديث حالة المهمة
app.patch("/tasks/:id/status", (req, res) => {
const task = tasks.find((t) => t.id === parseInt(req.params.id));
if (!task) return res.status(404).send("المهمة غير موجودة");
task.status = req.body.status;
res.render("partials/task-item", { task });
});
// تعديل عنوان المهمة (مباشر)
app.put("/tasks/:id", (req, res) => {
const task = tasks.find((t) => t.id === parseInt(req.params.id));
if (!task) return res.status(404).send("المهمة غير موجودة");
task.title = req.body.title;
res.render("partials/task-item", { task });
});
// حذف مهمة
app.delete("/tasks/:id", (req, res) => {
tasks = tasks.filter((t) => t.id !== parseInt(req.params.id));
res.send("");
});
app.listen(3000, () => {
console.log("الخادم يعمل على http://localhost:3000");
});لاحظ كيف أن كل نقطة نهاية تعيد HTML، وليس JSON. هذا هو المبدأ الأساسي لـ htmx.
الخطوة 4: التخطيط الرئيسي مع htmx و Alpine.js
أنشئ views/index.ejs:
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>مدير المهام — htmx + Alpine.js</title>
<!-- htmx من CDN — هذا كل شيء، لا حاجة لخطوة بناء -->
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<!-- Alpine.js من CDN -->
<script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<div class="container" x-data="{ showForm: false }">
<header>
<h1>مدير المهام</h1>
<p>مبني بـ htmx + Alpine.js — بدون bundler، بدون framework</p>
</header>
<!-- شريط البحث — htmx يرسل طلبًا عند كل ضغطة مفتاح -->
<div class="search-bar">
<input
type="search"
name="q"
placeholder="البحث في المهام..."
hx-get="/tasks/search"
hx-trigger="input changed delay:300ms, search"
hx-target="#task-list"
hx-indicator=".search-spinner"
/>
<span class="search-spinner htmx-indicator">جاري البحث...</span>
</div>
<!-- تبديل النموذج مع Alpine.js (لا حاجة للخادم) -->
<button @click="showForm = !showForm" class="btn-primary">
<span x-text="showForm ? 'إلغاء' : 'مهمة جديدة'"></span>
</button>
<!-- نموذج المهمة الجديدة -->
<div x-show="showForm" x-transition class="task-form">
<%- include('partials/task-form') %>
</div>
<!-- حاوية قائمة المهام -->
<div id="task-list">
<%- include('partials/task-list', { tasks }) %>
</div>
</div>
</body>
</html>دعنا نحلل ما يحدث:
hx-get="/tasks/search"— htmx يرسل طلب GET لهذه النقطةhx-trigger="input changed delay:300ms"— يُفعَّل بعد 300 مللي ثانية من توقف المستخدم عن الكتابة (debounce)hx-target="#task-list"— HTML الاستجابة يستبدل محتوى#task-listx-data="{ showForm: false }"— حالة Alpine.js المحلية لتبديل النموذجx-show="showForm"— يعرض النموذج بشكل شرطي، بدون رحلة للخادم
الخطوة 5: إنشاء الأجزاء (Partials)
قائمة المهام (views/partials/task-list.ejs)
<div class="task-columns">
<% const statuses = ['todo', 'in-progress', 'done']; %>
<% statuses.forEach(status => { %>
<div class="column">
<h2 class="column-header column-<%= status %>">
<%= status === 'todo' ? 'قيد الانتظار' : status === 'in-progress' ? 'قيد التنفيذ' : 'مكتمل' %>
<span class="count">
(<%= tasks.filter(t => t.status === status).length %>)
</span>
</h2>
<div class="task-items">
<% tasks.filter(t => t.status === status).forEach(task => { %>
<%- include('task-item', { task }) %>
<% }) %>
</div>
</div>
<% }) %>
</div>عنصر المهمة (views/partials/task-item.ejs)
<div
class="task-card priority-<%= task.priority %>"
id="task-<%= task.id %>"
x-data="{ editing: false }"
>
<!-- وضع العرض -->
<div x-show="!editing">
<div class="task-header">
<span class="task-title"><%= task.title %></span>
<span class="priority-badge"><%= task.priority %></span>
</div>
<div class="task-actions">
<!-- تغيير الحالة مع htmx -->
<% if (task.status === 'todo') { %>
<button
hx-patch="/tasks/<%= task.id %>/status"
hx-vals='{"status": "in-progress"}'
hx-target="#task-list"
hx-get="/tasks/search?q="
hx-swap="innerHTML"
class="btn-sm btn-start"
>ابدأ</button>
<% } else if (task.status === 'in-progress') { %>
<button
hx-patch="/tasks/<%= task.id %>/status"
hx-vals='{"status": "done"}'
hx-target="#task-list"
hx-get="/tasks/search?q="
hx-swap="innerHTML"
class="btn-sm btn-done"
>إنهاء</button>
<% } %>
<!-- التحرير المباشر مع Alpine.js -->
<button @click="editing = true" class="btn-sm btn-edit">تعديل</button>
<!-- الحذف مع htmx -->
<button
hx-delete="/tasks/<%= task.id %>"
hx-target="#task-<%= task.id %>"
hx-swap="outerHTML"
hx-confirm="هل تريد حذف هذه المهمة؟"
class="btn-sm btn-delete"
>حذف</button>
</div>
</div>
<!-- وضع التحرير — Alpine يتحكم في الرؤية، htmx يحفظ -->
<div x-show="editing" x-transition>
<form
hx-put="/tasks/<%= task.id %>"
hx-target="#task-<%= task.id %>"
hx-swap="outerHTML"
>
<input
type="text"
name="title"
value="<%= task.title %>"
class="edit-input"
@keydown.escape="editing = false"
/>
<div class="edit-actions">
<button type="submit" class="btn-sm btn-save">حفظ</button>
<button type="button" @click="editing = false" class="btn-sm">إلغاء</button>
</div>
</form>
</div>
</div>نموذج المهمة (views/partials/task-form.ejs)
<form
hx-post="/tasks"
hx-target="#task-list"
hx-get="/tasks/search?q="
hx-swap="innerHTML"
x-data="{ title: '' }"
>
<div class="form-group">
<input
type="text"
name="title"
placeholder="ما الذي يجب فعله؟"
x-model="title"
required
/>
<select name="priority">
<option value="low">منخفضة</option>
<option value="medium" selected>متوسطة</option>
<option value="high">عالية</option>
</select>
<button type="submit" class="btn-primary" :disabled="!title.trim()">
إضافة
</button>
</div>
</form>الخطوة 6: إضافة الأنماط والانتقالات
أنشئ public/css/styles.css:
:root {
--bg: #0f172a;
--surface: #1e293b;
--border: #334155;
--text: #e2e8f0;
--text-muted: #94a3b8;
--primary: #3b82f6;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 2rem;
}
/* الأعمدة */
.task-columns {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
margin-top: 1.5rem;
}
/* بطاقات المهام */
.task-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
margin-top: 0.75rem;
transition: all 0.2s ease;
}
.task-card:hover { border-color: var(--primary); }
/* انتقالات htmx */
.htmx-swapping { opacity: 0; transition: opacity 0.2s ease-out; }
.htmx-settling { opacity: 1; transition: opacity 0.3s ease-in; }
@media (max-width: 768px) {
.task-columns { grid-template-columns: 1fr; }
}الخطوة 7: تشغيل التطبيق
شغّل الخادم:
node server.jsافتح http://localhost:3000 في متصفحك. يجب أن ترى:
- شريط بحث — اكتب أي شيء وستُفلتر النتائج فوريًا
- زر "مهمة جديدة" — يعرض/يخفي النموذج مع Alpine.js (بدون طلب شبكة)
- بطاقات مهام مع إجراءات ابدأ/إنهاء/تعديل/حذف
- ثلاثة أعمدة — قيد الانتظار، قيد التنفيذ، مكتمل
الخطوة 8: فهم توزيع المسؤوليات بين htmx و Alpine.js
إليك المفتاح المعماري:
| المسؤولية | الأداة | لماذا |
|---|---|---|
| جلب البيانات | htmx | الخادم يملك البيانات، يعيد HTML |
| إرسال النماذج | htmx | الخادم يتحقق ويعيد الواجهة المحدثة |
| البحث/التصفية | htmx | الخادم يصفي البيانات، يعيد جزء HTML |
| إظهار/إخفاء | Alpine.js | حالة واجهة بحتة، لا حاجة للخادم |
| التحقق من النموذج | Alpine.js | استجابة فورية، بدون رحلة ذهاب وإياب |
| القوائم المنسدلة | Alpine.js | تفاعل محلي فقط |
| الرسوم المتحركة | CSS + Alpine.js | x-transition لواجهة سلسة |
القاعدة العامة: إذا احتجت بيانات من الخادم، استخدم htmx. إذا كان تفاعلًا بصريًا بحتًا، استخدم Alpine.js.
الخطوة 9: أنماط متقدمة
التمرير اللانهائي مع htmx
أضف التقسيم إلى صفحات لقائمة مهامك:
<div
hx-get="/tasks?page=2"
hx-trigger="revealed"
hx-swap="afterend"
>
جاري تحميل المزيد من المهام...
</div>المُحفّز revealed يُفعَّل عندما يدخل العنصر منطقة العرض — مثالي للتمرير اللانهائي.
واجهة متفائلة مع Alpine.js
أظهر استجابة فورية قبل رد الخادم:
<button
x-data="{ loading: false }"
@click="loading = true"
:class="loading && 'opacity-50'"
hx-delete="/tasks/1"
hx-on::after-request="loading = false"
>
<span x-show="!loading">حذف</span>
<span x-show="loading">جاري الحذف...</span>
</button>دمج WebSocket
htmx يدعم اتصالات WebSocket للتحديثات في الوقت الفعلي:
<div hx-ext="ws" ws-connect="/ws">
<div id="notifications" hx-swap-oob="beforeend">
<!-- الخادم يدفع HTML هنا -->
</div>
</div>تبديلات Out-of-Band
حدّث عدة أجزاء من الصفحة من استجابة واحدة:
<!-- استجابة الخادم يمكن أن تتضمن عناصر out-of-band -->
<div id="task-list" hx-swap-oob="true">
<!-- قائمة المهام المحدثة -->
</div>
<div id="task-count" hx-swap-oob="true">
<!-- شارة العداد المحدثة -->
</div>الخطوة 10: إضافة تبديل السمة الداكنة/الفاتحة
هذه حالة استخدام مثالية لـ Alpine.js — بدون تدخل الخادم:
<div x-data="{ dark: true }" :class="dark ? 'theme-dark' : 'theme-light'">
<button @click="dark = !dark; localStorage.setItem('theme', dark ? 'dark' : 'light')">
<span x-text="dark ? 'الوضع الفاتح' : 'الوضع الداكن'"></span>
</button>
</div>Alpine.js يدير التبديل، CSS يدير التنسيق، و localStorage يحفظ التفضيل — كل ذلك بدون لمس الخادم.
متى لا تستخدم htmx
htmx ليس الخيار الصحيح لكل مشروع:
- حالة عميل معقدة — إذا كان تطبيقك يحتاج إدارة حالة متداخلة ومترابطة (مثل جدول بيانات أو أداة تصميم)، فإن إطار عمل مثل React أنسب
- تطبيقات offline-first — htmx يحتاج اتصال خادم لكل تفاعل
- التعاون المكثف في الوقت الفعلي — أدوات مثل Google Docs تحتاج حل نزاعات متطور لا يوفره htmx
- تطبيقات الهاتف المحمول — استخدم React Native أو Flutter أو SDKs الأصلية
اختبار التطبيق
- البحث: اكتب في شريط البحث — يجب أن تُفلتر المهام في الوقت الفعلي بدون إعادة تحميل
- الإنشاء: انقر على "مهمة جديدة"، املأ النموذج، أرسل — يجب أن تظهر المهمة الجديدة في عمود "قيد الانتظار"
- تغيير الحالة: انقر على "ابدأ" على مهمة — يجب أن تنتقل إلى "قيد التنفيذ"
- التحرير المباشر: انقر على "تعديل"، غيّر العنوان، اضغط Enter — يجب أن يتحدث العنوان فورًا
- الحذف: انقر على "حذف"، أكّد — يجب أن تختفي بطاقة المهمة بانتقال سلس
- تبديل النموذج: انقر على "مهمة جديدة" / "إلغاء" — يجب أن ينزلق النموذج بدون أي طلب شبكة
استكشاف الأخطاء وإصلاحها
طلبات htmx لا تعمل
تأكد أن سكريبت htmx.org محمّل قبل عناصر HTML الخاصة بك. تحقق من وحدة تحكم المتصفح لأخطاء 404 على رابط CDN.
توجيهات Alpine.js لا تعمل
تأكد أن سمة defer موجودة على وسم سكريبت Alpine.js. يحتاج Alpine للتهيئة بعد جاهزية DOM.
الاستجابات الجزئية تستبدل كل الصفحة
تحقق من سمة hx-target — يجب أن تشير إلى معرّف عنصر محدد. إذا لم تُحدد، يستبدل htmx العنصر الذي أطلق الطلب.
مقارنة الأداء
إليك ما يرسله تطبيق htmx + Alpine.js نموذجي مقارنة بـ SPA React:
| المقياس | htmx + Alpine.js | SPA React |
|---|---|---|
| حزمة JS | ~18 كيلوبايت (مضغوطة) | ~150-300 كيلوبايت |
| وقت التفاعل | ~200 مللي ثانية | ~1-3 ثانية |
| خطوة بناء مطلوبة | لا | نعم |
| عرض من الخادم | أصلي | يحتاج إعداد SSR |
| صديق لـ SEO | نعم، افتراضيًا | يحتاج Next.js/Remix |
الخطوات التالية
- استكشف إضافات htmx مثل
response-targetsوloading-states - أضف htmx + SSE للإشعارات في الوقت الفعلي
- جرب دمج htmx مع خلفية Python (مثل Django أو Flask أو FastAPI)
- اكتشف إضافات Alpine.js مثل
@alpinejs/persistو@alpinejs/intersect - اقرأ كتاب Hypermedia Systems لفهم أعمق
الخلاصة
يمثل htmx و Alpine.js عودة إلى بساطة تطوير الويب المعروض من الخادم — لكن مع التفاعلية التي يتوقعها المستخدمون من التطبيقات الحديثة. بترك الخادم يدير البيانات والحالة مع استخدام الحد الأدنى من JavaScript للتفاعلات، تحصل على تطبيقات أسرع وأبسط في الصيانة وأكثر وصولًا.
هذا المزيج قوي بشكل خاص لـ:
- تطبيقات CRUD (لوحات التحكم، مديري المهام، أنظمة إدارة المحتوى)
- المواقع الغنية بالمحتوى التي تحتاج بعض التفاعلية
- النماذج الأولية و MVP حيث سرعة التطوير مهمة
- الفرق بدون مطوري واجهات أمامية متخصصين
جربه في مشروعك القادم — قد تتفاجأ بما يمكنك تحقيقه بدون إطار عمل JavaScript.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

ابنِ أول إضافة كروم مدعومة بالذكاء الاصطناعي باستخدام Manifest V3 و OpenAI
تعلّم كيف تبني إضافة كروم تُلخّص صفحات الويب وتشرح النصوص باستخدام الذكاء الاصطناعي — خطوة بخطوة مع Manifest V3 و واجهة OpenAI البرمجية.

بناء واجهات REST API باستخدام Go و Fiber: دليل عملي للمبتدئين
تعلّم كيف تبني واجهات REST API سريعة وجاهزة للإنتاج باستخدام لغة Go وإطار Fiber. يغطي هذا الدليل خطوة بخطوة إعداد المشروع، التوجيه، معالجة JSON، الربط بقاعدة البيانات عبر GORM، الوسطاء، معالجة الأخطاء، والاختبارات — من الصفر إلى واجهة API عاملة.

بناء واجهات REST API باستخدام Rust و Axum: دليل عملي للمبتدئين
تعلّم كيف تبني واجهات REST API سريعة وآمنة باستخدام لغة Rust وإطار Axum. يغطي هذا الدليل خطوة بخطوة إعداد المشروع، التوجيه، معالجة JSON، الربط بقاعدة البيانات عبر SQLx، معالجة الأخطاء، والاختبارات — من الصفر إلى واجهة API عاملة.