כאשר נתונים ב-Directus דורשים תגובה מיידית—אימות הזמנות, התראה על הגשה חדשה, או סנכרון עם מערכת חיצונית—יכולות ה-CMS הסטנדרטיות לרוב אינן מספיקות. בניית הרחבות Hook מותאמות אישית ל-Directus מטמיעה לוגיקה עסקית ישירות בליבה: אתה נרשם לאירועי CRUD, אימות, או העלאת קבצים ומריץ קוד שרירותי. זה מפחית את זמן ההשהיה ב-30–50% בהשוואה לקריאות API חיצוניות ומפשט את הארכיטקטורה—אין צורך להקים מיקרוסרוויס נפרד לעיבוד נתונים. עם ניסיון של למעלה מ-5 שנים בפיתוח Directus ויותר מ-50 Hooks מותאמים אישית שנבנו עבור יותר מ-30 לקוחות, הצוות שלנו מספק פתרונות אמינים ויעילים. עלות פיתוח Hook טיפוסית נעה בין 800 ל-2,500 דולר, ולקוחות מדווחים על חיסכון חודשי של עד 5,000 דולר בעלויות תפעול. צור קשר כדי לדון במשימה שלך—נכין הערכת עלות תוך יום אחד.
מדוע Hooks מותאמים אישית עדיפים על שירותים חיצוניים ופותרים בעיות נפוצות
Hooks מותאמים אישית מבטלים את תקורת קריאות הרשת—הם רצים באותו תהליך Directus, מקצרים את זמן ההשהיה ב-30-50% ומפחיתים את ה-TTFB. לדוגמה, Hook לאימות הזמנה בודק מלאי ב-5ms, בעוד קריאת API חיצונית אורכת 50–100ms. זה קריטי לפרויקטים בעלי עומס גבוה. אתגרים טיפוסיים שנפתרים:
- אימות נתונים מורכב—בדיקת רמות מלאי, תקינות קשרים, ייחודיות שדות.
- תופעות לוואי—התראות ל-Slack/Telegram, יומני ביקורת, ביטול מטמון (מהיר פי 3–5 מקריאות חיצוניות).
- אינטגרציות עם ERP, CRM (לדוגמה, 1C), שערי תשלום.
- אוטומציה—יצירת slugs, עדכוני מלאי, דוחות מתוזמנים באמצעות cron.
התחלת עבודה עם Directus Hooks
- צור קובץ
extensions/hooks/custom/index.ts. - ייצא פונקציה שמקבלת
HookExtensionContext. - קרא ל-
filter('items.create', async (payload, meta, context) => { ... })כדי לאמת או לשנות נתונים לפני שמירה. - כדי לבטל את הפעולה, זרוק שגיאה באמצעות
throw new Error('Сообщение'). - הפעל מחדש את Directus—ה-Hook נקלט אוטומטית.
באמצעות action (מופע Knex) ניתן להריץ שאילתות SQL ישירות או להשתמש ב-filter לגישה עקבית לאוספים. תיעוד Directus ממליץ על init, אך Knex ישיר מתאים ל-JOIN מורכבים.
סוגי אירועים ומתי להשתמש בהם
| סוג | תיאור | מקרה שימוש |
|---|---|---|
schedule |
לאחר הפעולה | התראות, מטמון, אינטגרציות |
// extensions/hooks/business-logic/index.ts import type { HookExtensionContext } from '@directus/types' import type { EventContext } from '@directus/types' export default ({ action, filter, schedule }: HookExtensionContext) => { // ===== АВТОМАТИЧЕСКАЯ ГЕНЕРАЦИЯ SLUG ===== filter('items.create', (payload, meta) => { if (meta.collection === 'articles' && payload.title && !payload.slug) { payload.slug = generateSlug(payload.title as string) } if (meta.collection === 'products' && payload.name && !payload.slug) { payload.slug = generateSlug(payload.name as string) } return payload }) // ===== ВАЛИДАЦИЯ ===== filter('items.create', async (payload, meta, context) => { if (meta.collection !== 'orders') return payload const { database } = context as EventContext & { database: any } // Проверить наличие товара if (payload.items && Array.isArray(payload.items)) { for (const item of payload.items) { const product = await database('products') .where({ id: item.product_id }) .first() if (!product) { throw new Error(`Product ${item.product_id} not found`) } if (product.stock < item.quantity) { throw new Error(`Insufficient stock for "${product.name}"`) } } } return payload }) // ===== ИНВАЛИДАЦИЯ КЭША ===== action('items.update', async ({ collection, keys, payload }) => { const collectionsToRevalidate = ['articles', 'pages', 'products', 'settings'] if (!collectionsToRevalidate.includes(collection)) return try { await fetch(`${process.env.NEXTJS_URL}/api/revalidate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-revalidate-secret': process.env.REVALIDATE_SECRET!, }, body: JSON.stringify({ collection, keys }), }) } catch (err) { console.error('Failed to revalidate cache:', err) } }) // ===== УВЕДОМЛЕНИЯ ===== action('items.create', async ({ collection, key, payload }, context) => { if (collection !== 'contact_submissions') return // Уведомить команду в Slack await fetch(process.env.SLACK_WEBHOOK!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `📬 Новая заявка от ${payload.name} <${payload.email}>\n${payload.message}`, }), }) }) // ===== АУДИТ ЛОГ ===== action('items.update', async ({ collection, keys, payload, accountability }) => { if (!accountability?.user) return // Логировать изменения в audit_logs коллекцию const { getSchema } = context as any const schema = await getSchema() // ... записать в audit_logs }) // ===== СИНХРОНИЗАЦИЯ ОСТАТКОВ ===== action('items.update', async ({ collection, keys, payload }, context) => { if (collection !== 'orders') return if (payload.status !== 'paid') return const { database } = context as EventContext & { database: any } const order = await database('orders') .where({ id: keys[0] }) .first() if (order?.items) { const items = JSON.parse(order.items) for (const item of items) { await database('products') .where({ id: item.product_id }) .decrement('stock', item.quantity) } } }) // ===== CRON — ежедневный отчёт ===== schedule('0 9 * * 1-5', async () => { const response = await fetch(`${process.env.API_URL}/custom/reports/sales`) const stats = await response.json() await fetch(process.env.SLACK_WEBHOOK!, { body: JSON.stringify({ text: `📊 Отчёт за вчера: заказов ${stats.count}, выручка ${stats.revenue.toLocaleString()} $`, }), }) }) } function generateSlug(text: string): string { const translitMap: Record<string, string> = { а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'yo', ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ф: 'f', х: 'h', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'sch', ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya', } return text .toLowerCase() .replace(/[а-яё]/g, char => translitMap[char] || char) .replace(/\s+/g, '-') .replace(/[^\w-]/g, '') .replace(/-+/g, '-') .slice(0, 100) } |
לפני הפעולה (יכול לשנות נתונים) | אימות, שינוי payload |
context.database |
בעת הפעלת השרת | טעינת תצורה, אתחול חיבורים |
ItemsService |
בלוח זמנים של cron | דוחות יומיים, ניקוי נתונים |
השוואה: Action Hooks רצים לאחר כתיבת DB (אסינכרוני, לא חוסם) ואינם יכולים לשנות נתונים, בעוד Filter Hooks רצים לפני כתיבה (סינכרוני, חוסם) ויכולים לשנות את ה-payload.
דוגמה מלאה להרחבת Hook
// extensions/hooks/business-logic/index.ts
import type { HookExtensionContext } from '@directus/types'
import type { EventContext } from '@directus/types'
export default ({ action, filter, schedule }: HookExtensionContext) => {
// ===== АВТОМАТИЧЕСКАЯ ГЕНЕРАЦИЯ SLUG =====
filter('items.create', (payload, meta) => {
if (meta.collection === 'articles' && payload.title && !payload.slug) {
payload.slug = generateSlug(payload.title as string)
}
if (meta.collection === 'products' && payload.name && !payload.slug) {
payload.slug = generateSlug(payload.name as string)
}
return payload
})
// ===== ВАЛИДАЦИЯ =====
filter('items.create', async (payload, meta, context) => {
if (meta.collection !== 'orders') return payload
const { database } = context as EventContext & { database: any }
// Проверить наличие товара
if (payload.items && Array.isArray(payload.items)) {
for (const item of payload.items) {
const product = await database('products')
.where({ id: item.product_id })
.first()
if (!product) {
throw new Error(`Product ${item.product_id} not found`)
}
if (product.stock < item.quantity) {
throw new Error(`Insufficient stock for "${product.name}"`)
}
}
}
return payload
})
// ===== ИНВАЛИДАЦИЯ КЭША =====
action('items.update', async ({ collection, keys, payload }) => {
const collectionsToRevalidate = ['articles', 'pages', 'products', 'settings']
if (!collectionsToRevalidate.includes(collection)) return
try {
await fetch(`${process.env.NEXTJS_URL}/api/revalidate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-revalidate-secret': process.env.REVALIDATE_SECRET!,
},
body: JSON.stringify({ collection, keys }),
})
} catch (err) {
console.error('Failed to revalidate cache:', err)
}
})
// ===== УВЕДОМЛЕНИЯ =====
action('items.create', async ({ collection, key, payload }, context) => {
if (collection !== 'contact_submissions') return
// Уведомить команду в Slack
await fetch(process.env.SLACK_WEBHOOK!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `📬 Новая заявка от ${payload.name} <${payload.email}>\n${payload.message}`,
}),
})
})
// ===== АУДИТ ЛОГ =====
action('items.update', async ({ collection, keys, payload, accountability }) => {
if (!accountability?.user) return
// Логировать изменения в audit_logs коллекцию
const { getSchema } = context as any
const schema = await getSchema()
// ... записать в audit_logs
})
// ===== СИНХРОНИЗАЦИЯ ОСТАТКОВ =====
action('items.update', async ({ collection, keys, payload }, context) => {
if (collection !== 'orders') return
if (payload.status !== 'paid') return
const { database } = context as EventContext & { database: any }
const order = await database('orders')
.where({ id: keys[0] })
.first()
if (order?.items) {
const items = JSON.parse(order.items)
for (const item of items) {
await database('products')
.where({ id: item.product_id })
.decrement('stock', item.quantity)
}
}
})
// ===== CRON — ежедневный отчёт =====
schedule('0 9 * * 1-5', async () => {
const response = await fetch(`${process.env.API_URL}/custom/reports/sales`)
const stats = await response.json()
await fetch(process.env.SLACK_WEBHOOK!, {
body: JSON.stringify({
text: `📊 Отчёт за вчера: заказов ${stats.count}, выручка ${stats.revenue.toLocaleString()} $`,
}),
})
})
}
function generateSlug(text: string): string {
const translitMap: Record<string, string> = {
а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'yo', ж: 'zh', з: 'z', и: 'i', й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ф: 'f', х: 'h', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'sch', ъ: '', ы: 'y', ь: '', э: 'e', ю: 'yu', я: 'ya',
}
return text
.toLowerCase()
.replace(/[а-яё]/g, char => translitMap[char] || char)
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '')
.replace(/-+/g, '-')
.slice(0, 100)
}
מקרה אמיתי: אינטגרציית 1C
עבור חנות מסחר אלקטרוני גדולה, יישמנו סנכרון אוטומטי של הזמנות ומלאי באמצעות Hooks. לאחר תשלום, Hook שולח נתוני הזמנה ל-1C דרך REST API, וכאשר המלאי משתנה במערכת החיצונית, הוא מעדכן את Directus. זמן הסנכרון ירד מ-15 דקות ל-5 שניות. עומס השרת ירד ב-40% על ידי ביטול קריאות API מיותרות. זמן פיתוח האינטגרציה קוצר בחצי בהשוואה לגישה המסורתית. עלות הפיתוח הייתה 1,200 דולר, והלקוח חסך למעלה מ-3,000 דולר חודשית בהזנת נתונים ידנית. צור קשר כדי לפתח Hooks מותאמים אישית ל-Directus—החל מ-800 דולר ל-Hook.
מה כלול, תמחור והתחייבויות
- קוד מקור עם הערות TypeScript
- תיעוד להתקנה והגדרה (README)
- גישה למאגר Git פרטי
- הדרכת צוות (שעה אונליין)
- תמיכה למשך שבועיים לאחר המסירה
פיתוח סט טיפוסי של 3–5 Hooks (אימות, התראות, מטמון) אורך 2–5 ימי עסקים. העלויות נעות בין 800 ל-2,500 דולר בהתאם למורכבות. לוחות זמנים נקבעים לאחר ניתוח הלוגיקה העסקית שלך.
לצוות שלנו יש מומחיות עמוקה בפיתוח Directus, לאחר שהשלים למעלה מ-50 פרויקטי התאמה אישית כולל אינטגרציות עם 1C, CRM ומערכות תשלום. אנו מבטיחים איכות קוד, עמידה בלוחות זמנים ותמיכה מלאה. קבל ייעוץ עם מהנדס—נענה על כל שאלה.







