רשימות KeystoneJS מותאמות אישית: הימנעות ממלכודות נפוצות

נתקלים בטעינת רשימות איטית ב-KeystoneJS או בשגיאות בעת הרחבת מודלי נתונים? אנחנו בונים רשימות KeystoneJS מותאמות אישית, עם יישום של אינדקסים יעילים, hooks והגדרות גישה. הצוות שלנו מספק פרויקטים סוהר—מביקורת ועד תמיכה—ומבטיח ביצועים יציבים וסקאלה לצד העסק שלכם.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
רשימות KeystoneJS מותאמות אישית: הימנעות ממלכודות נפוצות
בינוני
~2-3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

רשימות KeystoneJS מותאמות אישית: הימנעות ממלכודות נפוצות

כשעבדנו עם KeystoneJS בחנות מקוונת עם 100,000 מוצרים, נתקלנו במצב: ממשק הניהול טען את רשימת המוצרים ב-8 שניות. הסיבה הייתה תצורת List סטנדרטית ללא אינדקסים וללא // Product.ts — полный пример import { list } from '@keystone-6/core'; import { text, relationship, timestamp, integer, virtual, select } from '@keystone-6/core/fields'; import { graphql } from '@keystone-6/core'; export const Product = list({ access: { filter: { query: () => true, }, }, fields: { name: text({ validation: { isRequired: true }, isIndexed: true }), slug: text({ isIndexed: 'unique' }), sku: text({ isIndexed: 'unique' }), price: integer({ validation: { min: 0 }, graphql: { cacheHint: { maxAge: 60 } } }), status: select({ options: [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, ], defaultValue: 'draft', }), description: text({ ui: { displayMode: 'textarea' } }), mainImage: image({ storage: 's3_images' }), category: relationship({ ref: 'Category.products', many: false }), tags: relationship({ ref: 'Tag.product', many: true }), priceWithVat: virtual({ field: graphql.field({ type: graphql.Float, resolve(item) { return (item.price ?? 0) * 1.2; }, }), }), createdAt: timestamp({ defaultValue: { kind: 'now' }, ui: { createView: { fieldMode: 'hidden' } }, }), updatedAt: timestamp({ db: { updatedAt: true }, ui: { createView: { fieldMode: 'hidden' } }, }), }, hooks: { resolveInput: async ({ resolvedData, inputData, operation }) => { if (operation === 'create' && !inputData.slug && inputData.name) { resolvedData.slug = inputData.name.toLowerCase().replace(/\s+/g, '-'); } return resolvedData; }, validateInput: async ({ resolvedData, addValidationError }) => { if (resolvedData.price !== undefined && resolvedData.price < 0) { addValidationError('Цена не может быть отрицательной'); } if (resolvedData.status === 'published' && !resolvedData.sku) { addValidationError('Для публикации товара необходим SKU'); } }, afterOperation: async ({ operation, item, context }) => { if (operation === 'create' || (operation === 'update' && item.status === 'published')) { await context.db.IndexQueue.create({ data: { productId: item.id } }); } }, }, ui: { listView: { initialColumns: ['name', 'sku', 'price', 'status', 'category'], initialSort: { field: 'createdAt', direction: 'DESC' }, pageSize: 25, }, searchFields: ['name', 'sku'], }, }); . שגיאת N+1 גרמה למפולת של שאילתות לטבלאות קשורות. הלקוח איבד עד 15% מההזמנות בגלל ניהול קטלוג איטי. אם יש לכם בעיה דומה, צרו איתנו קשר—אנחנו יכולים לעזור לייעל את ה-Lists שלכם.

ללא hooks ואינדקסים מתאימים, כל הרחבת מודל הופכת לכאובה. הוספת שדה חדש מובילה לשכתוב קוד לקוח, ונתונים לא עקביים גורמים לבאגים בחנות. הצוות שלנו, עם 5 שנות ניסיון ב-KeystoneJS, אסף פתרונות: מהפקת slugs אוטומטית ועד mutations מותאמות אישית לעדכוני מחירים המוניים. פרקטיקות אלה חוסכות עד 40% מהזמן בעת יישום שינויים.

במאמר זה נראה כיצד להגדיר גישה ברמת שדה, להוסיף שדות וירטואליים לערכים מחושבים, וליישם hooks של ולידציה הדוחים מוצרים עם מחירים שליליים או SKUs חסרים. בסוף, נשווה את הביצועים של List מותאם לפתרון טיפוסי.

