بناء تطبيق ويب كامل باستخدام SolidStart: دليل عملي شامل

ما الذي ستبنيه
في هذا الدليل، ستبني تطبيق إدارة مهام كامل باستخدام SolidStart — الإطار الرسمي لـ SolidJS. في النهاية، سيكون لديك تطبيق يعمل بالكامل مع:
- توجيه مبني على الملفات مع تخطيطات متداخلة
- دوال الخادم باستخدام
"use server"لمنطق الواجهة الخلفية الآمن - تحميل بيانات تفاعلي باستخدام
queryوcreateAsync - تعديل البيانات باستخدام
actionلمعالجة النماذج - تخزين SQLite باستخدام
better-sqlite3 - دعم TypeScript الكامل
- عرض من جانب الخادم (SSR) مع البث
الوقت المطلوب: 60-90 دقيقة
المتطلبات الأساسية
قبل البدء، تأكد من وجود:
- Node.js 20+ — شغّل
node --versionللتحقق - معرفة أساسية بـ HTML وCSS وJavaScript
- إلمام بأُطر العمل التفاعلية (معرفة React أو Solid الأساسية مفيدة)
- محرر أكواد — VS Code مع إضافة Solid موصى به
لماذا SolidStart؟
ما هو SolidStart؟
SolidStart هو الإطار الرسمي الكامل لـ SolidJS. يجمع بين التفاعلية الدقيقة لـ Solid وبيئة تشغيل خادم قوية.
| الميزة | الوصف |
|---|---|
| تفاعلية دقيقة | يُحدّث فقط ما تغيّر — بدون مقارنة DOM افتراضي |
| توجيه مبني على الملفات | الصفحات تُعرَّف بهيكل نظام الملفات |
| دوال الخادم | تشغيل كود الخادم بتوجيه "use server" |
| أوضاع عرض متعددة | SSR وCSR وSSG والبث مدمجة |
| مبني على Vinxi | يعمل بـ Nitro وVite تحت الغطاء |
| انشر في أي مكان | إعدادات مسبقة لـ Vercel وNetlify وCloudflare وAWS |
كيف يقارن SolidStart؟
إذا استخدمت Next.js أو Nuxt أو SvelteKit، فإن SolidStart يؤدي نفس الدور لـ SolidJS. الفرق الرئيسي هو نموذج التفاعلية في Solid — الإشارات والتأثيرات بدلاً من DOM الافتراضي — مما يوفر أداء تشغيل استثنائي.
الخطوة 1: إنشاء مشروع SolidStart جديد
افتح الطرفية وشغّل:
npx create-solid@latest task-managerعند الطلب، اختر الخيارات التالية:
- هل هذا مشروع SolidStart؟ → نعم
- القالب →
basic - استخدام TypeScript؟ → نعم
انتقل إلى المشروع وثبّت التبعيات:
cd task-manager
npm installشغّل خادم التطوير:
npm run devزر http://localhost:3000 — يجب أن ترى صفحة الترحيب الافتراضية لـ SolidStart.
الخطوة 2: فهم هيكل المشروع
هذا ما أنشأه SolidStart لك:
task-manager/
├── public/ # الملفات الثابتة
├── src/
│ ├── routes/ # التوجيه المبني على الملفات
│ │ └── index.tsx # الصفحة الرئيسية (/)
│ ├── components/ # المكونات القابلة لإعادة الاستخدام
│ ├── app.tsx # غلاف التطبيق الجذري
│ ├── entry-client.tsx # نقطة دخول العميل
│ └── entry-server.tsx # نقطة دخول الخادم
├── app.config.ts # إعدادات SolidStart
├── tsconfig.json
└── package.json
الملفات الرئيسية
src/app.tsx — المكون الجذري الذي يغلف تطبيقك بالكامل. يُعدّ الموجّه ويُعرّف غلاف HTML:
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
export default function App() {
return (
<Router
root={(props) => (
<Suspense>{props.children}</Suspense>
)}
>
<FileRoutes />
</Router>
);
}src/entry-server.tsx — يتعامل مع العرض من جانب الخادم:
import { createHandler, StartServer } from "@solidjs/start/server";
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body>
<div id="app">{children}</div>
{scripts}
</body>
</html>
)}
/>
));الخطوة 3: إعداد قاعدة البيانات
ثبّت better-sqlite3 لقاعدة بيانات بسيطة مبنية على الملفات:
npm install better-sqlite3
npm install -D @types/better-sqlite3أنشئ ملف قاعدة البيانات في src/lib/db.ts:
import Database from "better-sqlite3";
import { join } from "path";
const db = new Database(join(process.cwd(), "tasks.db"));
// تفعيل وضع WAL لأداء أفضل
db.pragma("journal_mode = WAL");
// إنشاء جدول المهام
db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT DEFAULT '',
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)
`);
export { db };الخطوة 4: إنشاء دوال الخادم لعمليات CRUD
أنشئ src/lib/tasks.ts لتعريف جميع عمليات البيانات على الخادم:
import { action, query, redirect } from "@solidjs/router";
import { db } from "./db";
// الأنواع
export interface Task {
id: number;
title: string;
description: string;
completed: number;
created_at: string;
updated_at: string;
}
// ─── الاستعلامات ──────────────────────────────────────
export const getTasks = query(async (filter?: string) => {
"use server";
let sql = "SELECT * FROM tasks";
if (filter === "active") {
sql += " WHERE completed = 0";
} else if (filter === "completed") {
sql += " WHERE completed = 1";
}
sql += " ORDER BY created_at DESC";
return db.prepare(sql).all() as Task[];
}, "tasks");
export const getTask = query(async (id: number) => {
"use server";
return db.prepare("SELECT * FROM tasks WHERE id = ?").get(id) as Task | undefined;
}, "task");
export const getTaskStats = query(async () => {
"use server";
const total = db.prepare("SELECT COUNT(*) as count FROM tasks").get() as { count: number };
const completed = db.prepare("SELECT COUNT(*) as count FROM tasks WHERE completed = 1").get() as { count: number };
return {
total: total.count,
completed: completed.count,
active: total.count - completed.count,
};
}, "taskStats");
// ─── الإجراءات ──────────────────────────────────────
export const addTask = action(async (formData: FormData) => {
"use server";
const title = formData.get("title")?.toString().trim();
const description = formData.get("description")?.toString().trim() ?? "";
if (!title) {
throw new Error("العنوان مطلوب");
}
db.prepare("INSERT INTO tasks (title, description) VALUES (?, ?)").run(title, description);
throw redirect("/");
});
export const toggleTask = action(async (formData: FormData) => {
"use server";
const id = Number(formData.get("id"));
db.prepare(
"UPDATE tasks SET completed = CASE WHEN completed = 0 THEN 1 ELSE 0 END, updated_at = datetime('now') WHERE id = ?"
).run(id);
throw redirect("/");
});
export const deleteTask = action(async (formData: FormData) => {
"use server";
const id = Number(formData.get("id"));
db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
throw redirect("/");
});
export const updateTask = action(async (formData: FormData) => {
"use server";
const id = Number(formData.get("id"));
const title = formData.get("title")?.toString().trim();
const description = formData.get("description")?.toString().trim() ?? "";
if (!title) {
throw new Error("العنوان مطلوب");
}
db.prepare(
"UPDATE tasks SET title = ?, description = ?, updated_at = datetime('now') WHERE id = ?"
).run(title, description, id);
throw redirect(`/tasks/${id}`);
});شرح المفاهيم الرئيسية
query()— يغلف دالة الخادم لجلب البيانات. يتم تخزين النتائج مؤقتاً ويمكن تحميلها مسبقاً عند التنقلaction()— يغلف دالة الخادم للتعديلات (إنشاء، تحديث، حذف). تُعيد الإجراءات تحقق ذاكرة التخزين المؤقت تلقائياً"use server"— هذا التوجيه يخبر SolidStart بتشغيل الدالة حصرياً على الخادمthrow redirect("/")— بعد التعديل، إعادة التوجيه. في SolidStart، تستخدم عمليات إعادة التوجيه من الإجراءاتthrow
الخطوة 5: بناء التخطيط الجذري مع التنقل
حدّث src/app.tsx لتضمين شريط التنقل:
import { A, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import "./app.css";
export default function App() {
return (
<Router
root={(props) => (
<div class="app">
<header class="header">
<div class="container">
<A href="/" class="logo">
مدير المهام
</A>
<nav>
<A href="/" end>جميع المهام</A>
<A href="/tasks/new">مهمة جديدة</A>
</nav>
</div>
</header>
<main class="container">
<Suspense fallback={<div class="loading">جارٍ التحميل...</div>}>
{props.children}
</Suspense>
</main>
</div>
)}
>
<FileRoutes />
</Router>
);
}الخطوة 6: بناء الصفحة الرئيسية — قائمة المهام
استبدل محتوى src/routes/index.tsx بصفحة قائمة المهام:
import { For, Show } from "solid-js";
import { A, useSearchParams } from "@solidjs/router";
import { createAsync, type RouteDefinition } from "@solidjs/router";
import { getTasks, getTaskStats, toggleTask, deleteTask } from "~/lib/tasks";
export const route = {
preload: () => {
getTasks();
getTaskStats();
},
} satisfies RouteDefinition;
export default function Home() {
const [searchParams, setSearchParams] = useSearchParams();
const filter = () => searchParams.filter as string | undefined;
const tasks = createAsync(() => getTasks(filter()));
const stats = createAsync(() => getTaskStats());
return (
<div>
<div class="page-header">
<h1>مهامي</h1>
<A href="/tasks/new" class="btn btn-primary">
+ إضافة مهمة
</A>
</div>
<Show when={stats()}>
{(s) => (
<div class="stats">
<div class="stat">
<span class="stat-value">{s().total}</span>
<span class="stat-label">الإجمالي</span>
</div>
<div class="stat">
<span class="stat-value">{s().active}</span>
<span class="stat-label">نشطة</span>
</div>
<div class="stat">
<span class="stat-value">{s().completed}</span>
<span class="stat-label">مكتملة</span>
</div>
</div>
)}
</Show>
<div class="filters">
<button
class={`filter-btn ${!filter() ? "active" : ""}`}
onClick={() => setSearchParams({ filter: undefined })}
>
الكل
</button>
<button
class={`filter-btn ${filter() === "active" ? "active" : ""}`}
onClick={() => setSearchParams({ filter: "active" })}
>
نشطة
</button>
<button
class={`filter-btn ${filter() === "completed" ? "active" : ""}`}
onClick={() => setSearchParams({ filter: "completed" })}
>
مكتملة
</button>
</div>
<Show
when={tasks()?.length}
fallback={
<div class="empty-state">
<p>لا توجد مهام بعد. أنشئ مهمتك الأولى!</p>
<A href="/tasks/new" class="btn btn-primary">
إنشاء مهمة
</A>
</div>
}
>
<ul class="task-list">
<For each={tasks()}>
{(task) => (
<li class={`task-item ${task.completed ? "completed" : ""}`}>
<form action={toggleTask} method="post" class="toggle-form">
<input type="hidden" name="id" value={task.id} />
<button type="submit" class="checkbox" aria-label="تبديل المهمة">
{task.completed ? "✓" : ""}
</button>
</form>
<A href={`/tasks/${task.id}`} class="task-content">
<span class="task-title">{task.title}</span>
<Show when={task.description}>
<span class="task-desc">{task.description}</span>
</Show>
</A>
<form action={deleteTask} method="post">
<input type="hidden" name="id" value={task.id} />
<button type="submit" class="btn btn-danger btn-sm">
حذف
</button>
</form>
</li>
)}
</For>
</ul>
</Show>
</div>
);
}كيف يعمل تحميل البيانات
route.preload— عندما ينتقل المستخدم إلى هذه الصفحة، يبدأ SolidStart في جلب البيانات مبكراًcreateAsync— ينشئ مورداً تفاعلياً يُعلّق العرض حتى تصبح البيانات جاهزة<For>— مكون الحلقة المحسّن في Solid. يُعيد عرض العناصر المتغيرة فقط<Show>— عرض شرطي يتجنب إنشاء DOM غير ضروري
الخطوة 7: إنشاء صفحة المهمة الجديدة
أنشئ src/routes/tasks/new.tsx:
import { A } from "@solidjs/router";
import { addTask } from "~/lib/tasks";
export default function NewTask() {
return (
<div>
<A href="/" class="back-link">
→ العودة إلى المهام
</A>
<h1>إنشاء مهمة جديدة</h1>
<form action={addTask} method="post" class="task-form">
<div class="form-group">
<label for="title">العنوان *</label>
<input
type="text"
id="title"
name="title"
placeholder="ما الذي يجب القيام به؟"
required
autofocus
/>
</div>
<div class="form-group">
<label for="description">الوصف</label>
<textarea
id="description"
name="description"
placeholder="أضف تفاصيل (اختياري)"
rows="4"
/>
</div>
<div class="form-actions">
<A href="/" class="btn btn-danger">
إلغاء
</A>
<button type="submit" class="btn btn-primary">
إنشاء المهمة
</button>
</div>
</form>
</div>
);
}كيف تعمل النماذج في SolidStart
لاحظ أنه لا يوجد معالج onSubmit أو useState. يستخدم النموذج action HTML أصلي يشير إلى إجراء الخادم addTask. عند الإرسال:
- يعترض SolidStart إرسال النموذج
- يُسلسل
FormData - يرسله إلى دالة الخادم
- تعالج دالة الخادم البيانات وتُعيد التوجيه
- يتم إبطال جميع ذاكرة التخزين المؤقت لـ
queryتلقائياً
هذا هو التحسين التدريجي — النموذج يعمل حتى بدون تفعيل JavaScript.
الخطوة 8: إنشاء صفحة تفاصيل المهمة
أنشئ src/routes/tasks/[id].tsx — الأقواس تجعل id معامل مسار ديناميكي:
import { Show } from "solid-js";
import { A, useParams } from "@solidjs/router";
import { createAsync, type RouteDefinition } from "@solidjs/router";
import { getTask, toggleTask, deleteTask, updateTask } from "~/lib/tasks";
import { createSignal } from "solid-js";
export const route = {
preload: ({ params }) => getTask(Number(params.id)),
} satisfies RouteDefinition;
export default function TaskDetail() {
const params = useParams();
const task = createAsync(() => getTask(Number(params.id)));
const [editing, setEditing] = createSignal(false);
return (
<div>
<A href="/" class="back-link">
→ العودة إلى المهام
</A>
<Show when={task()} fallback={<p>المهمة غير موجودة.</p>}>
{(t) => (
<div class="task-detail">
<Show
when={!editing()}
fallback={
<form action={updateTask} method="post" class="edit-form">
<input type="hidden" name="id" value={t().id} />
<div class="form-group">
<label for="title">العنوان</label>
<input type="text" id="title" name="title" value={t().title} required />
</div>
<div class="form-group">
<label for="description">الوصف</label>
<textarea id="description" name="description" rows="4">
{t().description}
</textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-danger" onClick={() => setEditing(false)}>
إلغاء
</button>
<button type="submit" class="btn btn-primary">
حفظ التغييرات
</button>
</div>
</form>
}
>
<div class="detail-header">
<h1 class={t().completed ? "completed-title" : ""}>
{t().title}
</h1>
<div class="detail-actions">
<button class="btn btn-primary btn-sm" onClick={() => setEditing(true)}>
تعديل
</button>
<form action={toggleTask} method="post" style="display:inline">
<input type="hidden" name="id" value={t().id} />
<button type="submit" class="btn btn-sm" style={`background: ${t().completed ? "var(--border)" : "var(--success)"}; color: white;`}>
{t().completed ? "تنشيط" : "إكمال"}
</button>
</form>
<form action={deleteTask} method="post" style="display:inline">
<input type="hidden" name="id" value={t().id} />
<button type="submit" class="btn btn-danger btn-sm">
حذف
</button>
</form>
</div>
</div>
<Show when={t().description}>
<div class="detail-description">
<h3>الوصف</h3>
<p>{t().description}</p>
</div>
</Show>
<div class="detail-meta">
<p><strong>الحالة:</strong> <span class={`status ${t().completed ? "done" : "active"}`}>{t().completed ? "مكتملة" : "نشطة"}</span></p>
<p><strong>تاريخ الإنشاء:</strong> {new Date(t().created_at).toLocaleDateString("ar")}</p>
<p><strong>آخر تحديث:</strong> {new Date(t().updated_at).toLocaleDateString("ar")}</p>
</div>
</Show>
</div>
)}
</Show>
</div>
);
}شرح المسارات الديناميكية
[id].tsx— الأقواس تنشئ جزءاً ديناميكياً./tasks/42يربطidبـ"42"useParams()— يصل إلى المعاملات الديناميكية بشكل تفاعليcreateSignal— مكافئuseStateفي React عند Solid، لكن مع تفاعلية دقيقة
الخطوة 9: إضافة مسار API
يدعم SolidStart مسارات API لبناء نقاط نهاية REST. أنشئ src/routes/api/tasks.ts:
import type { APIEvent } from "@solidjs/start/server";
import { db } from "~/lib/db";
import type { Task } from "~/lib/tasks";
export async function GET(event: APIEvent) {
const url = new URL(event.request.url);
const filter = url.searchParams.get("filter");
let sql = "SELECT * FROM tasks";
if (filter === "active") sql += " WHERE completed = 0";
else if (filter === "completed") sql += " WHERE completed = 1";
sql += " ORDER BY created_at DESC";
const tasks = db.prepare(sql).all() as Task[];
return new Response(JSON.stringify(tasks), {
headers: { "Content-Type": "application/json" },
});
}
export async function POST(event: APIEvent) {
const body = await event.request.json();
if (!body.title?.trim()) {
return new Response(JSON.stringify({ error: "العنوان مطلوب" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const result = db
.prepare("INSERT INTO tasks (title, description) VALUES (?, ?)")
.run(body.title.trim(), body.description?.trim() ?? "");
const task = db.prepare("SELECT * FROM tasks WHERE id = ?").get(result.lastInsertRowid);
return new Response(JSON.stringify(task), {
status: 201,
headers: { "Content-Type": "application/json" },
});
}يمكنك الآن اختبار الـ API:
# جلب جميع المهام
curl http://localhost:3000/api/tasks
# إنشاء مهمة عبر API
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "تعلم SolidStart", "description": "بناء مدير مهام"}'الخطوة 10: إضافة معالجة الأخطاء
أنشئ صفحة حدود الخطأ في src/routes/*404.tsx:
import { A } from "@solidjs/router";
export default function NotFound() {
return (
<div class="not-found">
<h1>404</h1>
<p>الصفحة التي تبحث عنها غير موجودة.</p>
<A href="/" class="btn btn-primary">
العودة للرئيسية
</A>
</div>
);
}الخطوة 11: إعداد وبناء للإنتاج
حدّث app.config.ts:
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
server: {
preset: "node-server",
},
});ابنِ وشغّل:
npm run build
node .output/server/index.mjsالنشر على منصات أخرى
| المنصة | الإعداد المسبق |
|---|---|
| Vercel | vercel |
| Netlify | netlify |
| Cloudflare Pages | cloudflare-pages |
| AWS Lambda | aws-lambda |
| Node.js | node-server |
| ثابت (SSG) | static |
اختبار التطبيق
تحقق من أن كل شيء يعمل:
- إنشاء مهام — انتقل إلى
/tasks/new، املأ العنوان والوصف، أرسل - عرض المهام — الصفحة الرئيسية تعرض جميع المهام مع الإحصائيات
- تصفية المهام — انقر "نشطة" أو "مكتملة" للتصفية
- تبديل الإكمال — انقر مربع الاختيار لتحديد المهام
- عرض التفاصيل — انقر عنوان المهمة لرؤية صفحة التفاصيل
- تعديل المهام — في صفحة التفاصيل، انقر "تعديل"
- حذف المهام — انقر "حذف" لإزالة مهمة
- اختبار الـ API — استخدم
curlلاختبار/api/tasks
استكشاف الأخطاء
المشاكل الشائعة
"Cannot find module 'better-sqlite3'"
تأكد من تثبيته: npm install better-sqlite3 @types/better-sqlite3
"tasks.db is locked"
يحدث عند وصول عمليات متعددة لقاعدة البيانات. وضع WAL الذي فعّلناه يساعد، لكن تأكد من تشغيل خادم تطوير واحد فقط.
أخطاء TypeScript مع معاملات المسار
حوّل معاملات المسار دائماً: Number(params.id) — المعاملات دائماً سلاسل نصية.
مقارنة SolidStart مع أُطر العمل الأخرى
| الميزة | SolidStart | Next.js | SvelteKit | Nuxt |
|---|---|---|---|---|
| التفاعلية | إشارات دقيقة | DOM افتراضي | وقت التجميع | DOM افتراضي (Vue) |
| حجم الحزمة | ~7 KB | ~85 KB | ~15 KB | ~60 KB |
| دوال الخادم | "use server" | Server Actions | Form actions | مجلد server/ |
| تحميل البيانات | query + createAsync | fetch في RSC | دوال load | useFetch |
الخطوات التالية
بعد بناء تطبيق كامل مع SolidStart، إليك بعض الأفكار للتوسع:
- إضافة المصادقة — استخدم Clerk أو Lucia
- التبديل إلى PostgreSQL — استبدل SQLite بقاعدة بيانات إنتاجية باستخدام Drizzle ORM
- إضافة تحديثات فورية — استخدم WebSockets أو Server-Sent Events
- تطبيق السحب والإفلات — أضف إعادة ترتيب المهام
- النشر في الإنتاج — جرّب النشر على Vercel أو Cloudflare Pages
موارد مفيدة
الخلاصة
لقد بنيت مدير مهام كامل باستخدام SolidStart، وغطّيت:
- التوجيه المبني على الملفات مع المعاملات الديناميكية
- دوال الخادم باستخدام توجيه
"use server"للوصول الآمن للبيانات - تحميل البيانات التفاعلي مع
queryوcreateAsync - تعديلات مبنية على النماذج مع
actionوالتحسين التدريجي - مسارات API لنقاط نهاية REST
- تخزين SQLite للاستمرارية
- نشر الإنتاج مع إعدادات مسبقة قابلة للتكوين
يجلب SolidStart مزايا أداء التفاعلية الدقيقة لـ SolidJS إلى التطوير الكامل، مع تجربة مطور ممتازة. حجم حزمته الصغير وتشغيله السريع وواجهاته البرمجية البديهية تجعله خياراً مقنعاً لتطبيقات الويب الحديثة.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق ويب متكامل باستخدام Deno 2 وإطار عمل Fresh
تعلم كيفية بناء تطبيق إدارة مهام متكامل باستخدام Deno 2 وإطار عمل Fresh. يغطي هذا الدرس العملي بنية الجزر (Islands)، والعرض من جانب الخادم، ومسارات API، وقاعدة بيانات Deno KV.

بناء تطبيق ويب متكامل باستخدام SvelteKit 2: دليل عملي شامل
تعلم كيفية بناء تطبيق إدارة ملاحظات متكامل باستخدام SvelteKit 2. يغطي هذا الدرس العملي التوجيه القائم على الملفات، والعرض من جانب الخادم، وForm Actions، ومسارات API، والنشر على Vercel.

AI SDK 4.0: الميزات الجديدة وحالات الاستخدام
اكتشف الميزات الجديدة وحالات الاستخدام لـ AI SDK 4.0، بما في ذلك دعم PDF واستخدام الكمبيوتر والمزيد.