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

ما ستبنيه
في هذا الدرس، ستبني تطبيق إدارة ملاحظات متكامل باستخدام SvelteKit 2 — الإطار الرسمي لبناء تطبيقات Svelte. بنهاية الدرس، سيكون لديك تطبيق يعمل بالكامل يتضمن:
- توجيه قائم على الملفات مع تخطيطات متداخلة
- عرض من جانب الخادم (SSR) لأداء فائق السرعة
- Form Actions لمعالجة النماذج بدون JavaScript
- مسارات API لعمليات CRUD كاملة
- تخزين بيانات باستخدام SQLite عبر
better-sqlite3 - TypeScript مدمج بالكامل
- تصميم متجاوب مع CSS حديث
الوقت المطلوب: 60-90 دقيقة
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 18+ — شغّل
node --versionللتحقق - معرفة أساسية بـ HTML و CSS و JavaScript
- إلمام بأساسيات Svelte (مفيد لكن ليس ضرورياً)
- محرر أكواد (يُنصح بـ VS Code مع إضافة Svelte)
لماذا SvelteKit 2؟
ما هو SvelteKit؟
SvelteKit هو الإطار الرسمي لبناء تطبيقات ويب باستخدام Svelte. يجمع بين أفضل ميزات أطر العمل الحديثة:
| الميزة | الوصف |
|---|---|
| توجيه الملفات | هيكل المجلدات يحدد المسارات تلقائياً |
| SSR + CSR | عرض هجين من الخادم والعميل |
| Form Actions | معالجة النماذج بدون JavaScript على العميل |
| محولات النشر | نشر على أي منصة (Vercel، Node، Cloudflare) |
| أداء استثنائي | حجم حزم أصغر مقارنة بـ React/Next.js |
لماذا Svelte بدلاً من React؟
Svelte يعمل كمترجم (compiler) بدلاً من مكتبة وقت التشغيل. هذا يعني:
- حجم حزم أصغر بـ 40-70% مقارنة بـ React
- لا Virtual DOM — تحديثات مباشرة على DOM الحقيقي
- كود أقل — بنية أبسط وأوضح
- تفاعلية مدمجة — بدون
useStateأوuseEffect
الخطوة 1: إنشاء المشروع
ابدأ بإنشاء مشروع SvelteKit جديد:
npx sv create notes-appعند ظهور الخيارات، اختر:
┌ Welcome to the Svelte CLI!
│
◇ Which template would you like?
│ SvelteKit minimal
│
◇ Add type checking with TypeScript?
│ Yes, using TypeScript syntax
│
◇ What would you like to add to your project?
│ prettier, eslint
│
◇ Which package manager do you want to install dependencies with?
│ npm
│
└ You're all set!
ادخل إلى المجلد وشغّل الخادم:
cd notes-app
npm run devافتح http://localhost:5173 — سترى صفحة الترحيب الافتراضية.
الخطوة 2: فهم هيكل المشروع
notes-app/
├── src/
│ ├── lib/ # مكتبات ومكونات مشتركة
│ │ └── index.ts
│ ├── routes/ # صفحات التطبيق (توجيه الملفات)
│ │ └── +page.svelte
│ ├── app.html # قالب HTML الرئيسي
│ └── app.d.ts # أنواع TypeScript
├── static/ # ملفات ثابتة (صور، خطوط)
├── svelte.config.js # إعدادات SvelteKit
├── tsconfig.json
├── vite.config.ts
└── package.json
الملفات الخاصة في SvelteKit
| الملف | الوظيفة |
|---|---|
+page.svelte | واجهة الصفحة (UI) |
+page.server.ts | منطق الخادم (تحميل البيانات، Form Actions) |
+page.ts | تحميل بيانات عالمي (خادم + عميل) |
+layout.svelte | تخطيط مشترك يلف الصفحات |
+layout.server.ts | بيانات مشتركة للتخطيط |
+server.ts | مسار API (GET, POST, PUT, DELETE) |
+error.svelte | صفحة خطأ مخصصة |
الخطوة 3: إعداد قاعدة البيانات
ثبّت better-sqlite3 لإدارة البيانات:
npm install better-sqlite3
npm install -D @types/better-sqlite3أنشئ ملف إعداد قاعدة البيانات:
// src/lib/server/db.ts
import Database from 'better-sqlite3';
import { dev } from '$app/environment';
import path from 'path';
const dbPath = dev ? 'notes.db' : path.join(process.cwd(), 'notes.db');
const db = new Database(dbPath);
// تفعيل WAL mode لأداء أفضل
db.pragma('journal_mode = WAL');
// إنشاء جدول الملاحظات
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT '#ffffff',
pinned INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
export default db;أنشئ ملف الأنواع:
// src/lib/types.ts
export interface Note {
id: number;
title: string;
content: string;
color: string;
pinned: number;
created_at: string;
updated_at: string;
}الخطوة 4: بناء التخطيط الرئيسي
استبدل محتوى التخطيط الرئيسي:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
<div class="app">
<header>
<nav>
<a href="/" class="logo">📝 ملاحظاتي</a>
<div class="nav-links">
<a href="/">الرئيسية</a>
<a href="/notes">الملاحظات</a>
<a href="/about">حول التطبيق</a>
</div>
</nav>
</header>
<main>
{@render children()}
</main>
<footer>
<p>تم بناؤه باستخدام SvelteKit 2 © {new Date().getFullYear()}</p>
</footer>
</div>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: #f5f5f5;
color: #333;
direction: ltr;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: #1a1a2e;
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
nav {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: white;
text-decoration: none;
}
.nav-links {
display: flex;
gap: 1.5rem;
}
.nav-links a {
color: #a8a8b3;
text-decoration: none;
transition: color 0.2s;
}
.nav-links a:hover {
color: white;
}
main {
flex: 1;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
width: 100%;
box-sizing: border-box;
}
footer {
background: #1a1a2e;
color: #a8a8b3;
text-align: center;
padding: 1rem;
margin-top: auto;
}
</style>الخطوة 5: الصفحة الرئيسية
<!-- src/routes/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<svelte:head>
<title>ملاحظاتي — تطبيق SvelteKit</title>
</svelte:head>
<section class="hero">
<h1>مرحباً بك في تطبيق الملاحظات</h1>
<p>تطبيق متكامل مبني بـ SvelteKit 2 مع TypeScript و SQLite</p>
<a href="/notes" class="cta-button">ابدأ الآن →</a>
</section>
<section class="stats">
<div class="stat-card">
<span class="stat-number">{data.totalNotes}</span>
<span class="stat-label">ملاحظة</span>
</div>
<div class="stat-card">
<span class="stat-number">{data.pinnedNotes}</span>
<span class="stat-label">مثبتة</span>
</div>
<div class="stat-card">
<span class="stat-number">{data.recentNotes}</span>
<span class="stat-label">هذا الأسبوع</span>
</div>
</section>
<style>
.hero {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
color: white;
margin-bottom: 2rem;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.hero p {
font-size: 1.2rem;
opacity: 0.9;
margin-bottom: 2rem;
}
.cta-button {
display: inline-block;
padding: 0.8rem 2rem;
background: white;
color: #667eea;
border-radius: 8px;
text-decoration: none;
font-weight: bold;
font-size: 1.1rem;
transition: transform 0.2s;
}
.cta-button:hover {
transform: translateY(-2px);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.stat-card {
background: white;
padding: 2rem;
border-radius: 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.stat-number {
display: block;
font-size: 3rem;
font-weight: bold;
color: #667eea;
}
.stat-label {
display: block;
color: #666;
margin-top: 0.5rem;
}
</style>أنشئ ملف تحميل البيانات:
// src/routes/+page.server.ts
import db from '$lib/server/db';
export function load() {
const totalNotes = db.prepare('SELECT COUNT(*) as count FROM notes').get() as { count: number };
const pinnedNotes = db.prepare('SELECT COUNT(*) as count FROM notes WHERE pinned = 1').get() as { count: number };
const recentNotes = db.prepare(
"SELECT COUNT(*) as count FROM notes WHERE created_at >= datetime('now', '-7 days')"
).get() as { count: number };
return {
totalNotes: totalNotes.count,
pinnedNotes: pinnedNotes.count,
recentNotes: recentNotes.count
};
}الخطوة 6: صفحة قائمة الملاحظات
هذه هي الصفحة الأساسية — عرض وإضافة الملاحظات باستخدام Form Actions:
// src/routes/notes/+page.server.ts
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export function load() {
const notes = db.prepare(
'SELECT * FROM notes ORDER BY pinned DESC, updated_at DESC'
).all() as Note[];
return { notes };
}
export const actions: Actions = {
create: async ({ request }) => {
const formData = await request.formData();
const title = formData.get('title')?.toString().trim();
const content = formData.get('content')?.toString().trim() ?? '';
const color = formData.get('color')?.toString() ?? '#ffffff';
if (!title) {
return fail(400, { error: 'العنوان مطلوب', title, content, color });
}
if (title.length > 200) {
return fail(400, { error: 'العنوان طويل جداً (الحد الأقصى 200 حرف)', title, content, color });
}
db.prepare(
'INSERT INTO notes (title, content, color) VALUES (?, ?, ?)'
).run(title, content, color);
return { success: true };
},
delete: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id');
if (!id) {
return fail(400, { error: 'معرّف الملاحظة مطلوب' });
}
db.prepare('DELETE FROM notes WHERE id = ?').run(id);
return { success: true };
},
togglePin: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id');
if (!id) {
return fail(400, { error: 'معرّف الملاحظة مطلوب' });
}
db.prepare(
'UPDATE notes SET pinned = CASE WHEN pinned = 1 THEN 0 ELSE 1 END, updated_at = datetime(\'now\') WHERE id = ?'
).run(id);
return { success: true };
}
};<!-- src/routes/notes/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData; form: ActionData } = $props();
let showForm = $state(false);
const colors = [
{ value: '#ffffff', label: 'أبيض' },
{ value: '#fff3cd', label: 'أصفر' },
{ value: '#d1ecf1', label: 'أزرق' },
{ value: '#d4edda', label: 'أخضر' },
{ value: '#f8d7da', label: 'وردي' },
{ value: '#e2d9f3', label: 'بنفسجي' }
];
</script>
<svelte:head>
<title>الملاحظات — تطبيق SvelteKit</title>
</svelte:head>
<div class="notes-page">
<div class="page-header">
<h1>ملاحظاتي</h1>
<button class="add-btn" onclick={() => showForm = !showForm}>
{showForm ? '✕ إلغاء' : '+ ملاحظة جديدة'}
</button>
</div>
{#if showForm}
<form method="POST" action="?/create" use:enhance class="note-form">
{#if form?.error}
<div class="error">{form.error}</div>
{/if}
<input
type="text"
name="title"
placeholder="عنوان الملاحظة..."
value={form?.title ?? ''}
required
maxlength="200"
/>
<textarea
name="content"
placeholder="محتوى الملاحظة..."
rows="4"
>{form?.content ?? ''}</textarea>
<div class="color-picker">
<span>اللون:</span>
{#each colors as color}
<label class="color-option">
<input
type="radio"
name="color"
value={color.value}
checked={color.value === (form?.color ?? '#ffffff')}
/>
<span
class="color-swatch"
style="background: {color.value}"
title={color.label}
></span>
</label>
{/each}
</div>
<button type="submit" class="submit-btn">حفظ الملاحظة</button>
</form>
{/if}
{#if data.notes.length === 0}
<div class="empty-state">
<p>لا توجد ملاحظات بعد. أضف ملاحظتك الأولى!</p>
</div>
{:else}
<div class="notes-grid">
{#each data.notes as note (note.id)}
<div class="note-card" style="background: {note.color}">
<div class="note-header">
<h3>{note.title}</h3>
<div class="note-actions">
<form method="POST" action="?/togglePin" use:enhance>
<input type="hidden" name="id" value={note.id} />
<button type="submit" class="icon-btn" title={note.pinned ? 'إلغاء التثبيت' : 'تثبيت'}>
{note.pinned ? '📌' : '📍'}
</button>
</form>
<form method="POST" action="?/delete" use:enhance>
<input type="hidden" name="id" value={note.id} />
<button type="submit" class="icon-btn delete" title="حذف">🗑️</button>
</form>
</div>
</div>
{#if note.content}
<p class="note-content">{note.content}</p>
{/if}
<time class="note-date">
{new Date(note.updated_at).toLocaleDateString('ar-SA', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</time>
</div>
{/each}
</div>
{/if}
</div>
<style>
.notes-page {
max-width: 900px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.page-header h1 {
font-size: 2rem;
color: #1a1a2e;
}
.add-btn {
padding: 0.6rem 1.2rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
.add-btn:hover {
background: #5a6fd6;
}
.note-form {
background: white;
padding: 1.5rem;
border-radius: 12px;
margin-bottom: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 1rem;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 0.8rem;
border-radius: 6px;
font-size: 0.9rem;
}
input[type="text"],
textarea {
padding: 0.8rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
font-family: inherit;
}
input[type="text"]:focus,
textarea:focus {
outline: none;
border-color: #667eea;
}
.color-picker {
display: flex;
align-items: center;
gap: 0.8rem;
}
.color-option {
cursor: pointer;
}
.color-option input {
display: none;
}
.color-swatch {
display: inline-block;
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid #ddd;
transition: transform 0.2s;
}
.color-option input:checked + .color-swatch {
border-color: #667eea;
transform: scale(1.2);
}
.submit-btn {
padding: 0.8rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
}
.submit-btn:hover {
background: #5a6fd6;
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #999;
font-size: 1.1rem;
}
.notes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.note-card {
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.2s, box-shadow 0.2s;
}
.note-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.note-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.8rem;
}
.note-header h3 {
margin: 0;
font-size: 1.1rem;
color: #1a1a2e;
flex: 1;
}
.note-actions {
display: flex;
gap: 0.3rem;
}
.icon-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
padding: 0.2rem;
opacity: 0.6;
transition: opacity 0.2s;
}
.icon-btn:hover {
opacity: 1;
}
.note-content {
color: #555;
font-size: 0.95rem;
line-height: 1.5;
margin-bottom: 1rem;
}
.note-date {
font-size: 0.8rem;
color: #999;
}
</style>الخطوة 7: صفحة تعديل الملاحظة
أنشئ مسار صفحة التعديل مع معامل ديناميكي:
// src/routes/notes/[id]/+page.server.ts
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { error, fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(params.id) as Note | undefined;
if (!note) {
throw error(404, 'الملاحظة غير موجودة');
}
return { note };
};
export const actions: Actions = {
update: async ({ request, params }) => {
const formData = await request.formData();
const title = formData.get('title')?.toString().trim();
const content = formData.get('content')?.toString().trim() ?? '';
const color = formData.get('color')?.toString() ?? '#ffffff';
if (!title) {
return fail(400, { error: 'العنوان مطلوب' });
}
db.prepare(
"UPDATE notes SET title = ?, content = ?, color = ?, updated_at = datetime('now') WHERE id = ?"
).run(title, content, color, params.id);
redirect(303, '/notes');
}
};<!-- src/routes/notes/[id]/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData; form: ActionData } = $props();
const colors = [
{ value: '#ffffff', label: 'أبيض' },
{ value: '#fff3cd', label: 'أصفر' },
{ value: '#d1ecf1', label: 'أزرق' },
{ value: '#d4edda', label: 'أخضر' },
{ value: '#f8d7da', label: 'وردي' },
{ value: '#e2d9f3', label: 'بنفسجي' }
];
</script>
<svelte:head>
<title>تعديل: {data.note.title}</title>
</svelte:head>
<div class="edit-page">
<h1>تعديل الملاحظة</h1>
<form method="POST" action="?/update" use:enhance class="edit-form">
{#if form?.error}
<div class="error">{form.error}</div>
{/if}
<label>
العنوان
<input type="text" name="title" value={data.note.title} required maxlength="200" />
</label>
<label>
المحتوى
<textarea name="content" rows="8">{data.note.content}</textarea>
</label>
<div class="color-picker">
<span>اللون:</span>
{#each colors as color}
<label class="color-option">
<input
type="radio"
name="color"
value={color.value}
checked={color.value === data.note.color}
/>
<span class="color-swatch" style="background: {color.value}" title={color.label}></span>
</label>
{/each}
</div>
<div class="form-actions">
<a href="/notes" class="cancel-btn">إلغاء</a>
<button type="submit" class="save-btn">حفظ التعديلات</button>
</div>
</form>
</div>
<style>
.edit-page {
max-width: 700px;
margin: 0 auto;
}
.edit-page h1 {
color: #1a1a2e;
margin-bottom: 1.5rem;
}
.edit-form {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 1.2rem;
}
label {
display: flex;
flex-direction: column;
gap: 0.4rem;
font-weight: 500;
color: #333;
}
input[type="text"],
textarea {
padding: 0.8rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
}
input[type="text"]:focus,
textarea:focus {
outline: none;
border-color: #667eea;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 0.8rem;
border-radius: 6px;
}
.color-picker {
display: flex;
align-items: center;
gap: 0.8rem;
}
.color-option input { display: none; }
.color-swatch {
display: inline-block;
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid #ddd;
cursor: pointer;
transition: transform 0.2s;
}
.color-option input:checked + .color-swatch {
border-color: #667eea;
transform: scale(1.2);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1rem;
}
.cancel-btn {
padding: 0.8rem 1.5rem;
color: #666;
text-decoration: none;
border-radius: 8px;
border: 2px solid #e0e0e0;
}
.save-btn {
padding: 0.8rem 1.5rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
}
.save-btn:hover {
background: #5a6fd6;
}
</style>الخطوة 8: مسارات API
أنشئ مسارات API RESTful للتعامل مع الملاحظات برمجياً:
// src/routes/api/notes/+server.ts
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { json } from '@sveltejs/kit';
export function GET({ url }) {
const search = url.searchParams.get('search');
const pinned = url.searchParams.get('pinned');
let query = 'SELECT * FROM notes';
const conditions: string[] = [];
const params: unknown[] = [];
if (search) {
conditions.push('(title LIKE ? OR content LIKE ?)');
params.push(`%${search}%`, `%${search}%`);
}
if (pinned === 'true') {
conditions.push('pinned = 1');
}
if (conditions.length > 0) {
query += ' WHERE ' + conditions.join(' AND ');
}
query += ' ORDER BY pinned DESC, updated_at DESC';
const notes = db.prepare(query).all(...params) as Note[];
return json(notes);
}
export async function POST({ request }) {
const { title, content, color } = await request.json();
if (!title?.trim()) {
return json({ error: 'العنوان مطلوب' }, { status: 400 });
}
const result = db.prepare(
'INSERT INTO notes (title, content, color) VALUES (?, ?, ?)'
).run(title.trim(), content?.trim() ?? '', color ?? '#ffffff');
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(result.lastInsertRowid) as Note;
return json(note, { status: 201 });
}// src/routes/api/notes/[id]/+server.ts
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { json, error } from '@sveltejs/kit';
export function GET({ params }) {
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(params.id) as Note | undefined;
if (!note) {
throw error(404, 'الملاحظة غير موجودة');
}
return json(note);
}
export async function PUT({ params, request }) {
const { title, content, color } = await request.json();
if (!title?.trim()) {
return json({ error: 'العنوان مطلوب' }, { status: 400 });
}
const existing = db.prepare('SELECT id FROM notes WHERE id = ?').get(params.id);
if (!existing) {
throw error(404, 'الملاحظة غير موجودة');
}
db.prepare(
"UPDATE notes SET title = ?, content = ?, color = ?, updated_at = datetime('now') WHERE id = ?"
).run(title.trim(), content?.trim() ?? '', color ?? '#ffffff', params.id);
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(params.id) as Note;
return json(note);
}
export function DELETE({ params }) {
const existing = db.prepare('SELECT id FROM notes WHERE id = ?').get(params.id);
if (!existing) {
throw error(404, 'الملاحظة غير موجودة');
}
db.prepare('DELETE FROM notes WHERE id = ?').run(params.id);
return json({ success: true });
}الخطوة 9: صفحة الخطأ المخصصة
<!-- src/routes/+error.svelte -->
<script lang="ts">
import { page } from '$app/state';
</script>
<div class="error-page">
<h1>{page.status}</h1>
<p>{page.error?.message ?? 'حدث خطأ غير متوقع'}</p>
<a href="/">العودة للرئيسية</a>
</div>
<style>
.error-page {
text-align: center;
padding: 4rem 2rem;
}
.error-page h1 {
font-size: 6rem;
color: #667eea;
margin-bottom: 0.5rem;
}
.error-page p {
font-size: 1.3rem;
color: #666;
margin-bottom: 2rem;
}
.error-page a {
padding: 0.8rem 2rem;
background: #667eea;
color: white;
border-radius: 8px;
text-decoration: none;
}
</style>الخطوة 10: Hooks و Middleware
SvelteKit يوفر نظام hooks قوي لمعالجة الطلبات:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
// قياس وقت الاستجابة
const start = Date.now();
const response = await resolve(event);
const duration = Date.now() - start;
console.log(`${event.request.method} ${event.url.pathname} — ${duration}ms`);
// إضافة headers أمان
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
return response;
};الخطوة 11: التحميل التدريجي مع Streaming
ميزة متقدمة في SvelteKit — يمكنك بث البيانات البطيئة تدريجياً:
// src/routes/notes/+page.server.ts (نسخة محسّنة)
import db from '$lib/server/db';
import type { Note } from '$lib/types';
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';
export function load() {
// البيانات السريعة تُحمّل فوراً
const notes = db.prepare(
'SELECT * FROM notes ORDER BY pinned DESC, updated_at DESC'
).all() as Note[];
// البيانات البطيئة تُبث لاحقاً (مثال: إحصائيات معقدة)
const stats = new Promise<{ wordCount: number }>((resolve) => {
const result = db.prepare(
"SELECT SUM(LENGTH(content) - LENGTH(REPLACE(content, ' ', '')) + 1) as wordCount FROM notes"
).get() as { wordCount: number | null };
resolve({ wordCount: result.wordCount ?? 0 });
});
return {
notes,
streamed: { stats }
};
}
// ... actions تبقى كما هيواستخدام البيانات المبثوثة في الواجهة:
{#await data.streamed.stats}
<p class="loading">جاري حساب الإحصائيات...</p>
{:then stats}
<p class="word-count">إجمالي الكلمات: {stats.wordCount}</p>
{/await}الخطوة 12: إعداد النشر
النشر على Node.js
npm install @sveltejs/adapter-node// svelte.config.js
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
out: 'build',
precompress: true
})
}
};
export default config;# بناء ونشر
npm run build
node buildالنشر على Vercel
# لا حاجة لتغيير المحول — المحول الافتراضي يعمل مع Vercel
npx vercelالنشر عبر Docker
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/build ./build
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "build"]اختبار التطبيق
تشغيل التطبيق محلياً
npm run devاختبار المسارات
- الصفحة الرئيسية:
http://localhost:5173— تعرض الإحصائيات - الملاحظات:
http://localhost:5173/notes— أضف وعدّل واحذف - API:
curl http://localhost:5173/api/notes— ترجع JSON
اختبار Form Actions
Form Actions تعمل بدون JavaScript! جرّب تعطيل JS في المتصفح — النماذج ستستمر بالعمل لأنها تُعالج على الخادم.
استكشاف الأخطاء
خطأ: Cannot find module '$lib/server/db'
تأكد من إنشاء المجلد src/lib/server/ وأن الملف db.ts موجود فيه.
خطأ: better-sqlite3 لا يعمل
npm rebuild better-sqlite3صفحة 404 للمسارات الديناميكية
تأكد من أن اسم المجلد يحتوي على أقواس مربعة: [id] وليس id.
البيانات لا تتحدث في الصفحة
تحقق من أن use:enhance مضاف إلى عنصر <form> — بدونه ستُعاد تحميل الصفحة بالكامل.
ما تعلمته
في هذا الدرس، تعلمت كيفية:
- إنشاء مشروع SvelteKit 2 من الصفر
- نظام التوجيه القائم على الملفات مع المعاملات الديناميكية
- Form Actions لمعالجة النماذج بأمان على الخادم
- تحميل البيانات باستخدام وظائف
load - مسارات API مع GET و POST و PUT و DELETE
- بث البيانات (Streaming) للتحميل التدريجي
- Hooks لمعالجة الطلبات وإضافة middleware
- النشر على Node.js و Vercel و Docker
الخطوات التالية
- المصادقة: أضف تسجيل دخول باستخدام Lucia Auth
- قاعدة بيانات إنتاجية: استبدل SQLite بـ PostgreSQL عبر Drizzle ORM
- اختبارات: أضف اختبارات E2E باستخدام Playwright
- PWA: حوّل التطبيق إلى تطبيق ويب تقدمي
- الوثائق الرسمية: svelte.dev/docs/kit
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

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

بناء تطبيق ويب كامل باستخدام SolidStart: دليل عملي شامل
تعلّم كيفية بناء تطبيق إدارة مهام كامل باستخدام SolidStart وSolidJS. يغطي هذا الدليل التوجيه المبني على الملفات، ودوال الخادم، وتحميل البيانات التفاعلي، والإجراءات، والنشر.

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