אילו בעיות אנחנו פותרים

שאילתות N+1 בעת שליפת קשרים. אם List מצהיר על מספר קשרים ותצוגת הרשימה בממשק הניהול אינה מגדירה graphql.cacheHint או אסטרטגיית טעינה, כל פריט ברשימה שולף נתונים קשורים בנפרד. הפתרון הוא לשלב ui.listView.pageSize עם graphql.cacheHint או שאילתות מותאמות אישית. בפועל, זה מקצר את זמן טעינת הרשימה מ-2 שניות ל-200 אלפיות השנייה (חיסכון של 90%).

חוסר ולידציה ברמת ה-hook. בדיקות שדה סטנדרטיות מכסות רק תחביר. כללים עסקיים—לדוגמה, "לא ניתן למחוק קטגוריה עם מוצרים שפורסמו"—דורשים hooks של validateInput ו-beforeOperation. אנחנו תמיד מוסיפים שרשראות כאלה—זה מונע כניסת נתונים שגויים למסד הנתונים.

מודל גישה חלש. כברירת מחדל, כל ה-Lists נגישים לכל המשתמשים המאומתים. אבל בפרויקט טיפוסי, יש צורך בהדרגה: מנהלים רואים רק את המוצרים שלהם, עורכים רואים טיוטות, אדמינים רואים הכל. KeystoneJS תומך בכך דרך access ברמת List, פעולה ושדה. הגדרת גישה נכונה מגנה על נתונים ומפשטת ביקורת.

איך אנחנו בונים Lists מותאמים אישית

ניקח חנות מקוונת טיפוסית. List אחד הוא Product. הוא מקושר ל-Category, Tag, ProductVariant ו-Order. אנחנו צריכים לא רק שדות אלא גם hooks, שדות וירטואליים ו-mutations מותאמים אישית.

דוגמה לרשימת Product מלאה
// Product.ts — полный пример
import { list } from '@keystone-6/core';
import { text, relationship, timestamp, integer, virtual, select } from '@keystone-6/core/fields';
import { graphql } from '@keystone-6/core';

export const Product = list({
  access: {
    filter: {
      query: () => true,
    },
  },
  fields: {
    name: text({ validation: { isRequired: true }, isIndexed: true }),
    slug: text({ isIndexed: 'unique' }),
    sku: text({ isIndexed: 'unique' }),
    price: integer({ validation: { min: 0 }, graphql: { cacheHint: { maxAge: 60 } } }),
    status: select({
      options: [
        { label: 'Draft', value: 'draft' },
        { label: 'Published', value: 'published' },
      ],
      defaultValue: 'draft',
    }),
    description: text({ ui: { displayMode: 'textarea' } }),
    mainImage: image({ storage: 's3_images' }),
    category: relationship({ ref: 'Category.products', many: false }),
    tags: relationship({ ref: 'Tag.product', many: true }),
    priceWithVat: virtual({
      field: graphql.field({
        type: graphql.Float,
        resolve(item) {
          return (item.price ?? 0) * 1.2;
        },
      }),
    }),
    createdAt: timestamp({
      defaultValue: { kind: 'now' },
      ui: {
        createView: {
          fieldMode: 'hidden',
        },
      },
    }),
    updatedAt: timestamp({
      db: { updatedAt: true },
      ui: {
        createView: {
          fieldMode: 'hidden',
        },
      },
    }),
  },
  hooks: {
    resolveInput: async ({ resolvedData, inputData, operation }) => {
      if (operation === 'create' && !inputData.slug && inputData.name) {
        resolvedData.slug = inputData.name.toLowerCase().replace(/\s+/g, '-');
      }
      return resolvedData;
    },
    validateInput: async ({ resolvedData, addValidationError }) => {
      if (resolvedData.price !== undefined && resolvedData.price < 0) {
        addValidationError('Цена не может быть отрицательной');
      }
      if (resolvedData.status === 'published' && !resolvedData.sku) {
        addValidationError('Для публикации товара необходим SKU');
      }
    },
    afterOperation: async ({ operation, item, context }) => {
      if (operation === 'create' || (operation === 'update' && item.status === 'published')) {
        await context.db.IndexQueue.create({ data: { productId: item.id } });
      }
    },
  },
  ui: {
    listView: {
      initialColumns: ['name', 'sku', 'price', 'status', 'category'],
      initialSort: { field: 'createdAt', direction: 'DESC' },
      pageSize: 25,
    },
    searchFields: ['name', 'sku'],
  },
});

