כיצד לבנות מחוון נוכחות בזמן אמת לאתר שלך
הבעיה: משתמשים רואים תמונות פרופיל אבל לא יודעים מי מחובר כרגע
עבדנו על פרויקט — פלטפורמת קורסים מקוונת עם סמינרים חיים. המשתתפים התלוננו: "אני כותב למורה בצ'אט, אבל הם לא עונים — מסתבר שהם כבר לא מחוברים." זה אולי נראה טריוויאלי, אבל היעדר מחוון בזמן אמת הפחית את המעורבות ב-15%. יישמנו מחוון נוכחות ביומיים. עכשיו כולם רואים נקודה ירוקה ויודעים למי לשאול שאלה כרגע. במשך שלוש שנים, פרסנו יותר מ-50 פתרונות כאלה לצ'אטים, קורסים ופורטלים ארגוניים.
דופק דרך HTTP במקום WebSocket
אנחנו משתמשים בדופק דרך HTTP — אנו שולחים POST /api/presence/ping כל 30 שניות. אם אין פינג במשך 90 שניות, המשתמש נחשב לא מקוון. גישה זו פשוטה יותר מ-WebSocket ואינה דורשת חיבור קבוע. עבור רוב האתרים, זה מספיק. WebSocket מספק דיוק עד שנייה אבל דורש תשתית שרת וחיבור קבוע. הבחירה תלויה בתרחיש: לצ'אט — WebSocket, לרשימת משתתפים בקורס — דופק HTTP.
| גישה | דיוק | מורכבות | מתי להשתמש |
|---|---|---|---|
| WebSocket/SSE | ~שנייה אחת | גבוהה | צ'אט, עריכה שיתופית |
| דופק HTTP | ~60 שניות | נמוכה | פרופילים, רשימות משתתפים |
| נראה לאחרונה | ~3 דקות | נמוכה מאוד | הגדרות פרטיות |
Redis כאחסון האידיאלי לנוכחות
Redis מהיר פי 10 מ-PostgreSQL במהירות כתיבה למשימה זו ומסיר אוטומטית נתונים מיושנים ללא משימות רקע. כל קריאה לsetex יוצרת מפתח עם TTL של 90 שניות. אם המשתמש מפסיק לשלוח פינג, המפתח נעלם אוטומטית. כפי שצוין בתיעוד Redis, הפקודה SETEX מגדירה מפתח עם תפוגה אוטומטית. להלן יישום מפושט של השירות שלנו:
class PresenceService {
private const TTL = 90;
public function markOnline(int $userId, string $context = 'global'): void {
Redis::setex("presence:{$context}:{$userId}", self::TTL, now()->timestamp);
$wasOnline = Redis::exists("presence_flag:{$context}:{$userId}");
if (!$wasOnline) {
Redis::setex("presence_flag:{$context}:{$userId}", self::TTL + 10, 1);
broadcast(new UserCameOnline($userId, $context));
}
}
public function markOffline(int $userId, string $context = 'global'): void {
Redis::del("presence:{$context}:{$userId}");
Redis::del("presence_flag:{$context}:{$userId}");
broadcast(new UserWentOffline($userId, $context));
}
public function getOnlineUsers(string $context = 'global'): array {
$keys = Redis::keys("presence:{$context}:*");
return array_map(fn($k) => (int) last(explode(':', $k)), $keys);
}
public function isOnline(int $userId, string $context = 'global'): bool {
return (bool) Redis::exists("presence:{$context}:{$userId}");
}
} הסבר על הקוד
הפרמטר `$context` מאפשר פיצול נוכחות בין סעיפים: `chat_room:42`, `course:17`, `global`. הפקודה `setex` מגדירה מפתח שפג תוקפו לאחר TTL שניות. הדגל `presence_flag` מונע אירועי שידור כפולים בכל פינג.הפרמטר class PresenceService { private const TTL = 90; public function markOnline(int $userId, string $context = 'global'): void { Redis::setex("presence:{$context}:{$userId}", self::TTL, now()->timestamp); $wasOnline = Redis::exists("presence_flag:{$context}:{$userId}"); if (!$wasOnline) { Redis::setex("presence_flag:{$context}:{$userId}", self::TTL + 10, 1); broadcast(new UserCameOnline($userId, $context)); } } public function markOffline(int $userId, string $context = 'global'): void { Redis::del("presence:{$context}:{$userId}"); Redis::del("presence_flag:{$context}:{$userId}"); broadcast(new UserWentOffline($userId, $context)); } public function getOnlineUsers(string $context = 'global'): array { $keys = Redis::keys("presence:{$context}:*"); return array_map(fn($k) => (int) last(explode(':', $k)), $keys); } public function isOnline(int $userId, string $context = 'global'): bool { return (bool) Redis::exists("presence:{$context}:{$userId}"); } } מאפשר פיצול נוכחות בין סעיפים: $context, chat_room:42, course:17.
אירועי שידור מסנכרנים סטטוס
כאשר הסטטוס משתנה, אנו משדרים אירועי global ו-UserCameOnline. הם מכילים רק את מזהה המשתמש וההקשר. הלקוח מקבל את האירוע ומעדכן את הנקודה הירוקה. לשידור Laravel אנו משתמשים ב-Pusher או Redis + Socket.IO. דוגמה לאירוע:
class UserCameOnline implements ShouldBroadcast {
public $userId;
public $context;
public function broadcastOn(): array {
return [new PresenceChannel("presence.{$this->context}")];
}
}הלקוח נרשם לערוץ דרך Laravel Echo ומגיב להודעות.
למה לבחור TTL של 90 שניות?
ה-TTL צריך להיות פי שלושה ממרווח הפינג כדי לפצות על אובדני חיבור קצרים. עם פינג כל 30 שניות, TTL של 90 שניות מכסה שלושה פינגים שהוחמצו. אם החיבור נופל ל-40 שניות, המשתמש לא יוצא למצב לא מקוון. TTL קצר יותר (למשל, 60 שניות) גורם להבהוב סטטוס ברשתות לא יציבות. TTL ארוך יותר (120+ שניות) מעכב את זיהוי עזיבת המשתמש.
| TTL | התנהגות |
|---|---|
| 60 שניות | מהבהב לאחר החמצת 2 פינגים |
| 90 שניות | יציב, מגיב לאחר 3 שהוחמצו |
| 120 שניות | זיהוי איטי של ניתוק |
הוראות שלב אחר שלב ליישום פינג דופק
- צור נקודת קצה POST עם אימות (למשל, Sanctum).
- בשירות PresenceService, יישם שיטות
UserWentOffline/class UserCameOnline implements ShouldBroadcast { public $userId; public $context; public function broadcastOn(): array { return [new PresenceChannel("presence.{$this->context}")]; } }. - בצד הלקוח, שלח פינג בטעינת הדף, ולאחר מכן חזור כל 30 שניות באמצעות
setInterval. - במטפל
beforeunload, שלח בקשת ניתוק דרךnavigator.sendBeacon(ראה תיעוד MDN). - בשרת, עם קבלת פינג, עדכן את ה-TTL. אם אין פינג במשך 90 שניות, Redis מוחק את המפתח ונשלח אירוע ניתוק (יישם בדיקה בכל פינג או השתמש במשימת רקע).
טעויות נפוצות ביישום מחוון נוכחות
- אי שימוש ב-
navigator.sendBeacon— כאשר הכרטיסייה נסגרת, הבקשה לא יוצאת, והמשתמש נשאר מחובר עד שה-TTL פג. פתרון: השתמש ב-context. - אין הפרדת הקשר — כל המשתמשים רואים זה את זה ללא קשר לסעיף. פתרון: העבר
UserCameOnlineבכל פינג. - TTL קצר מדי — המשתמש מהבהב (מחובר/לא מקוון) בחיבורים לא יציבים. אנו ממליצים על TTL = פי 3 ממרווח הפינג.
מה כלול בעבודה
- פיתוח נקודת קצה לדופק ושירות Redis
- הגדרת אירועי שידור
UserWentOffline,UserWentOffline - יישום המחוון בממשק המשתמש (נקודה / תג)
- תיעוד API ואינטגרציה
- הדרכת צוות לתחזוקה שוטפת
למהנדסים שלנו יש ניסיון של 5+ שנים עם Laravel ו-Vue/React. במשך 3 שנים, יישמנו יותר מ-50 פתרונות דומים לצ'אטים, קורסים ופורטלים ארגוניים. רקורד מוכח שלנו מבטיח יישום אמין.
לוחות זמנים ועלות
- פינג דופק + Redis TTL + מחוון: 1–2 ימים (כ-$1,500–$3,000)
- שידור בשינוי סטטוס: יום אחד (כ-$1,000)
- ערוצי נוכחות דרך Laravel Echo: יום אחד (כ-$1,000)
- נראה לאחרונה: 0.5 יום (כ-$500)
- הגדרות פרטיות: +0.5 יום (כ-$500)
העלות מחושבת באופן אישי לפי מורכבות. צור קשר — נבדוק את הפרויקט שלך תוך יום עסקים אחד. קבל ייעוץ לפרויקט שלך — המהנדסים שלנו יעזרו לבחור את הפתרון האופטימלי. אנו מציעים אחריות של 100% שביעות רצון.







