شرح htmx: TypeScript Express Live

٩ مايو ٢٠٢٦

htmx Tutorial: TypeScript Express Live Kanban (2026)

ملخص

ستقوم ببناء لوحة كانبان (kanban board) كاملة الوظائف وتتحدث لحظيًا باستخدام htmx 2.0.10، و TypeScript 6.0.3، و Express 5.2.1، و better-sqlite3 12.9.0 على Node.js 24 LTS — دون كتابة سطر واحد من كود إطارات عمل JavaScript في جانب العميل. يقوم الخادم بإرجاع أجزاء HTML (fragments)، ويقوم htmx بتبديلها في DOM، وتقوم Server-Sent Events بدفع التحديثات إلى كل متصفح متصل عندما يقوم أي مستخدم بتحريك بطاقة. إجمالي التطبيق: حوالي 200 سطر من TypeScript عبر أربعة ملفات مصدر، قابلة للتشغيل في 15 دقيقة.

يركز هذا البرنامج التعليمي لـ htmx على TypeScript و Express لأن النظام البيئي الحالي لعام 2026 يميل بشدة نحو نماذج Python/Flask أو Go. إذا كان فريقك يستخدم Node.js بالفعل، فهذه هي الحزمة التقنية التي تريدها — مسارات آمنة النوع (type-safe) تُرجع وسائط تشعبية (hypermedia)، و SSE للتحديثات اللحظية بدون WebSockets، و SQLite حتى لا تحتاج إلى إعداد قاعدة بيانات للمتابعة. كل إصدار أدناه محدد ومثبت مقابل السجل الرسمي في اليوم الذي كُتب فيه هذا المقال.

ما ستتعلمه

  • كيفية إنشاء مشروع htmx + Express 5 مكتوب بـ TypeScript باستخدام tsx للحصول على استجابة سريعة
  • كيفية نمذجة لوحة كانبان في SQLite باستخدام معاملات better-sqlite3 المتزامنة
  • كيفية عرض HTML الأولي و أجزاء HTML لـ hx-get، و hx-post، و hx-put، و hx-delete
  • كيفية استخدام hx-swap-oob (التبديلات خارج النطاق) لتحديث مناطق DOM متعددة غير مرتبطة من استجابة واحدة
  • كيفية إضافة تحديثات مباشرة لعدة مستخدمين باستخدام امتداد htmx SSE و EventSource API الأصلي
  • كيفية تأمين المسارات باستخدام التحقق من الصحة في جانب الخادم، وأجزاء الخطأ المنظمة، ومسار خطأ متوافق مع htmx
  • كيفية التحقق من كل شيء باستخدام curl حتى تثق في تنسيق البيانات قبل فتح المتصفح

المتطلبات الأساسية

الأداةالإصدارالسبب
Node.js24.15.0 (Active LTS، أبريل 2026)1Express 5 يحتاج Node 18+؛ إصدار LTS يحصل على تحديثات أمنية حتى أبريل 2028
npm10.x (مرفق مع Node 24)تثبيت الحزم
SQLiteمدمج في better-sqlite3 12.9.0لا حاجة لتثبيت منفصل
متصفح حديثChrome 120+ / Firefox 128+ / Safari 17+دعم EventSource الأصلي و CSS Grid الحديث

هذا البرنامج التعليمي لا يستخدم Docker. يوفر لك SQLite قاعدة بيانات بدون إعداد، ويأتي better-sqlite3 مع نسخة SQLite الخاصة به — اسحب المستودع، نفذ npm install، شغل، وانتهى الأمر.

الخطوة 1 — إنشاء هيكل المشروع

أنشئ المجلد ومشروع TypeScript الأساسي. نستخدم tsx (غلاف esbuild خفيف) بدلاً من ts-node لأنه يشغل ESM بشكل أصلي وهو أسرع بكثير في دورة التطوير.2

mkdir htmx-kanban && cd htmx-kanban
npm init -y
npm pkg set type=module
npm install express@5.2.1 better-sqlite3@12.9.0
npm install -D TypeScript@6.0.3 tsx@4.21.0 @types/node@24.0.0 @types/express@5.0.6 @types/better-sqlite3@7.6.13

