WebSockets والتطبيقات الفورية

بناء مشروع لوحة معلومات في الوقت الفعلي

15 دقيقة الدرس 33 من 35

بناء مشروع لوحة معلومات في الوقت الفعلي

توفر لوحات المعلومات في الوقت الفعلي رؤية فورية لمقاييس النظام ونشاط المستخدم وبيانات الأعمال. في هذا الدرس الشامل، سنقوم ببناء لوحة معلومات جاهزة للإنتاج تعرض البيانات المباشرة باستخدام WebSockets، مع تطبيق الرسوم البيانية والمقاييس وقدرات المراقبة.

نظرة عامة على بنية لوحة المعلومات

تتكون لوحة المعلومات المصممة جيداً من عدة مكونات رئيسية:

مكونات البنية:
  • طبقة جمع البيانات: تجمع المقاييس من مصادر مختلفة
  • خادم WebSocket: يبث التحديثات للعملاء المتصلين
  • واجهة لوحة المعلومات الأمامية: تعرض البيانات بالتصورات البصرية
  • تجميع البيانات: يعالج ويلخص البيانات الخام
  • نظام التنبيهات: يُخطر المستخدمين بالأحداث الحرجة

خادم لوحة المعلومات الخلفي

لنبني خادم Node.js يجمع ويبث مقاييس لوحة المعلومات:

