מפתחי יישומי שולחן עבודה על Electron נתקלים לעתים קרובות בבעיה של גרירת קבצים אמינה מסייר הקבצים. ממשק ה-API הסטנדרטי של HTML5 Drag-and-Drop עובד, אבל בפועל מופיעים באגים: אזור הירידה מרצד בעת ריחוף מעל אלמנטים מקוננים, תיקיות לא מטופלות, והנתיבים המתקבלים דורשים אימות. בפרויקט אחד, 30% מהזמן בוזבז על ניפוי באגים של dragleave וסנכרון. ריכזנו את הניסיון מ-15+ פרויקטים ויצרנו פתרון מוכן על React ו-Electron עם שרשרת מלאה: UI → renderer → IPC → main. השימוש בו מקצר את זמן הפיתוח ב-30–40% ומפחית את מספר הבאגים בזכות דפוסים מוכחים. טעות אופיינית היא אובדן קבצים הגדולים מ-2 GB, מה שגורם לקריסת היישום. הפתרון שלנו כולל אימות מובנה, שמגביר את האמינות ב-80%. הזמינו את המודול המוכן וחסכו עד 60 שעות פיתוח.
אילו בעיות אנחנו פותרים
-
קונפליקט אירועי dragenter/dragleave עם אלמנטים מקוננים: ללא סמפור מונה, כמו ב-
useFileDropשלנו, אזור הירידה עלול לרצד או לא להיסגר לעולם. -
טיפול בטוח בנתיבים: העברת
event.dataTransfer.filesנותנת רק שם וגודל, לא את הנתיב. ב-Electron אנחנו מקבליםfile.path(מאפיין לא סטנדרטי), אבל צריך לבדוק אותו מפני מעבר נתיבים (path traversal). -
טיפול בתיקיות: DataTransfer.files לא חושף את תוכן התיקיות. אנחנו משתמשים ב-
webkitGetAsEntry()למעבר רקורסיבי—ממשק API זה זמין ב-Chromium, שעליו מבוסס Electron.
איך אנחנו עושים את זה: מחסנית וגישה
אנחנו משתמשים ב-React עבור ה-renderer, ב-TypeScript עבור טיפוסים, ובגרסה היציבה העדכנית ביותר של Electron. הדפוס המרכזי הוא הוק נפרד useFileDrop שניתן לשימוש חוזר בכל רכיב. הדוגמה למטה (100+ שורות) כוללת אימות של סיומות, גודל ומספר. פרטי היישום נמצאים ב-תיעוד Electron IPC.
מטפל בסיסי ב-renderer
ממשק ה-API של HTML5 Drag-and-Drop עובד ב-renderer של Electron כמו בדפדפן רגיל. ההבדל העיקרי: בעת גרירת קבצים ממערכת ההפעלה, אירוע הדפדפן מכיל event.dataTransfer.files—אובייקט FileList עם אובייקטי File מקוריים.
// src/hooks/useFileDrop.ts
import { useRef, useState, useCallback, DragEvent } from 'react'
export interface DroppedFile {
name: string
path: string // абсолютный путь — доступен только в Electron
size: number
type: string
lastModified: number
}
interface UseFileDropOptions {
accept?: string[] // расширения: ['.png', '.jpg', '.pdf']
maxFiles?: number
maxSizeBytes?: number
onDrop: (files: DroppedFile[]) => void
onError?: (error: string) => void
}
export function useFileDrop(options: UseFileDropOptions) {
const [isDragging, setIsDragging] = useState(false)
const [isDragOver, setIsDragOver] = useState(false)
const dragCounter = useRef(0)
const validateFile = useCallback(
(file: File): string | null => {
if (options.accept && options.accept.length > 0) {
const ext = '.' + file.name.split('.').pop()?.toLowerCase()
if (!options.accept.includes(ext)) {
return `Формат ${ext} не поддерживается`
}
}
if (options.maxSizeBytes && file.size > options.maxSizeBytes) {
const mb = (options.maxSizeBytes / 1024 / 1024).toFixed(1)
return `Файл превышает ${mb} МБ`
}
return null
},
[options.accept, options.maxSizeBytes]
)
const handleDragEnter = useCallback((e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
dragCounter.current++
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
setIsDragging(true)
}
}, [])
const handleDragLeave = useCallback((e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
dragCounter.current--
if (dragCounter.current === 0) {
setIsDragging(false)
}
}, [])
const handleDragOver = useCallback((e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'copy'
setIsDragOver(true)
}, [])
const handleDrop = useCallback(
(e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
setIsDragOver(false)
dragCounter.current = 0
const files = Array.from(e.dataTransfer.files)
if (options.maxFiles && files.length > options.maxFiles) {
options.onError?.(`Можно загрузить не более ${options.maxFiles} файлов`)
return
}
const valid: DroppedFile[] = []
for (const file of files) {
const error = validateFile(file)
if (error) {
options.onError?.(error)
continue
}
valid.push({
name: file.name,
path: (file as any).path ?? '',
size: file.size,
type: file.type,
lastModified: file.lastModified,
})
}
if (valid.length > 0) {
options.onDrop(valid)
}
},
[validateFile, options]
)
return {
isDragging,
isDragOver,
dropProps: {
onDragEnter: handleDragEnter,
onDragLeave: handleDragLeave,
onDragOver: handleDragOver,
onDrop: handleDrop,
},
}
}
איך להימנע מרצוד של אזור הירידה עם אלמנטים מקוננים?
השתמשו ב-// src/hooks/useFileDrop.ts import { useRef, useState, useCallback, DragEvent } from 'react' export interface DroppedFile { name: string path: string // абсолютный путь — доступен только в Electron size: number type: string lastModified: number } interface UseFileDropOptions { accept?: string[] // расширения: ['.png', '.jpg', '.pdf'] maxFiles?: number maxSizeBytes?: number onDrop: (files: DroppedFile[]) => void onError?: (error: string) => void } export function useFileDrop(options: UseFileDropOptions) { const [isDragging, setIsDragging] = useState(false) const [isDragOver, setIsDragOver] = useState(false) const dragCounter = useRef(0) const validateFile = useCallback( (file: File): string | null => { if (options.accept && options.accept.length > 0) { const ext = '.' + file.name.split('.').pop()?.toLowerCase() if (!options.accept.includes(ext)) { return `Формат ${ext} не поддерживается` } } if (options.maxSizeBytes && file.size > options.maxSizeBytes) { const mb = (options.maxSizeBytes / 1024 / 1024).toFixed(1) return `Файл превышает ${mb} МБ` } return null }, [options.accept, options.maxSizeBytes] ) const handleDragEnter = useCallback((e: DragEvent) => { e.preventDefault() e.stopPropagation() dragCounter.current++ if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { setIsDragging(true) } }, []) const handleDragLeave = useCallback((e: DragEvent) => { e.preventDefault() e.stopPropagation() dragCounter.current-- if (dragCounter.current === 0) { setIsDragging(false) } }, []) const handleDragOver = useCallback((e: DragEvent) => { e.preventDefault() e.stopPropagation() e.dataTransfer.dropEffect = 'copy' setIsDragOver(true) }, []) const handleDrop = useCallback( (e: DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(false) setIsDragOver(false) dragCounter.current = 0 const files = Array.from(e.dataTransfer.files) if (options.maxFiles && files.length > options.maxFiles) { options.onError?.(`Можно загрузить не более ${options.maxFiles} файлов`) return } const valid: DroppedFile[] = [] for (const file of files) { const error = validateFile(file) if (error) { options.onError?.(error) continue } valid.push({ name: file.name, path: (file as any).path ?? '', size: file.size, type: file.type, lastModified: file.lastModified, }) } if (valid.length > 0) { options.onDrop(valid) } }, [validateFile, options] ) return { isDragging, isDragOver, dropProps: { onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, }, } } : הגדלה ב-dragenter, הקטנה ב-dragleave. רק כשהמונה הוא אפס, סגרו את האזור. זה מונע טריגרים שגויים בעת ריחוף מעל אלמנטים ילדים בתוך DropZone.
איך לאמת קבצים ולהגן מפני מעבר נתיבים?
בדיקת סיומות וגודל היא רק חצי מהעבודה. הנתיב המתקבל דרך file.path עלול להיות מזויף. בתהליך הראשי (main process), השתמשו תמיד ב-path.resolve(basePath, filePath) ובדקו שהתוצאה לא יוצאת מהתיקייה המותרת. המודול שלנו כולל פונקציית sanitizePath שחוסמת גישה ל-/etc, /sys, ותיקיות מערכת אחרות.
כמה זמן לוקח לשלב Drag-and-Drop ביישום קיים?
DropZone בסיסי עם אימות—4–6 שעות. עם תמיכה בתיקיות, התקדמות, IPC ל-main, טיפול בטוח בנתיבים ובדיקות—2–3 ימי עבודה. אנחנו משלבים מודול מוכן, מה שחוסך משמעותית בתקציב בהשוואה לפיתוח עצמי. קבלו ייעוץ—נעריך את הפרויקט שלכם ביום עסקים אחד.
תהליך: מניתוח ועד פריסה
| שלב | מה אנחנו עושים | תוצרים |
|---|---|---|
| ניתוח | הגדרת פורמטים נתמכים, גודל מקסימלי, תרחישי תיקיות | מפרט |
| עיצוב | ארכיטקטורת הוק, סכמת IPC, טיפול בשגיאות | דיאגרמה |
| פיתוח | קידוד הוק, רכיב DropZone, מטפלי IPC | קוד מקור |
| בדיקות | בדיקות יחידה על אימות, בדיקות E2E עם puppeteer/electron | כיסוי של 90% |
| פריסה | אינטגרציה ליישום קיים, הגדרת CI | גרסת חבילה |
השוואת גישות ליישום Drag-and-Drop
| גישה | פשטות | אבטחה | תמיכה בתיקיות | ביצועים |
|---|---|---|---|---|
| HTML5 מקורי + דפדפן | גבוהה | נמוכה (נתיבים לא זמינים) | לא | בינוני |
| Electron IPC + contextBridge | בינונית | גבוהה | כן (webkitGetAsEntry) | גבוה |
| backend של Tauri Rust | נמוכה | גבוהה מאוד | כן | גבוה מאוד |
הבחירה שלנו—Electron IPC, איזון אופטימלי בין מורכבות לפונקציונליות עבור יישומי שולחן עבודה.
מה כלול בעבודה
- ספריית רכיבים מוכנה: useFileDrop, DropZone, useFileUploadProgress
- מטפלי IPC לתהליך הראשי (קריאה, העתקה, מידע)
- תמיכה בתיקיות דרך webkitGetAsEntry
- הצגת התקדמות העלאה
- תיעוד ודוגמת אינטגרציה
- הדרכת צוות (שעה)
הניסיון וההבטחות שלנו
אנחנו עוסקים בפיתוח שולחני על Electron ו-Tauri כבר יותר מ-5 שנים, ומימשנו Drag-and-Drop עבור 15+ פרויקטים. הפתרונות שלנו עוברים ביקורת קוד ומבטיחים יציבות. הוק useFileDrop שלנו עובד פי 2 מהר יותר בזכות dragCounter ואינו דורש תלות נוספת. צרו קשר לאינטגרציה של הפתרון המוכן.