ثبّت الإصدارات بدقة. يتحرك نظام npm البيئي في عام 2026 بسرعة، والدرس التعليمي الذي يتوقف عن العمل بمجرد تحديث إصدار رئيسي لـ ^ ليس درساً تعليمياً جيداً. إذا قرأت هذا بعد مايو 2026، فقم بتشغيل npm view <pkg> version لكل سطر أعلاه وقم إما بالتثبيت على تلك القيم أو التحديث بوعي.

أنشئ ملف tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2023"],
    "outDir": "dist",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts"]
}

خيار noUncheckedIndexedAccess هو أكثر علامة صرامة فائدة في قاعدة الكود هذه — فهو يجبرك على التعامل مع الصفوف المفقودة من استعلامات SQLite بشكل صريح.

أضف البرامج النصية (scripts) إلى package.json:

{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

أنشئ مجلدات المصدر: mkdir -p src public.

الخطوة 2 — نمذجة لوحة كانبان في SQLite

لوحة كانبان تتكون من جدولين: الأعمدة والبطاقات. سنقوم بتثبيت الأعمدة الثلاثة (todo، doing، done) برمجياً حتى لا نضطر لبناء واجهة مستخدم لإدارة الأعمدة، وسنحتفظ بالبطاقات في جدول واحد مع مميز column ورقم صحيح لـ sort_order.

أنشئ ملف src/db.ts:

import Database from "better-sqlite3";

export type Column = "todo" | "doing" | "done";
export interface Card {
  id: number;
  title: string;
  column: Column;
  sort_order: number;
  created_at: string;
}

const db = new Database("kanban.db");
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");

db.exec(`
  CREATE TABLE IF NOT EXISTS cards (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    title       TEXT    NOT NULL CHECK (length(title) BETWEEN 1 AND 200),
    "column"    TEXT    NOT NULL CHECK ("column" IN ('todo', 'doing', 'done')),
    sort_order  INTEGER NOT NULL DEFAULT 0,
    created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
  );
  CREATE INDEX IF NOT EXISTS idx_cards_column_sort ON cards("column", sort_order);
`);

export const queries = {
  listByColumn: db.prepare<[Column], Card>(
    `SELECT * FROM cards WHERE "column" = ? ORDER BY sort_order, id`
  ),
  insert: db.prepare<[string, Column, number]>(
    `INSERT INTO cards (title, "column", sort_order) VALUES (?, ?, ?)`
  ),
  getById: db.prepare<[number], Card>(`SELECT * FROM cards WHERE id = ?`),
  move: db.prepare<[Column, number]>(
    `UPDATE cards SET "column" = ? WHERE id = ?`
  ),
  remove: db.prepare<[number]>(`DELETE FROM cards WHERE id = ?`),
  maxSort: db.prepare<[Column], { m: number | null }>(
    `SELECT COALESCE(MAX(sort_order), -1) AS m FROM cards WHERE "column" = ?`
  ),
};

export default db;

هناك ثلاثة أمور تستحق الملاحظة. أولاً، كلمة column هي كلمة محجوزة في SQL، لذا نضعها بين علامتي اقتباس باستمرار — سواء في المخطط (schema) أو في كل عبارة معدة. ثانياً، قيد CHECK على "column" يسمح لقاعدة البيانات نفسها برفض قيم التعداد (enum) الخاطئة؛ نوع TypeScript يضيق نفس المجموعة في الكود، لكن SQL هو الحارس النهائي. ثالثاً، better-sqlite3 متزامن حسب التصميم3 — لا يوجد await هنا، ويمكن لدورة واحدة من حلقة أحداث Node تشغيل معاملة كاملة دون تنافس. هذه الخاصية هي ما يجعل عرض أجزاء htmx رخيصاً جداً من حيث الموارد.

الخطوة 3 — هيكل Express 5 مع ترميز HTML

أنشئ ملف src/server.ts:

import express from "express";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { queries } from "./db.js";
import { renderBoard, renderCard, renderError } from "./views.js";
import { sse } from "./sse.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express();

app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, "..", "public")));