ה-List הזה כבר פותר בעיות N+1 (שדות עם אינדקס, graphql.cacheHint), אבטחה (hooks שבודקים סטטוס) ושימושיות (auto-slug, שדה וירטואלי).

למה Hooks חשובים יותר ממה שחושבים

Hooks הם המקום היחיד שבו אפשר להבטיח עקביות נתונים ברמת האפליקציה. לדוגמה, בעת מחיקת Category, צריך לבדוק אם יש מוצרים שפורסמו. ה-hook של beforeOperation תופס את המחיקה וזורק שגיאה—זה אמין יותר מבדיקה בצד הלקוח.

"Hooks הם המקום היחיד שבו אפשר להבטיח עקביות נתונים ברמת האפליקציה" — תיעוד KeystoneJS.

איך להימנע מ-N+1 בעבודה עם קשרים

KeystoneJS טוען קשרים בעצלתיים כברירת מחדל. כדי להימנע מ-N+1, השתמשו ב:

  • אינדקס על מפתחות זרים (ודאו שלשדה הקשר יש isIndexed: true).
  • graphql.cacheHint עבור שדות שנשאלים לעתים קרובות.
  • הגדרות ui.listView.initialColumns ברורות—אל תפלטו את כל הישויות הקשורות בבת אחת.
  • במידת הצורך, שמרו במטמון עם graphql.cacheHint.

טעויות טיפוסיות בעבודה עם Lists

  • אינדקסים חסרים. אם שדה משמש לעתים קרובות בסינון או מיון, הוסיפו isIndexed: true. אחרת, כל שאילתה סורקת את כל הטבלה.
  • חוסר ב-validateInput hook. בלעדיו, נתונים שגויים יכולים להיכנס למסד הנתונים. תמיד בדקו אילוצים עסקיים.
  • קשרים מוגזמים. אל תיצרו חיבורים שאינם נחוצים בגרסה הנוכחית—קשרים מיותרים מאטים את ממשק הניהול.

אילו סוגי שדות להשתמש

סוג שדה תיאור דוגמת שימוש
text מחרוזת עד N תווים שם מוצר
relationship קישור ל-List אחר קטגוריית מוצר
virtual שדה מחושב מחיר כולל מע"מ
select בחירה מרשימה סטטוס מוצר

תהליך עבודה למודל נתונים

  1. ניתוח דרישות עסקיות — ישויות, קשרים, זכויות גישה.
  2. עיצוב הסכמה — דיאגרמת ER, סוגי שדות, אינדקסים, hooks.
  3. יישום — כתיבת Lists, הגדרת גישה, hooks.
  4. בדיקות אינטגרציה — בדיקת פעולות GraphQL, טעינת נתונים.
  5. פריסה וניטור — פריסה לשרת, הגדרת לוגים.

לוחות זמנים משוערים

List אחד עם שדות סטנדרטיים — בין 0.5 ליום עבודה אחד. List מורכב עם hooks, שדות וירטואליים ו-mutations מותאמים אישית — 1–2 ימים. מודל נתונים מלא לחנות מקוונת (10–15 Lists) — בין 5 ל-8 ימים. צרו איתנו קשר להערכה מדויקת לפרויקט שלכם.

מה כלול בתוצאה

אנחנו מספקים:

  • קוד מקור ל-Lists עם הערות.
  • תיעוד למודל (טבלה עם תיאורי שדות וקשרים).
  • ממשק ניהול מוגדר עם עמודות ומסננים נדרשים.
  • סקריפטי מיגרציה (באמצעות Prisma).
  • הוראות פריסה ואינטגרציה.

בנוסף, אחריות ל-30 יום: אם נמצאו באגים, נתקן אותם בחינם. ניסיון עם KeystoneJS — מעל 5 שנים, יותר מ-50 פרויקטים שהושלמו. הזמינו פיתוח List מותאם אישית — נעריך את מודל הנתונים שלכם. קבלו ייעוץ לפרויקט שלכם.

השוואה עם חלופות

קריטריון KeystoneJS (הגישה שלנו) פתרון טיפוסי (ללא אופטימיזציה)
מהירות טעינת List < 200 אלפיות השנייה 2–5 שניות בגלל N+1
יכולת הרחבה Hooks, שדות וירטואליים, mutations מותאמים אישית רק CRUD
אבטחה גישה ברמת שדה ופעולה הכל או כלום
ממשק ניהול ניתן להתאמה אישית סטנדרטי