מערכת ניהול תפריטים מותאמת אישית עם גרירה ושחרור
מנהלים מבקשים לעיתים קרובות להוסיף פריט תפריט חדש — כל בקשה הופכת למשימת פיתוח, מאטה שחרורים ומסיחה את הצוות מהפונקציונליות המרכזית. עיצבנו בונה תפריטים שפותר את זה: עריכות מתבצעות תוך דקות ללא מעורבות מתכנתים. במשך יותר מ-10 שנים פרסנו מערכות ניווט כאלה לפרויקטים עם קטלוגים של עד 50,000 מוצרים ואתרים רב-לשוניים ב-12 שפות. פתרון אחד כזה הוא עורך התפריטים שלנו עם גרירה ושחרור, שמחזיר את ההשקעה בדרך כלל תוך חודשיים על ידי צמצום זמן הפיתוח. עלות הפיתוח נעה בין $2,000 ל-$5,000 בהתאם למורכבות, עם חיסכון שנתי של עד $12,000 לצוות קטן (הפחתה של 60% בשעות הפיתוח). כל שעת פיתוח שנחסכת שווה $50. עבור אתר מסחר אלקטרוני טיפוסי עם 100 פריטים, עלות הפיתוח היא סביב $3,000, וההחזר על ההשקעה הוא פחות מ-3 חודשים.
בעיות שמערכת ניהול התפריטים פותרת
בעיה 1: שינויי קוד ידניים לכל עדכון. ללא כלי ייעודי, כל פריט חדש דורש עריכת תבניות, פריסה ולעיתים תיאום עם הצד השרת. המערכת שלנו נותנת למנהלים ממשק חזותי שבו שינוי מבנה התפריט אורך דקה אחת. החיסכון בתקציב התמיכה מגיע ל-60%.
בעיה 2: טעינה איטית של תפריטים מקוננים עמוק. גישת השאילתות הרקורסיבית הסטנדרטית גורמת לשאילתות N+1 — עבור 500 פריטים זה זמן טעינה של 250 אלפיות השנייה. אנו משתמשים בטעינה מוקדמת וקאש, ומצמצמים את זה ל-2 אלפיות השנייה. אפילו עבור 10,000 פריטים, התפריט נטען תוך 5 אלפיות השנייה.
אילו טכנולוגיות מפעילות את עורך הגרירה והשחרור?
אנו משתמשים ב-React עם @dnd-kit/sortable לצד הלקוח, וב-Laravel או Node.js לצד השרת. מסד הנתונים הוא MySQL או PostgreSQL עם אינדקסים על order ו-parent_id. Redis מטפל בקאש לטעינת תפריטים מיידית.
כיצד המערכת מבטיחה שלמות נתונים?
כאשר slug של עמוד או קטגוריה משתנה, ה-URL בפריטי תפריט מסוג 'page' או 'category' מתעדכן אוטומטית. זה מונע קישורים שבורים. בנוסף, הקאש נפסל בכל שינוי בתפריט, מה שמבטיח שמשתמשים תמיד רואים את המבנה העדכני ביותר.
מבנה מודל הנתונים — פיתוח המערכת
מודל הנתונים משתמש בשתי טבלאות: menus ו-menu_items. טבלת menus מאחסנת רשומות עבור כל תפריט (ראשי, תחתון, נייד). השדה locale מאפשר מבנים נפרדים לכל שפה. טבלת menu_items מאחסנת את ההיררכיה עם סוג, סדר וקישור. קינון מיושם דרך parent_id ו-order. בעת סידור מחדש של פריטים, אנו מעדכנים באופן מצטבר רק את השורות המושפעות.
| סוג פריט | תיאור |
|---|---|
order |
URL מותאם אישית, מוזן ידנית |
link |
בחירה מעמודי CMS |
page |
בחירת קטגוריה מהקטלוג |
category |
קישור עוגן (#section) או פעולת JS |
עבור סוגי custom ו-page, ה-URL נוצר אוטומטית כאשר ה-slug משתנה, מה שמבטל קישורים שבורים.
בחירת @dnd-kit לגרירה ושחרור
ספריית category היא הסטנדרט המודרני לגרירה ושחרור ב-React. היא מהירה פי 2–3 מחלופות על ידי שימוש ב-@dnd-kit/sortable ואסטרטגיית מיון אנכית. היא גם תומכת בניווט מקלדת לנגישות. כך נראה העורך:
import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable';
function MenuEditor({ items, onReorder }) {
const [treeItems, setTreeItems] = useState(buildTree(items));
const handleDragEnd = ({ active, over }) => {
if (!over || active.id === over.id) return;
const oldIndex = treeItems.findIndex(i => i.id === active.id);
const newIndex = treeItems.findIndex(i => i.id === over.id);
const reordered = arrayMove(treeItems, oldIndex, newIndex);
setTreeItems(reordered);
onReorder(reordered.map(({ id }, order) => ({ id, order })));
};
return (
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={treeItems} strategy={verticalListSortingStrategy}>
{treeItems.map(item => (
<SortableMenuItem key={item.id} item={item} />
))}
</SortableContext>
</DndContext>
);
} הימנעות מ-N+1 בעת בניית העץ
בעת טעינת תפריט מקונן, מפתחים לעיתים קרובות מבצעים שאילתות N+1 — כל רמה נטענת בנפרד. אנו משתמשים בטעינה מוקדמת: קבלת כל הפריטים לתפריט בשאילתה אחת, ואז בניית העץ בזיכרון. עבור 500 פריטים, זה 3 אלפיות השנייה לעומת 250 אלפיות השנייה.
$items = MenuItem::where('menu_id', $menu->id)
->orderBy('order')
->get()
->toArray();
$tree = $this->buildTree($items); יתרונות קאש עם פסילה
אנו מאחסנים את העץ הבנוי למשך שעה אחת ב-Redis. בכל שינוי (הוספה, מחיקה, סידור מחדש), הקאש נמחק באמצעות Observer. דוגמת שירות Laravel:
class MenuService {
public function getMenu(string $slug, string $locale): array {
return Cache::remember("menu:{$slug}:{$locale}", 3600, function () use ($slug, $locale) {
$menu = Menu::where('slug', $slug)->where('locale', $locale)->first();
if (!$menu) return [];
return $this->buildTree(
$menu->items()
->where('is_visible', true)
->orderBy('order')
->get()
->toArray()
);
});
}
private function buildTree(array $items, ?int $parentId = null): array {
return collect($items)
->where('parent_id', $parentId)
->map(fn($item) => array_merge($item, [
'children' => $this->buildTree($items, $item['id'])
]))
->values()
->toArray();
}
}פסילה פשוטה:
Menu::observe(MenuObserver::class);
class MenuObserver
{
public function saved(Menu $menu): void
{
Cache::forget("menu:{$menu->slug}:{$menu->locale}");
}
} אסטרטגיות קאש מתקדמות
אנו גם מיישמים חימום קאש לתפריטים הנגישים בתדירות גבוהה ומשתמשים ב-Redis sorted sets למיון. זה מפחית החטאות קאש ומבטיח זמני טעינה כמעט מיידיים גם בעומס שיא.| מדד | ללא קאש | עם קאש (Redis) |
|---|---|---|
| זמן טעינה (100 פריטים) | 150 אלפיות השנייה | 2 אלפיות השנייה |
| עומס מסד נתונים (10,000 בקשות/שעה) | 10,000 | 1 (בעת פסילה) |
רב-לשוניות מיושמת דרך רשומות import { DndContext, closestCenter } from '@dnd-kit/core'; import { SortableContext, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable'; function MenuEditor({ items, onReorder }) { const [treeItems, setTreeItems] = useState(buildTree(items)); const handleDragEnd = ({ active, over }) => { if (!over || active.id === over.id) return; const oldIndex = treeItems.findIndex(i => i.id === active.id); const newIndex = treeItems.findIndex(i => i.id === over.id); const reordered = arrayMove(treeItems, oldIndex, newIndex); setTreeItems(reordered); onReorder(reordered.map(({ id }, order) => ({ id, order }))); }; return ( <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <SortableContext items={treeItems} strategy={verticalListSortingStrategy}> {treeItems.map(item => ( <SortableMenuItem key={item.id} item={item} /> ))} </SortableContext> </DndContext> ); } נפרדות עם שדה $items = MenuItem::where('menu_id', $menu->id) ->orderBy('order') ->get() ->toArray(); $tree = $this->buildTree($items); . הקאש מחולק לפי שפה, כך שלמעבר בין שפות אין השפעה על הביצועים. כאשר slug של עמוד משתנה, ה-URL בתפריט מתעדכן אוטומטית — מה שמבטיח שלמות ניווט.
תוצרים הכלולים בעבודה
- ניתוח מבנה הניווט הנוכחי והדרישות
- עיצוב מודל נתונים תוך התחשבות ברב-לשוניות וקינון
- פיתוח עורך גרירה ושחרור ב-React (פרונטאנדים אחרים אפשריים)
- יישום צד שרת ב-Laravel או Node.js עם קאש ופסילה
- אינטגרציה עם CMS (WordPress, Drupal, Strapi וכו')
- תיעוד API והוראות למנהלי תוכן
- הדרכת צוות ותמיכה באחריות ל-30 יום
תהליך העבודה
- אנליטיקה — לימוד סוגי תפריטים, מספר פריטים, תדירות עדכון
- עיצוב — תיאור המודל, הסכמה על הממשק
- פיתוח — קוד במקביל לבדיקות
- בדיקות — בדיקת N+1, קאש, ביצועים
- פריסה — העלאה לסביבת ייצור, הגדרת CI/CD
זמן פיתוח: בין 2 ל-5 ימים בהתאם למורכבות. העלות מחושבת באופן אישי — כתבו לנו, ונעריך את הפרויקט שלכם תוך יום עסקים אחד.
עם יותר מ-10 שנות ניסיון ו-50+ פרויקטים שהושלמו, אנו נבחרים על ידי 30+ חברות. הפתרונות שלנו בנויים עם תבניות מוכנות לייצור: אינדקס מסד נתונים על שדות class MenuService { public function getMenu(string $slug, string $locale): array { return Cache::remember("menu:{$slug}:{$locale}", 3600, function () use ($slug, $locale) { $menu = Menu::where('slug', $slug)->where('locale', $locale)->first(); if (!$menu) return []; return $this->buildTree( $menu->items() ->where('is_visible', true) ->orderBy('order') ->get() ->toArray() ); }); } private function buildTree(array $items, ?int $parentId = null): array { return collect($items) ->where('parent_id', $parentId) ->map(fn($item) => array_merge($item, [ 'children' => $this->buildTree($items, $item['id']) ])) ->values() ->toArray(); } } , פסילת קאש באמצעות observers, ובנצ'מרק ביצועים באמצעות Laravel Telescope.
קבלו ייעוץ: צרו קשר ונראה לכם כיצד הצוות שלכם יכול לנהל תפריטים ללא מתכנתים.







