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

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

هل سئمت من إرسال ميغابايتات من 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-list
  • x-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 في متصفحك. يجب أن ترى:

  1. شريط بحث — اكتب أي شيء وستُفلتر النتائج فوريًا
  2. زر "مهمة جديدة" — يعرض/يخفي النموذج مع Alpine.js (بدون طلب شبكة)
  3. بطاقات مهام مع إجراءات ابدأ/إنهاء/تعديل/حذف
  4. ثلاثة أعمدة — قيد الانتظار، قيد التنفيذ، مكتمل

الخطوة 8: فهم توزيع المسؤوليات بين htmx و Alpine.js

إليك المفتاح المعماري:

المسؤوليةالأداةلماذا
جلب البياناتhtmxالخادم يملك البيانات، يعيد HTML
إرسال النماذجhtmxالخادم يتحقق ويعيد الواجهة المحدثة
البحث/التصفيةhtmxالخادم يصفي البيانات، يعيد جزء HTML
إظهار/إخفاءAlpine.jsحالة واجهة بحتة، لا حاجة للخادم
التحقق من النموذجAlpine.jsاستجابة فورية، بدون رحلة ذهاب وإياب
القوائم المنسدلةAlpine.jsتفاعل محلي فقط
الرسوم المتحركةCSS + Alpine.jsx-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 الأصلية

اختبار التطبيق

  1. البحث: اكتب في شريط البحث — يجب أن تُفلتر المهام في الوقت الفعلي بدون إعادة تحميل
  2. الإنشاء: انقر على "مهمة جديدة"، املأ النموذج، أرسل — يجب أن تظهر المهمة الجديدة في عمود "قيد الانتظار"
  3. تغيير الحالة: انقر على "ابدأ" على مهمة — يجب أن تنتقل إلى "قيد التنفيذ"
  4. التحرير المباشر: انقر على "تعديل"، غيّر العنوان، اضغط Enter — يجب أن يتحدث العنوان فورًا
  5. الحذف: انقر على "حذف"، أكّد — يجب أن تختفي بطاقة المهمة بانتقال سلس
  6. تبديل النموذج: انقر على "مهمة جديدة" / "إلغاء" — يجب أن ينزلق النموذج بدون أي طلب شبكة

استكشاف الأخطاء وإصلاحها

طلبات htmx لا تعمل

تأكد أن سكريبت htmx.org محمّل قبل عناصر HTML الخاصة بك. تحقق من وحدة تحكم المتصفح لأخطاء 404 على رابط CDN.

توجيهات Alpine.js لا تعمل

تأكد أن سمة defer موجودة على وسم سكريبت Alpine.js. يحتاج Alpine للتهيئة بعد جاهزية DOM.

الاستجابات الجزئية تستبدل كل الصفحة

تحقق من سمة hx-target — يجب أن تشير إلى معرّف عنصر محدد. إذا لم تُحدد، يستبدل htmx العنصر الذي أطلق الطلب.

مقارنة الأداء

إليك ما يرسله تطبيق htmx + Alpine.js نموذجي مقارنة بـ SPA React:

المقياسhtmx + Alpine.jsSPA 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.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على ثورة في تفاعل العملاء: الوكلاء الافتراضيون المدعومون بالذكاء الاصطناعي من Twilio.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

بناء واجهات REST API باستخدام Go و Fiber: دليل عملي للمبتدئين

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

30 د قراءة·

بناء واجهات REST API باستخدام Rust و Axum: دليل عملي للمبتدئين

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

30 د قراءة·