app.get("/", (_req, res) => {
  const board = {
    todo: queries.listByColumn.all("todo"),
    doing: queries.listByColumn.all("doing"),
    done: queries.listByColumn.all("done"),
  };
  res.type("html").send(renderBoard(board));
});

const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
  console.log(`htmx-kanban listening on http://localhost:${port}`);
});

يتعامل Express 5 مع الأخطاء الملقاة والوعود المرفوضة في المعالجات غير المتزامنة بشكل أصلي، لذا لم نعد بحاجة إلى express-async-errors التي كان يتطلبها الإصدار v4.4 برمجية express.urlencoded الوسيطة هي ما يسمح لجسم طلب POST الافتراضي لـ htmx بتنسيق application/x-www-form-urlencoded بالتحول إلى req.body.

الآن أنشئ ملف src/views.ts لمعالجات عرض HTML. لن نستخدم محرك قوالب — بالنسبة لتطبيق وسائط تشعبية، فإن القوالب النصية الخام (template literals) بالإضافة إلى مساعد escape() صغير هو كل ما تحتاجه:

import type { Card, Column } from "./db.js";

const ESCAPE_MAP: Record<string, string> = {
  "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
};
export const esc = (s: string) =>
  s.replace(/[&<>"']/g, (c) => ESCAPE_MAP[c] ?? c);

export const renderCard = (card: Card): string => `
<li class="card" id="card-${card.id}">
  <span>${esc(card.title)}</span>
  <form
    hx-put="/cards/${card.id}/move"
    hx-target="closest .card"
    hx-swap="outerHTML"
    style="display:inline">
    ${(["todo", "doing", "done"] as Column[])
      .filter((c) => c !== card.column)
      .map((c) => `<button name="column" value="${c}">→ ${c}</button>`)
      .join("")}
  </form>
  <button
    hx-delete="/cards/${card.id}"
    hx-target="closest .card"
    hx-swap="outerHTML swap:200ms"
    hx-confirm="Delete this card?">×</button>
</li>`;

const renderColumn = (col: Column, cards: Card[]) => `
<section class="col" id="col-${col}">
  <header><h2>${col}</h2><span class="count">${cards.length}</span></header>
  <ul class="cards">
    ${cards.map(renderCard).join("")}
  </ul>
  <form hx-post="/cards" hx-target="#col-${col} .cards" hx-swap="beforeend">
    <input name="title" required maxlength="200" placeholder="New card…">
    <input type="hidden" name="column" value="${col}">
    <button type="submit">Add</button>
  </form>
</section>`;

export const renderBoard = (board: Record<Column, Card[]>) => `<!doctype html>
<html lang="en"><head>
  <meta charset="utf-8">
  <title>htmx kanban</title>
  <link rel="stylesheet" href="/ar/style.css">
  <script src="https://unpkg.com/htmx.org@2.0.10" crossorigin="anonymous"></script>
  <script src="https://unpkg.com/htmx-ext-sse@2.2.4" crossorigin="anonymous"></script>
  <script src="https://unpkg.com/htmx-ext-response-targets@2.0.4" crossorigin="anonymous"></script>
</head><body hx-ext="sse,response-targets" sse-connect="/events">
  <main class="board">
    ${(["todo", "doing", "done"] as Column[]).map((c) => renderColumn(c, board[c])).join("")}
  </main>
  <div id="toast" sse-swap="toast" hx-swap="innerHTML"></div>
</body></html>`;

export const renderError = (msg: string) =>
  `<p class="error" role="alert">${esc(msg)}</p>`;

بعض القرارات الخاصة بـ htmx التي يجب توضيحها:

  • hx-confirm على زر الحذف يعرض مربع حوار التأكيد الأصلي للمتصفح قبل إرسال الطلب.5
  • hx-swap="outerHTML swap:200ms" يؤخر تبديل DOM بمقدار 200 مللي ثانية، مما يمنح انتقال CSS وقتاً لتحريك البطاقة للخارج قبل أن تختفي.
  • يحمل عنصر body السمة hx-ext="sse,response-targets" sse-connect="/events" — يقوم htmx بتوصيل EventSource بهذا الرابط ويستمع للأحداث المسماة.6 يحتوي div #toast على sse-swap="toast"، مما يعني أن الأحداث المسماة فقط بـ toast هي التي ستؤدي إلى التبديل هناك. سنستخدم أسماء أحداث أخرى لتحديثات البطاقات اللحظية. (امتداد response-targets موجود أيضاً وهو ما يتيح سمة hx-target-4xx التي سنستخدمها لاحقاً في الخطوة 6.)

أضف ملف public/style.css لجعل اللوحة تبدو كلوحة كانبان (أي CSS بسيط سيفي بالغرض — اجعله قصيراً):

* { box-sizing: border-box; }
body { font: 16px/1.4 system-ui; margin: 0; padding: 1rem; background: #0f172a; color: #e2e8f0; }
.board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.col { background: #1e293b; border-radius: 8px; padding: 1rem; }
.col header { display: flex; justify-content: space-between; align-items: baseline; }
.cards { list-style: none; padding: 0; margin: 0 0 1rem; }
.card { background: #334155; border-radius: 6px; padding: 0.5rem; margin-bottom: 0.5rem;
        display: flex; gap: 0.5rem; align-items: center; transition: opacity 200ms; }
.card.htmx-swapping { opacity: 0; }
.card span { flex: 1; }
button { background: #475569; color: inherit; border: 0; padding: 0.25rem 0.5rem;
         border-radius: 4px; cursor: pointer; }
button:hover { background: #64748b; }
input[name="title"] { padding: 0.5rem; border-radius: 4px; border: 0; width: 100%; margin-bottom: 0.5rem; }
.error { color: #fca5a5; margin: 0.5rem 0; }
#toast:not(:empty) { position: fixed; bottom: 1rem; right: 1rem;
        background: #1e3a8a; padding: 0.75rem 1rem; border-radius: 6px; }

قاعدة .htmx-swapping هذه هي النصف المطابق لإعداد __PRE

هذا هو بالضبط دور hx-swap-oob.7 الاستجابة عبارة عن خليط من أجزاء صغيرة، كل منها يحتوي على سمة id وتوجيه hx-swap-oob. يقوم htmx بفحص الاستجابة وتطبيق كل جزء بشكل مستقل:

  • <li id="card-42" hx-swap-oob="delete"> — يبحث عن العنصر الموجود في الصفحة بالمعرف card-42 ويطبق استراتيجية التبديل delete. لا حاجة لمحدد (selector): افتراضيًا، يتم مطابقة العنصر الذي يحتوي على hx-swap-oob مع الـ DOM المباشر عن طريق الـ id.
  • <ul ... hx-swap-oob="beforeend:#col-doing .cards"> — يضيف البطاقة المغلفة إلى قائمة .cards داخل #col-doing. هذا هو شكل محدد CSS: <swap-strategy>:<selector>.
  • اثنان من <span ... hx-swap-oob="outerHTML:#col-todo .count"> و outerHTML:#col-doing .count — لتحديث عدادات الأعمدة معًا.

الشكلان قابلان للتبادل ولكنهما محسنان لحالات مختلفة. شكل مطابقة المعرف (hx-swap-oob="delete" على عنصر بمعرف id) هو الأبسط. أما شكل المحدد (hx-swap-oob="beforeend:#col-doing .cards") فيستهدف عبر محدد CSS ويسمح لك بربط أي استراتيجية تبديل مع أي هدف.7 رحلة ذهاب وإياب واحدة، أربعة تعديلات على الـ DOM، وصفر حالة (state) في جانب العميل.

الخطوة 5 — تحديثات مباشرة لعدة مستخدمين باستخدام Server-Sent Events

إذا كان هناك شخصان يفتحان اللوحة وقام أحدهما بنقل بطاقة، فيجب أن يرى المتصفح الآخر البطاقة وهي تتحرك أيضًا — دون الحاجة لعملية الاستقصاء (polling). هذه هي حالة الاستخدام النموذجية لـ SSE: من الخادم إلى العميل، أحادي الاتجاه، عبر نفس اتصال HTTP الذي يستخدمه تطبيقك بالفعل، دون تعقيدات الترقية (upgrade dance).8

أنشئ الملف src/sse.ts:

import type { Response } from "express";

class SseHub {
  private clients = new Set<Response>();

add(res: Response) {
    res.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
});
    res.flushHeaders();
    res.write(`retry: 5000\n\n`);
this.clients.add(res);
    res.on("close", () => this.clients.delete(res));
}

broadcast(event: string, html: string) {
const data = html.replace(/\n/g, "\ndata: ");
const payload = `event: ${event\ndata: ${data\n\n`;
for (const c of this.clients) c.write(payload);
}

count() { return this.clients.size; }
}

export const sse = new SseHub();

قم بتسجيل نقطة النهاية (endpoint) في src/server.ts قبل app.listen:

app.get("/events", (req, res) => {
  sse.add(res);
// Heartbeat every 25s so reverse proxies don't time the connection out.
const heartbeat = setInterval(() => res.write(`: heartbeat\n\n`), 25_000);
  req.on("close", () => clearInterval(heartbeat));
});

ثلاثة تفاصيل في بروتوكول SSE قد تسبب مشاكل:

  1. يجب أن تبدأ كل سطر من حمولة data: ببادئة data: . إذا كان كود HTML الخاص بك يحتوي على أسطر جديدة، فيجب استبدال كل \n بـ \ndata: وإلا سيتجاهل المتصفح الرسالة بصمت.8 وهذا ما يفعله سطر replace(/\n/g, "\ndata: ").
  2. السطر الجديد المزدوج \n\n هو منهي الرسالة. إذا نسيت السطر الجديد الثاني، فسيقوم المتصفح بتخزين الحدث مؤقتًا للأبد، منتظرًا الفاصل.
  3. retry: 5000 يخبر المتصفح بالانتظار لمدة 5 ثوانٍ قبل إعادة الاتصال إذا انقطع الاتصال. مفيد خلف البروكسيات غير المستقرة.

الآن نأتي لجانب htmx. يعلن الـ <body> بالفعل عن hx-ext="sse,response-targets" sse-connect="/events"، لذا فإن الـ EventSource مفتوح. للتبديل عند وقوع حدث مسمى، قم بتغليف المنطقة المتأثرة في حاوية تحتوي على sse-swap="<event-name>". ولكن لدينا شيء أكثر دقة هنا — البث يحتوي فقط على أجزاء hx-swap-oob. تعامل إضافة SSE في htmx الأحداث القادمة بنفس الطريقة التي تعامل بها استجابات AJAX، مما يعني أن تبديلات OOB في الحمولة تتم معالجتها تلقائيًا مقابل الـ DOM المباشر.9

أضف مستمعًا مخفيًا واحدًا في أسفل الجسم، مباشرة قبل </body>، عن طريق تعديل renderBoard:

// inside renderBoard, just above </body>:
<div id="sse-sink" sse-swap="card-added,card-moved,card-removed" hx-swap="none"></div>

hx-swap="none" يخبر htmx بعدم وضع الاستجابة داخل الـ sink — ولكنه لا يزال يحلل الاستجابة ويطبق تعليمات OOB. هذه هي بالضبط الحيلة التي تجعل هذا الأمر برمته يعمل بنقطة نهاية واحدة من عشرة أسطر.

افتح التطبيق في نافذتي متصفح جنبًا إلى جنب. أضف بطاقة في النافذة (أ). ستظهر في النافذة (ب) خلال أجزاء من الثانية، دون تحديث، ودون استقصاء، ودون مصافحة WebSocket.

الخطوة 6 — التحقق من الصحة وأجزاء الأخطاء المتوافقة مع htmx

التحقق من الصحة في الخطوة 4 بسيط للغاية. تحتاج التطبيقات الحقيقية إلى مكان تظهر فيه الأخطاء للمستخدم. أسلوب htmx هو إرجاع جزء الخطأ بحالة 4xx واستهداف منطقة خطأ مجاورة. قم بتحديث نموذج البطاقة الجديدة في renderColumn:

<form
  hx-post="/cards"
  hx-target="#col-${col} .cards"
  hx-swap="beforeend"
  hx-on::after-request="if(event.detail.successful) this.reset()"
  hx-target-4xx="#col-${col} .form-error">
  <input name="title" required maxlength="200" placeholder="New card…">
  <input type="hidden" name="column" value="${col}">
  <button type="submit">Add</button>
  <div class="form-error" id="col-${col}-err"></div>
</form>

هناك سمتان تثبتان جدارتهما هنا. hx-on::after-request هو معالج الأحداث المضمن في htmx 2.x — شكل النقطتين المزدوجتين هو اختصار لـ hx-on:htmx:after-request. يعمل المعالج عند اكتمال كل طلب، سواء نجح أو فشل، لذا نقوم بربط this.reset() بـ event.detail.successful — هذا العلم يكون true لاستجابات 2xx فقط.10 أما hx-target-4xx (الهدف الخاص بكود الحالة) فيرسل استجابات 400/422 إلى الـ <div> الخاص بالخطأ بدلاً من قائمة البطاقات، بحيث يرى المستخدم رسالة الخطأ في مكانها ولا تتلوث قائمة البطاقات بوسم <p class="error">. هذه السمة موجودة في إضافة response-targets — وهي متصلة بالفعل بالصفحة عبر وسم <script> الثالث و hx-ext="sse,response-targets" على الـ <body>.11 سمة واحدة، دون الحاجة لـ try/catch في جانب العميل.

الخطوة 7 — التحقق من كل شيء باستخدام curl

قبل فتح المتصفح، أثبت أن تنسيق البيانات هو بالضبط ما تتوقعه. قم بتشغيل خادم التطوير (npm run dev) في نافذة تيرمينال، وقم بتشغيل ما يلي في نافذة أخرى:

# 1. أنشئ بطاقة. توقع عنصر <li class="card" id="card-1">… واحدًا
curl -is -X POST http://localhost:3000/cards \
  -d 'title=Write tutorial&column=todo' | head

# 2. انقلها إلى "doing". توقع delete-OOB بالإضافة إلى insert-OOB بالإضافة إلى اثنين من count-OOBs.
curl -is -X PUT http://localhost:3000/cards/1/move -d 'column=doing'

# 3. راقب تدفق SSE بينما تقوم بالتعديل من تيرمينال آخر.
curl -N http://localhost:3000/events

يجب أن يظل الأمر الثالث صامتًا حتى تفعل شيئًا في تيرمينال آخر — وعندها سترى أسطرًا مثل:

event: card-moved
data: <li class="card" hx-swap-oob="delete:#card-1"></li>...

إذا رأيت أجزاءك تمر من هنا، فسيراها المتصفح أيضًا. إذا كانت أسطر data: مفقودة أو بدت الأحداث مشوهة، فإن معالجة السطر الجديد في SseHub.broadcast هي أول مكان يجب فحصه.

الأخطاء الشائعة

هذه هي حالات الفشل التي واجهتها أثناء بناء هذه المنظومة — وكل واحدة منها تأتي مع حل حقيقي.

htmx يرسل الطلب ولكن لا يحدث تبديل. المحدد في hx-target لا يطابق أي شيء في الصفحة. افتح DevTools، وقم بتشغيل document.querySelector("#col-todo .cards") في الكونسول، وعادة ما ستجد الخطأ الإملائي فورًا.

SSE يتصل ولكن لا تصل أي أحداث أبدًا. هناك سببان: قاعدة السطر الجديد لـ data: المذكورة أعلاه، أو بروكسي عكسي يقوم بتخزين الاستجابات مؤقتًا. يقوم Nginx بالإعدادات الافتراضية بتخزين SSE مؤقتًا؛ رأس X-Accel-Buffering: no في SseHub.add يعطل ذلك، ولكن فقط لـ nginx. في Cloudflare، اضبط Cache-Control: no-cache (تم بالفعل) وتأكد من أن مسارك ليس خلف قاعدة "تخزين كل شيء مؤقتًا" (cache everything).12

يتم تجاهل أجزاء hx-swap-oob بصمت. يعني هذا عادةً أن الاستجابة تتضمنها داخل عنصر آخر اعتبره htmx هدف التبديل الأساسي. أبسط حل: تأكد من أن استجابتك هي فقط أجزاء OOB (بدون عنصر مغلف)، أو قم بتضمين جزء أساسي متعمد أولاً يليه أشقاء OOB.

req.body هو undefined. لقد نسيت app.use(express.urlencoded({ extended: false })). يرسل htmx البيانات بتنسيق application/x-www-form-urlencoded افتراضيًا؛ وبدون برمجية المحلل الوسيطة (parser middleware)، يكون req.body هو undefined.

يشتكي TypeScript من result.lastInsertRowid. يقوم better-sqlite3 بإرجاع lastInsertRowid كـ number | bigint — لأي جدول واقعي يمكنك التحويل بأمان باستخدام Number()، ولكن إذا تجاوزت المعرفات الخاصة بك 2^53، فيجب عليك استخدام bigint من البداية إلى النهاية.

إلى أين تذهب بعد ذلك

تتكون قاعدة الكود المكتوبة من حوالي 200 سطر من TypeScript عبر أربعة ملفات وهي عرض توضيحي كامل وقابل للتشغيل. ثلاثة توسعات واضحة:

  • ترتيب الأعمدة المستمر. يقوم الكود الحالي بالإدراج فقط في نهاية العمود. لدعم إعادة الترتيب داخل العمود، التقط مؤشر الإدراج في نموذج النقل (<input type="hidden" name="position" value="...">) وأعد حساب sort_order للعمود الوجهة داخل كتلة db.transaction(...).
  • دعم اللوحات المتعددة. أضف جدول boards مع slug، واربط كل بطاقة بـ board_id، ووجه كل شيء عبر /b/:slug. يمكن لمركز SSE توزيع البيانات حسب board_id بحيث يرى المستخدمون البث الخاص بلوحتهم فقط.
  • المصادقة. Express 5 بالإضافة إلى express-session وبرمجية وسيطة واحدة requireUser كافية — اترك أجزاء htmx كما هي، ولكن قم بتغليف المسارات.

إذا كنت ترغب في مقارنة بدائل SSE لنقل HTML في الوقت الفعلي عبر الشبكة، فإن شرحنا لـ Postgres LISTEN/NOTIFY للتواجد في الوقت الفعلي يوضح نفس النمط مع مشغلات Postgres كعمود فقري للنشر/الاشتراك. للحصول على نظرة أعمق على تجميع Postgres المخصص للإنتاج والذي يناسب هذا النوع من أعباء عمل قاعدة البيانات المتزامنة، راجع برنامج pgbouncer و supavisor التعليمي للتجميع. وإذا قررت أن htmx ليس الخيار الصحيح بعد كل شيء، فإن برنامج هجرة Next.js من Pages إلى App-Router يغطي مكدس Next.js الحديث من البداية إلى النهاية.

خط htmx 4.0 في مرحلة التطوير الأولي (alpha) النشطة اعتباراً من مايو 2026، حيث يستهدف المشروع إصداراً تجريبياً (beta) في منتصف عام 2026 وإصداراً مستقراً 4.0 في أوائل عام 2027.13 يظل خط 2.x هو الإصدار الموصى به للإنتاج طوال تلك الفترة. التزم بالإصدار 2.0.10، وقم بإطلاقه، وأعد النظر عندما يحصل 4.0 على علامة إصدار مستقر.

Footnotes

  1. "Node.js Releases" — nodejs.org. دخل Node.js 24 مرحلة الدعم طويل الأمد (LTS) النشط في 28 أكتوبر 2025 (بعد ستة أشهر كإصدار حالي بعد إصدار v24.0.0 في 6 مايو 2025)؛ تم شحن التحديث 24.15.0 في أبريل 2026؛ ويستمر دعم LTS حتى 30 أبريل 2028. أصبح Node.js 26.0.0 هو الخط الحالي في 5 مايو 2026 ولن يدخل مرحلة LTS حتى أكتوبر 2026.

  2. "tsx — TypeScript Execute" — tsx.is. tsx هو غلاف esbuild خفيف لتشغيل ملفات TypeScript و ESM في Node مباشرة؛ أحدث إصدار على npm هو 4.21.0.

  3. "better-sqlite3" — npm. يوثق ملف README صراحةً واجهة برمجة التطبيقات (API) المتزامنة حسب التصميم؛ يدعم الإصدار 12.9.0 إصدارات Node من 20.x إلى 25.x.

  4. "Express@5.1.0: Now the Default on npm" — expressjs.com. أصبح Express 5 هو العلامة الافتراضية latest على npm في مارس 2025؛ وهو يتضمن معالجة أصلية للأخطاء الملقاة والوعود المرفوضة في معالجات المسارات غير المتزامنة. أحدث تصحيح في خط 5.x وقت كتابة هذا التقرير هو 5.2.1.

  5. "hx-confirm" — htmx.org. يطلق مربع حوار window.confirm() قبل إصدار الطلب.

  6. "The htmx Server Sent Event (SSE) Extension" — htmx.org. يستخدم ملحق 2.x السمة hx-ext="sse" للتثبيت و sse-connect بالإضافة إلى sse-swap لاستهلاك الأحداث.

  7. "hx-swap-oob Attribute" — htmx.org. التبديل خارج النطاق (Out-of-band swap)؛ يدعم true، وقيم استراتيجية التبديل، وأشكال المحددات مثل outerHTML:#some-selector أو beforeend:#some-selector. 2

  8. "Server-sent events" — MDN Web Docs. يوثق نوع المحتوى text/event-stream، وتأطير data: المسبوق بأسطر، ومنهي الرسالة بسطر جديد مزدوج، وحقل retry:. 2

  9. "The htmx Server Sent Event (SSE) Extension" — htmx.org. يسلم الملحق حمولات SSE إلى خط أنابيب تبديل htmx القياسي، مما يعني احترام توجيهات التبديل خارج النطاق في الحمولة.

  10. "hx-on" — htmx.org. يشغل اختصار hx-on::after-request تعبيراً مضمناً عندما ينهي htmx دورة حياة الطلب.

  11. "The htmx Response Targets Extension" — htmx.org. يتم توفير hx-target-[CODE] و hx-target-error بواسطة ملحق response-targets؛ يطابق متغير 4xx أي حالة من سلسلة 400. حزمة htmx-ext-response-targets في الإصدار 2.0.4 على npm.

  12. "NGINX Server-Sent Events" — Nginx documentation, plus Cloudflare community thread on SSE buffering. يتم التعرف على رأس الاستجابة X-Accel-Buffering: no بواسطة nginx لتعطيل تخزين الاستجابة مؤقتاً على أساس كل استجابة.

  13. "htmx 4.0 roadmap" — htmx GitHub Discussion #2198 و صفحة إصدارات htmx. 4.0 في مرحلة ألفا — تم شحن 4.0.0-alpha8 خلال أوائل عام 2026 — مع استهداف المشروع لنسخة تجريبية في منتصف عام 2026 ونسخة مستقرة 4.0 في أوائل عام 2027. خط 2.x هو الإصدار الموصى به للإنتاج؛ أحدث إصدار 2.x هو 2.0.10 على npm وعلى مقتطفات CDN الرسمية في htmx.org/extensions/sse/.

  14. نهاية القائمة المرتبة نهاية القسم

نشرة أسبوعية مجانية

ابقَ على مسار النيرد

بريد واحد أسبوعياً — دورات، مقالات معمّقة، أدوات، وتجارب ذكاء اصطناعي.

بدون إزعاج. إلغاء الاشتراك في أي وقت.