WebSockets والتطبيقات الفورية
نظام الإشعارات في الوقت الفعلي
نظام الإشعارات في الوقت الفعلي
تُبقي الإشعارات في الوقت الفعلي المستخدمين على اطلاع بالأحداث والتحديثات المهمة فورًا. بناء نظام إشعارات قوي مع WebSockets يتيح التسليم الفوري للتنبيهات والتحديثات والرسائل دون الحاجة إلى تحديث الصفحة أو الاستطلاع.
أنواع الإشعارات في الوقت الفعلي
تستخدم التطبيقات الحديثة أنواعًا مختلفة من الإشعارات حسب السياق والإلحاح:
- إشعارات Toast: رسائل منبثقة مؤقتة (3-5 ثوانٍ) للتحديثات غير الحرجة
- إشعارات الشارة: مؤشرات رقمية تعرض الأعداد غير المقروءة على الأيقونات أو عناصر القائمة
- إشعارات الدفع: إشعارات على مستوى النظام حتى عند إغلاق المتصفح (باستخدام Service Workers)
- إشعارات داخل التطبيق: مراكز/لوحات إشعارات دائمة داخل التطبيق
- تنبيهات الصوت/الاهتزاز: ملاحظات صوتية أو لمسية للإشعارات ذات الأولوية العالية
نظام الإشعارات من جانب الخادم
أنشئ نظام إشعارات شامل على الخادم يدير التسليم والاستمرارية:
// server.js - خادم الإشعارات
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
// تخزين الإشعارات واتصالات المستخدم
const notifications = new Map(); // userId => [notifications]
const userSockets = new Map(); // userId => مجموعة معرفات المقابس
// أنواع الإشعارات
const NotificationType = {
INFO: 'info',
SUCCESS: 'success',
WARNING: 'warning',
ERROR: 'error',
MESSAGE: 'message',
FRIEND_REQUEST: 'friend_request',
COMMENT: 'comment',
LIKE: 'like',
SYSTEM: 'system'
};
io.on('connection', (socket) => {
const userId = socket.handshake.auth.userId;
console.log(`المستخدم ${userId} متصل`);
// تتبع مقبس المستخدم
if (!userSockets.has(userId)) {
userSockets.set(userId, new Set());
}
userSockets.get(userId).add(socket.id);
// إرسال الإشعارات غير المقروءة عند الاتصال
socket.emit('notificationHistory', getUserNotifications(userId, false));
// إرسال عدد الإشعارات
const unreadCount = getUserNotifications(userId, false).length;
socket.emit('notificationCount', unreadCount);
// وضع علامة على الإشعار كمقروء
socket.on('markNotificationRead', (notificationId) => {
markAsRead(userId, notificationId);
// إرسال العدد المحدث
const unreadCount = getUserNotifications(userId, false).length;
socket.emit('notificationCount', unreadCount);
});
// وضع علامة على جميع الإشعارات كمقروءة
socket.on('markAllRead', () => {
markAllAsRead(userId);
socket.emit('notificationCount', 0);
socket.emit('allNotificationsRead');
});
// الحصول على سجل الإشعارات
socket.on('getNotifications', (includeRead, callback) => {
const userNotifications = getUserNotifications(userId, includeRead);
callback(userNotifications);
});
// حذف الإشعار
socket.on('deleteNotification', (notificationId) => {
deleteNotification(userId, notificationId);
const unreadCount = getUserNotifications(userId, false).length;
socket.emit('notificationCount', unreadCount);
});
// معالجة قطع الاتصال
socket.on('disconnect', () => {
console.log(`المستخدم ${userId} قطع الاتصال`);
const sockets = userSockets.get(userId);
if (sockets) {
sockets.delete(socket.id);
if (sockets.size === 0) {
userSockets.delete(userId);
}
}
});
});
// دالة لإرسال الإشعار لمستخدم محدد
function sendNotification(userId, notification) {
const notificationData = {
id: Date.now() + Math.random(),
type: notification.type || NotificationType.INFO,
title: notification.title,
message: notification.message,
data: notification.data || {},
timestamp: Date.now(),
read: false,
priority: notification.priority || 'normal' // low, normal, high
};
// تخزين الإشعار
if (!notifications.has(userId)) {
notifications.set(userId, []);
}
notifications.get(userId).unshift(notificationData);
// الاحتفاظ بآخر 100 إشعار فقط لكل مستخدم
if (notifications.get(userId).length > 100) {
notifications.get(userId).pop();
}
// إرسال لجميع أجهزة المستخدم المتصلة
const sockets = userSockets.get(userId);
if (sockets) {
sockets.forEach(socketId => {
io.to(socketId).emit('notification', notificationData);
// تحديث العدد غير المقروء
const unreadCount = getUserNotifications(userId, false).length;
io.to(socketId).emit('notificationCount', unreadCount);
});
}
return notificationData;
}
// إشعارات الدفعة للكفاءة
function sendBatchNotifications(notifications) {
notifications.forEach(({ userId, notification }) => {
sendNotification(userId, notification);
});
}
// الحصول على إشعارات المستخدم
function getUserNotifications(userId, includeRead = true) {
const userNotifications = notifications.get(userId) || [];
if (includeRead) {
return userNotifications;
}
return userNotifications.filter(n => !n.read);
}
// وضع علامة على الإشعار كمقروء
function markAsRead(userId, notificationId) {
const userNotifications = notifications.get(userId);
if (userNotifications) {
const notification = userNotifications.find(n => n.id === notificationId);
if (notification) {
notification.read = true;
}
}
}
// وضع علامة على الكل كمقروء
function markAllAsRead(userId) {
const userNotifications = notifications.get(userId);
if (userNotifications) {
userNotifications.forEach(n => n.read = true);
}
}
// حذف الإشعار
function deleteNotification(userId, notificationId) {
const userNotifications = notifications.get(userId);
if (userNotifications) {
const index = userNotifications.findIndex(n => n.id === notificationId);
if (index !== -1) {
userNotifications.splice(index, 1);
}
}
}
const PORT = 3000;
http.listen(PORT, () => {
console.log(`خادم الإشعارات يعمل على المنفذ ${PORT}`);
});
ملاحظة: في الإنتاج، خزن الإشعارات في قاعدة بيانات (MongoDB، PostgreSQL) للاستمرارية وقابلية التوسع الأفضل.
واجهة الإشعارات من جانب العميل
أنشئ واجهة إشعارات جذابة وعملية:
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>إشعارات في الوقت الفعلي</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f5f5f5; }
.navbar {
background: #2c3e50;
color: white;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.notification-bell {
position: relative;
cursor: pointer;
font-size: 24px;
}
.notification-badge {
position: absolute;
top: -8px;
left: -8px;
background: #e74c3c;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.notification-panel {
position: absolute;
top: 60px;
left: 30px;
width: 400px;
max-height: 600px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
display: none;
flex-direction: column;
}
.notification-panel.active {
display: flex;
}
.notifications-list {
flex: 1;
overflow-y: auto;
max-height: 500px;
}
.notification-item {
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
}
.notification-item:hover {
background: #f8f9fa;
}
.notification-item.unread {
background: #e3f2fd;
}
.toast-container {
position: fixed;
top: 80px;
left: 30px;
z-index: 9999;
}
.toast {
background: white;
padding: 15px 20px;
margin-bottom: 10px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
min-width: 300px;
display: flex;
align-items: center;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(-400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
</head>
<body>
<div class="navbar">
<h1>تطبيقي</h1>
<div class="notification-bell" onclick="toggleNotifications()">
🔔
<span class="notification-badge" id="notificationBadge" style="display:none">0</span>
</div>
<div class="notification-panel" id="notificationPanel">
<div class="panel-header">
<h3>الإشعارات</h3>
<span class="mark-all-read" onclick="markAllRead()">وضع علامة مقروء على الكل</span>
</div>
<div class="notifications-list" id="notificationsList">
<div class="no-notifications">لا توجد إشعارات</div>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="/socket.io/socket.io.js"></script>
<script src="notifications.js"></script>
</body>
</html>
نصيحة: استخدم Service Workers لتمكين إشعارات الدفع حتى عند إغلاق المتصفح. اجمع بين WebSocket للتسليم الفوري مع Push API للسيناريوهات دون اتصال.
تحذير: لا ترسل رسائل غير مرغوب فيها للمستخدمين بالإشعارات. نفذ التجميع الذكي ومستويات الأولوية وتفضيلات إشعارات المستخدم لتجنب إرهاق الإشعارات.
تمرين: أنشئ نظام إشعارات شامل مع:
- تفضيلات إشعارات المستخدم (تمكين/تعطيل حسب النوع)
- مستويات أولوية الإشعارات (منخفض، عادي، مرتفع، عاجل)
- وضع عدم الإزعاج مع ساعات هادئة مجدولة
- تجميع الإشعارات (تجميع الإشعارات المتشابهة)
- أزرار إجراء على الإشعارات (قبول، رفض، عرض، إلخ)
- سجل الإشعارات مع البحث والتصفية
- خيار ملخص البريد الإلكتروني للإشعارات الفائتة
- التكامل مع Push API للمتصفح للإشعارات دون اتصال
- تتبع التحليلات (معدل التسليم، نسبة النقر، معدل الرفض)
اختبر الإشعارات عبر علامات تبويب المتصفح المتعددة والأجهزة لضمان المزامنة الصحيحة.