// dashboard-server.js\nconst WebSocket = require('ws');\nconst express = require('express');\nconst os = require('os');\n\nconst app = express();\nconst server = require('http').createServer(app);\nconst wss = new WebSocket.Server({ server });\n\n// تخزين الاتصالات النشطة\nconst clients = new Set();\n\n// تخزين مقاييس لوحة المعلومات\nconst metrics = {\n    activeUsers: 0,\n    systemHealth: {\n        cpu: 0,\n        memory: 0,\n        uptime: 0\n    },\n    realtimeStats: {\n        requestsPerMinute: 0,\n        errorsPerMinute: 0,\n        avgResponseTime: 0\n    },\n    userActivity: [],\n    alerts: []\n};\n\n// محاكاة تتبع النشاط\nconst activityLog = [];\nconst MAX_ACTIVITY_ENTRIES = 100;\n\nwss.on('connection', (ws) => {\n    console.log('عميل لوحة المعلومات متصل');\n    clients.add(ws);\n    metrics.activeUsers = clients.size;\n\n    // إرسال البيانات الأولية\n    ws.send(JSON.stringify({\n        type: 'initial',\n        data: metrics\n    }));\n\n    // بث تحديث عدد المستخدمين\n    broadcastMetric('activeUsers', metrics.activeUsers);\n\n    ws.on('close', () => {\n        clients.delete(ws);\n        metrics.activeUsers = clients.size;\n        broadcastMetric('activeUsers', metrics.activeUsers);\n    });\n\n    ws.on('error', console.error);\n});\n\n// وظيفة مساعدة للبث\nfunction broadcastMetric(type, data) {\n    const message = JSON.stringify({ type, data, timestamp: Date.now() });\n    clients.forEach(client => {\n        if (client.readyState === WebSocket.OPEN) {\n            client.send(message);\n        }\n    });\n}\n\n// جمع مقاييس صحة النظام\nfunction collectSystemHealth() {\n    const cpuUsage = os.loadavg()[0] / os.cpus().length * 100;\n    const totalMem = os.totalmem();\n    const freeMem = os.freemem();\n    const memUsage = ((totalMem - freeMem) / totalMem) * 100;\n\n    metrics.systemHealth = {\n        cpu: Math.round(cpuUsage * 100) / 100,\n        memory: Math.round(memUsage * 100) / 100,\n        uptime: Math.round(os.uptime())\n    };\n\n    broadcastMetric('systemHealth', metrics.systemHealth);\n\n    // التحقق من التنبيهات\n    if (cpuUsage > 80) {\n        addAlert('warning', 'تم اكتشاف استخدام عالٍ للمعالج: ' + cpuUsage.toFixed(1) + '%');\n    }\n    if (memUsage > 85) {\n        addAlert('danger', 'تم اكتشاف استخدام عالٍ للذاكرة: ' + memUsage.toFixed(1) + '%');\n    }\n}\n\n// محاكاة الإحصائيات في الوقت الفعلي\nfunction updateRealtimeStats() {\n    metrics.realtimeStats = {\n        requestsPerMinute: Math.floor(Math.random() * 1000) + 500,\n        errorsPerMinute: Math.floor(Math.random() * 10),\n        avgResponseTime: Math.floor(Math.random() * 200) + 50\n    };\n\n    broadcastMetric('realtimeStats', metrics.realtimeStats);\n}\n\n// تتبع نشاط المستخدم\nfunction logUserActivity(action, details) {\n    const activity = {\n        id: Date.now(),\n        action,\n        details,\n        timestamp: new Date().toISOString(),\n        user: 'مستخدم' + Math.floor(Math.random() * 1000)\n    };\n\n    activityLog.unshift(activity);\n    if (activityLog.length > MAX_ACTIVITY_ENTRIES) {\n        activityLog.pop();\n    }\n\n    metrics.userActivity = activityLog.slice(0, 10); // إرسال آخر 10\n    broadcastMetric('userActivity', metrics.userActivity);\n}\n\n// نظام التنبيهات\nfunction addAlert(level, message) {\n    const alert = {\n        id: Date.now(),\n        level, // info, warning, danger\n        message,\n        timestamp: new Date().toISOString()\n    };\n\n    metrics.alerts.unshift(alert);\n    if (metrics.alerts.length > 50) {\n        metrics.alerts.pop();\n    }\n\n    broadcastMetric('alert', alert);\n}\n\n// محاكاة نشاط المستخدم\nfunction simulateUserActivity() {\n    const actions = [\n        { action: 'تسجيل دخول', details: 'قام المستخدم بتسجيل الدخول' },\n        { action: 'عرض صفحة', details: 'شاهد لوحة المعلومات' },\n        { action: 'تصدير بيانات', details: 'صدّر التقرير' },\n        { action: 'تغيير إعدادات', details: 'حدّث التفضيلات' },\n        { action: 'تسجيل خروج', details: 'قام المستخدم بتسجيل الخروج' }\n    ];\n\n    const randomAction = actions[Math.floor(Math.random() * actions.length)];\n    logUserActivity(randomAction.action, randomAction.details);\n}\n\n// بدء فترات جمع المقاييس\nsetInterval(collectSystemHealth, 2000);  // كل ثانيتين\nsetInterval(updateRealtimeStats, 3000);  // كل 3 ثوانٍ\nsetInterval(simulateUserActivity, 5000); // كل 5 ثوانٍ\n\n// تنبيه أولي\naddAlert('info', 'بدأ خادم لوحة المعلومات بنجاح');\n\nserver.listen(3000, () => {\n    console.log('خادم لوحة المعلومات يعمل على المنفذ 3000');\n});

واجهة HTML للوحة المعلومات

أنشئ واجهة لوحة معلومات شاملة مع عناصر متعددة:

<!DOCTYPE html>\n<html lang="ar" dir="rtl">\n<head>\n    <meta charset="UTF-8">\n    <meta name="viewport" content="width=device-width, initial-scale=1.0">\n    <title>لوحة معلومات في الوقت الفعلي</title>\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n\n        body {\n            font-family: 'Segoe UI', Tahoma, sans-serif;\n            background: #f5f7fa;\n            color: #2d3748;\n        }\n\n        .header {\n            background: #2c3e50;\n            color: white;\n            padding: 1rem 2rem;\n            box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n\n        .header h1 {\n            font-size: 1.5rem;\n            font-weight: 600;\n        }\n\n        .connection-status {\n            display: inline-block;\n            padding: 0.25rem 0.75rem;\n            border-radius: 12px;\n            font-size: 0.875rem;\n            margin-right: 1rem;\n        }\n\n        .status-connected {\n            background: #10b981;\n            color: white;\n        }\n\n        .status-disconnected {\n            background: #ef4444;\n            color: white;\n        }\n\n        .dashboard {\n            padding: 2rem;\n            max-width: 1400px;\n            margin: 0 auto;\n        }\n\n        .metrics-grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));\n            gap: 1.5rem;\n            margin-bottom: 2rem;\n        }\n\n        .metric-card {\n            background: white;\n            padding: 1.5rem;\n            border-radius: 8px;\n            box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n        }\n\n        .metric-label {\n            font-size: 0.875rem;\n            color: #6b7280;\n            margin-bottom: 0.5rem;\n        }\n\n        .metric-value {\n            font-size: 2rem;\n            font-weight: 700;\n            color: #1f2937;\n        }\n\n        .progress-bar {\n            width: 100%;\n            height: 8px;\n            background: #e5e7eb;\n            border-radius: 4px;\n            overflow: hidden;\n            margin-top: 0.5rem;\n        }\n\n        .progress-fill {\n            height: 100%;\n            background: #3b82f6;\n            transition: width 0.3s ease;\n        }\n\n        .progress-fill.warning {\n            background: #f59e0b;\n        }\n\n        .progress-fill.danger {\n            background: #ef4444;\n        }\n    </style>\n</head>\n<body>\n    <div class="header">\n        <h1>\n            لوحة معلومات في الوقت الفعلي\n            <span id="connectionStatus" class="connection-status status-disconnected">غير متصل</span>\n        </h1>\n    </div>\n\n    <div class="dashboard">\n        <div class="metrics-grid">\n            <div class="metric-card">\n                <div class="metric-label">المستخدمون النشطون</div>\n                <div class="metric-value"><span id="activeUsers">0</span></div>\n            </div>\n            <div class="metric-card">\n                <div class="metric-label">الطلبات/دقيقة</div>\n                <div class="metric-value"><span id="requestsPerMin">0</span></div>\n            </div>\n            <div class="metric-card">\n                <div class="metric-label">متوسط زمن الاستجابة</div>\n                <div class="metric-value"><span id="avgResponse">0</span> <span class="metric-unit">ms</span></div>\n            </div>\n        </div>\n\n        <div class="chart-container">\n            <div class="chart-title">صحة النظام</div>\n            <div style="margin-bottom: 1rem;">\n                <div class="metric-label">استخدام المعالج</div>\n                <div><span id="cpuValue">0</span>%</div>\n                <div class="progress-bar">\n                    <div id="cpuProgress" class="progress-fill" style="width: 0%"></div>\n                </div>\n            </div>\n            <div>\n                <div class="metric-label">استخدام الذاكرة</div>\n                <div><span id="memValue">0</span>%</div>\n                <div class="progress-bar">\n                    <div id="memProgress" class="progress-fill" style="width: 0%"></div>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <script src="dashboard.js"></script>\n</body>\n</html>

منطق JavaScript للوحة المعلومات

نفّذ عميل WebSocket مع تصور البيانات:

// dashboard.js\nclass DashboardClient {\n    constructor() {\n        this.ws = null;\n        this.reconnectAttempts = 0;\n        this.maxReconnectAttempts = 5;\n        this.reconnectDelay = 3000;\n        this.connect();\n    }\n\n    connect() {\n        this.ws = new WebSocket('ws://localhost:3000');\n\n        this.ws.onopen = () => {\n            console.log('متصل بخادم لوحة المعلومات');\n            this.reconnectAttempts = 0;\n            this.updateConnectionStatus(true);\n        };\n\n        this.ws.onmessage = (event) => {\n            const message = JSON.parse(event.data);\n            this.handleMessage(message);\n        };\n\n        this.ws.onerror = (error) => {\n            console.error('خطأ WebSocket:', error);\n        };\n\n        this.ws.onclose = () => {\n            console.log('انقطع الاتصال بخادم لوحة المعلومات');\n            this.updateConnectionStatus(false);\n            this.attemptReconnect();\n        };\n    }\n\n    handleMessage(message) {\n        switch(message.type) {\n            case 'initial':\n                this.updateAllMetrics(message.data);\n                break;\n            case 'activeUsers':\n                this.updateActiveUsers(message.data);\n                break;\n            case 'systemHealth':\n                this.updateSystemHealth(message.data);\n                break;\n            case 'realtimeStats':\n                this.updateRealtimeStats(message.data);\n                break;\n            case 'userActivity':\n                this.updateActivityFeed(message.data);\n                break;\n            case 'alert':\n                this.addAlert(message.data);\n                break;\n        }\n    }\n\n    updateConnectionStatus(connected) {\n        const statusEl = document.getElementById('connectionStatus');\n        if (connected) {\n            statusEl.textContent = 'متصل';\n            statusEl.className = 'connection-status status-connected';\n        } else {\n            statusEl.textContent = 'غير متصل';\n            statusEl.className = 'connection-status status-disconnected';\n        }\n    }\n\n    updateSystemHealth(health) {\n        // تحديث المعالج\n        document.getElementById('cpuValue').textContent = health.cpu.toFixed(1);\n        const cpuProgress = document.getElementById('cpuProgress');\n        cpuProgress.style.width = health.cpu + '%';\n        cpuProgress.className = 'progress-fill ' + this.getHealthClass(health.cpu);\n\n        // تحديث الذاكرة\n        document.getElementById('memValue').textContent = health.memory.toFixed(1);\n        const memProgress = document.getElementById('memProgress');\n        memProgress.style.width = health.memory + '%';\n        memProgress.className = 'progress-fill ' + this.getHealthClass(health.memory);\n    }\n\n    getHealthClass(value) {\n        if (value > 85) return 'danger';\n        if (value > 70) return 'warning';\n        return '';\n    }\n\n    attemptReconnect() {\n        if (this.reconnectAttempts < this.maxReconnectAttempts) {\n            this.reconnectAttempts++;\n            console.log(`إعادة الاتصال... المحاولة ${this.reconnectAttempts}`);\n            setTimeout(() => this.connect(), this.reconnectDelay);\n        } else {\n            console.error('تم الوصول إلى الحد الأقصى لمحاولات إعادة الاتصال');\n        }\n    }\n}\n\n// تهيئة لوحة المعلومات\nconst dashboard = new DashboardClient();
أفضل الممارسات: نفّذ أنماط التحديث التلقائي مع التراجع الأسي، استخدم هياكل بيانات فعالة للبيانات الزمنية، نفّذ تجميع البيانات لتقليل النطاق الترددي، أضف التخزين المؤقت من جانب العميل للبيانات التاريخية، ووفّر قدرات التحديث اليدوي.

الميزات المتقدمة للوحة المعلومات

عزّز لوحة المعلومات بقدرات إضافية:

// الميزات المتقدمة لـ dashboard.js\n\n// تخزين البيانات الزمنية\nclass TimeSeriesStore {\n    constructor(maxPoints = 60) {\n        this.maxPoints = maxPoints;\n        this.data = [];\n    }\n\n    add(value, timestamp = Date.now()) {\n        this.data.push({ value, timestamp });\n        if (this.data.length > this.maxPoints) {\n            this.data.shift();\n        }\n    }\n\n    getLatest(count = 10) {\n        return this.data.slice(-count);\n    }\n\n    getAverage() {\n        if (this.data.length === 0) return 0;\n        const sum = this.data.reduce((acc, item) => acc + item.value, 0);\n        return sum / this.data.length;\n    }\n}\n\n// تصدير البيانات\nclass DataExporter {\n    static exportToCSV(data, filename) {\n        const csv = this.convertToCSV(data);\n        const blob = new Blob([csv], { type: 'text/csv' });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.href = url;\n        a.download = filename;\n        a.click();\n        URL.revokeObjectURL(url);\n    }\n\n    static convertToCSV(data) {\n        if (data.length === 0) return '';\n        const headers = Object.keys(data[0]).join(',');\n        const rows = data.map(row => \n            Object.values(row).map(val => \n                typeof val === 'string' ? `"${val}"` : val\n            ).join(',')\n        );\n        return [headers, ...rows].join('\n');\n    }\n}
اعتبارات الأداء: كن حذراً من استخدام الذاكرة عند تخزين البيانات الزمنية في المتصفح. حدد عدد نقاط البيانات المحفوظة، استخدم هياكل بيانات فعالة، نفّذ تجميع البيانات للبيانات القديمة، وفكّر في نقل تحليل البيانات التاريخية إلى الخادم.

أنماط التحديث التلقائي للوحة المعلومات

نفّذ استراتيجيات التحديث التلقائي الذكية:

// مدير التحديث التلقائي\nclass AutoRefreshManager {\n    constructor() {\n        this.refreshIntervals = new Map();\n        this.isPageVisible = true;\n        this.setupVisibilityHandling();\n    }\n\n    setupVisibilityHandling() {\n        document.addEventListener('visibilitychange', () => {\n            this.isPageVisible = !document.hidden;\n            if (this.isPageVisible) {\n                this.resumeAllRefreshes();\n            } else {\n                this.pauseAllRefreshes();\n            }\n        });\n    }\n\n    register(name, callback, interval, runImmediately = false) {\n        if (this.refreshIntervals.has(name)) {\n            this.unregister(name);\n        }\n\n        if (runImmediately) {\n            callback();\n        }\n\n        const intervalId = setInterval(() => {\n            if (this.isPageVisible) {\n                callback();\n            }\n        }, interval);\n\n        this.refreshIntervals.set(name, {\n            callback,\n            interval,\n            intervalId,\n            isPaused: false\n        });\n    }\n\n    pauseAllRefreshes() {\n        this.refreshIntervals.forEach((refresh) => {\n            if (!refresh.isPaused) {\n                clearInterval(refresh.intervalId);\n                refresh.isPaused = true;\n            }\n        });\n    }\n}\n\n// الاستخدام\nconst refreshManager = new AutoRefreshManager();\n\nrefreshManager.register('systemHealth', () => {\n    console.log('تحديث صحة النظام...');\n}, 5000, true);
تحدي المشروع: وسّع لوحة المعلومات هذه لتشمل: 1) رسم بياني صغير لاتجاهات المعالج/الذاكرة عبر الزمن، 2) عتبات مقاييس قابلة للتخصيص مع مؤشرات بصرية، 3) إعادة ترتيب عناصر لوحة المعلومات بالسحب والإفلات، 4) وظيفة التصدير لبيانات المقاييس إلى CSV، 5) استمرارية تفضيلات المستخدم باستخدام localStorage، 6) تبديل الوضع الداكن مع استمرارية المظهر.

اختبار لوحة المعلومات

ابدأ الخادم واختبر جميع الميزات:

# الطرفية 1: ابدأ خادم لوحة المعلومات\nnode dashboard-server.js\n\n# الطرفية 2: قدّم HTML للوحة المعلومات (أو افتح مباشرة في المتصفح)\npython -m http.server 8000\n\n# افتح المتصفح على http://localhost:8000\n# لاحظ:\n# - تحديثات حالة الاتصال\n# - تحديثات المقاييس المباشرة كل 2-3 ثوانٍ\n# - أشرطة تقدم صحة النظام\n# - تدفق نشاط المستخدم\n# - إشعارات التنبيه\n# - الرسوم المتحركة والانتقالات السلسة
النقاط الرئيسية: تتطلب لوحات المعلومات في الوقت الفعلي تخطيطاً دقيقاً للبنية، واستراتيجيات بث بيانات فعالة، وتصميم واجهة مستخدم سريع الاستجابة، وأنماط تحديث تلقائي ذكية، وتحسين الأداء. استخدم WebSockets للتحديثات الفورية، نفّذ تجميع البيانات من جانب العميل، وفّر تعليقات بصرية لجميع تغييرات الحالة، وتأكد من التدهور الرشيق عند فشل الاتصالات.