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

تصور البيانات في الوقت الفعلي

18 دقيقة الدرس 16 من 35

تصور البيانات في الوقت الفعلي

يحول تصور البيانات في الوقت الفعلي البيانات المتدفقة إلى تمثيلات مرئية ديناميكية وتفاعلية. تتيح WebSockets لوحات المعلومات المباشرة التي تتحدث فورًا دون تحديث الصفحة، مما يوفر للمستخدمين رؤى محدثة كل ثانية.

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

لننشئ لوحة تحليلات في الوقت الفعلي تعرض مقاييس مباشرة:

<!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <title>لوحة معلومات مباشرة</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; padding: 20px; } .metric-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .metric-value { font-size: 2.5rem; font-weight: bold; color: #2563eb; } .metric-label { color: #666; font-size: 0.9rem; } </style> </head> <body> <div class="dashboard"> <div class="metric-card"> <div class="metric-value" id="activeUsers">0</div> <div class="metric-label">المستخدمون النشطون</div> </div> <div class="metric-card"> <div class="metric-value" id="requestsPerMin">0</div> <div class="metric-label">الطلبات/دقيقة</div> </div> <div class="metric-card"> <canvas id="trafficChart"></canvas> </div> </div> <script> const ws = new WebSocket('ws://localhost:8080'); ws.onopen = () => { console.log('متصل ببث التحليلات'); ws.send(JSON.stringify({ type: 'subscribe', channel: 'analytics' })); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); updateDashboard(data); }; function updateDashboard(data) { if (data.activeUsers) { animateValue('activeUsers', data.activeUsers); } if (data.requestsPerMin) { animateValue('requestsPerMin', data.requestsPerMin); } if (data.traffic) { updateTrafficChart(data.traffic); } } function animateValue(id, value) { const element = document.getElementById(id); const current = parseInt(element.textContent); const increment = (value - current) / 20; let step = 0; const timer = setInterval(() => { step++; element.textContent = Math.round(current + (increment * step)); if (step >= 20) clearInterval(timer); }, 50); } </script> </body> </html>

بث بيانات الرسوم البيانية مع Chart.js

يتكامل Chart.js بسلاسة مع WebSockets للرسوم البيانية المباشرة:

// تهيئة الرسم البياني const ctx = document.getElementById('trafficChart').getContext('2d'); const trafficChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'حركة المرور (طلب/ثانية)', data: [], borderColor: 'rgb(37, 99, 235)', backgroundColor: 'rgba(37, 99, 235, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 750 }, scales: { x: { display: true, title: { display: true, text: 'الوقت' } }, y: { display: true, title: { display: true, text: 'الطلبات في الثانية' }, beginAtZero: true } } } }); function updateTrafficChart(traffic) { const now = new Date().toLocaleTimeString('ar-SA'); // إضافة نقطة بيانات جديدة trafficChart.data.labels.push(now); trafficChart.data.datasets[0].data.push(traffic); // الاحتفاظ بآخر 20 نقطة فقط if (trafficChart.data.labels.length > 20) { trafficChart.data.labels.shift(); trafficChart.data.datasets[0].data.shift(); } // تحديث الرسم البياني trafficChart.update('none'); // تعطيل الرسوم المتحركة للتحديثات السلسة }
نصيحة الأداء: استخدم chart.update('none') لتعطيل الرسوم المتحركة عند التحديث بشكل متكرر (أكثر من مرة في الثانية) للحفاظ على أداء سلس.

بث البيانات من جانب الخادم

ينشئ الخادم ويبث المقاييس في الوقت الفعلي:

const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); // تخزين العملاء المشتركين const analyticsSubscribers = new Set(); wss.on('connection', (ws) => { console.log('عميل متصل'); ws.on('message', (message) => { const data = JSON.parse(message); if (data.type === 'subscribe' && data.channel === 'analytics') { analyticsSubscribers.add(ws); console.log('عميل مشترك في التحليلات'); // إرسال البيانات الأولية ws.send(JSON.stringify({ activeUsers: getActiveUsers(), requestsPerMin: getRequestsPerMin(), traffic: getCurrentTraffic() })); } }); ws.on('close', () => { analyticsSubscribers.delete(ws); console.log('عميل منفصل'); }); }); // بث بيانات التحليلات كل ثانيتين setInterval(() => { const analyticsData = { activeUsers: getActiveUsers(), requestsPerMin: getRequestsPerMin(), traffic: getCurrentTraffic() }; analyticsSubscribers.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(analyticsData)); } }); }, 2000); // دوال المقاييس المحاكاة function getActiveUsers() { return Math.floor(Math.random() * 1000) + 500; } function getRequestsPerMin() { return Math.floor(Math.random() * 5000) + 2000; } function getCurrentTraffic() { return Math.floor(Math.random() * 100) + 50; }

تقييد التحديثات

منع إرباك واجهة المستخدم بعدد كبير من التحديثات:

class DataThrottler { constructor(callback, delay = 1000) { this.callback = callback; this.delay = delay; this.lastUpdate = 0; this.pendingData = null; this.timer = null; } update(data) { this.pendingData = data; const now = Date.now(); if (now - this.lastUpdate >= this.delay) { this.flush(); } else if (!this.timer) { this.timer = setTimeout(() => { this.flush(); }, this.delay - (now - this.lastUpdate)); } } flush() { if (this.pendingData) { this.callback(this.pendingData); this.lastUpdate = Date.now(); this.pendingData = null; this.timer = null; } } } // الاستخدام const dashboardThrottler = new DataThrottler(updateDashboard, 500); ws.onmessage = (event) => { const data = JSON.parse(event.data); dashboardThrottler.update(data); };

استراتيجيات تجميع البيانات

قم بتجميع البيانات على الخادم لتقليل النطاق الترددي:

class MetricsAggregator { constructor(window = 5000) { this.window = window; this.data = []; } add(value, timestamp = Date.now()) { this.data.push({ value, timestamp }); this.cleanup(); } cleanup() { const cutoff = Date.now() - this.window; this.data = this.data.filter(d => d.timestamp > cutoff); } getAverage() { if (this.data.length === 0) return 0; const sum = this.data.reduce((acc, d) => acc + d.value, 0); return Math.round(sum / this.data.length); } getMax() { if (this.data.length === 0) return 0; return Math.max(...this.data.map(d => d.value)); } getMin() { if (this.data.length === 0) return 0; return Math.min(...this.data.map(d => d.value)); } getPercentile(p) { if (this.data.length === 0) return 0; const sorted = this.data.map(d => d.value).sort((a, b) => a - b); const index = Math.ceil((p / 100) * sorted.length) - 1; return sorted[index]; } } // الاستخدام const trafficAggregator = new MetricsAggregator(5000); // إضافة نقاط البيانات عند وصولها app.use((req, res, next) => { trafficAggregator.add(1); next(); }); // بث البيانات المجمعة setInterval(() => { const stats = { avgTraffic: trafficAggregator.getAverage(), maxTraffic: trafficAggregator.getMax(), p95Traffic: trafficAggregator.getPercentile(95) }; broadcast(stats); }, 2000);
ملاحظة: يقلل التجميع من نقل البيانات بنسبة تصل إلى 90٪ مع الحفاظ على الدقة الإحصائية. أرسل ملخصات بدلاً من نقاط البيانات الخام.

تكامل D3.js

استخدم D3.js للتصورات المتقدمة في الوقت الفعلي:

const margin = { top: 20, right: 20, bottom: 30, left: 50 }; const width = 600 - margin.left - margin.right; const height = 400 - margin.top - margin.bottom; const svg = d3.select('#chart') .append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', `translate(${margin.left},${margin.top})`); const data = []; const maxDataPoints = 50; const x = d3.scaleTime().range([0, width]); const y = d3.scaleLinear().range([height, 0]); const line = d3.line() .x(d => x(d.timestamp)) .y(d => y(d.value)) .curve(d3.curveMonotoneX); const path = svg.append('path') .datum(data) .attr('class', 'line') .attr('fill', 'none') .attr('stroke', 'steelblue') .attr('stroke-width', 2); const xAxis = svg.append('g') .attr('transform', `translate(0,${height})`); const yAxis = svg.append('g'); function updateChart(newValue) { data.push({ timestamp: new Date(), value: newValue }); if (data.length > maxDataPoints) { data.shift(); } x.domain(d3.extent(data, d => d.timestamp)); y.domain([0, d3.max(data, d => d.value) * 1.1]); path.datum(data) .transition() .duration(300) .attr('d', line); xAxis.transition().duration(300).call(d3.axisBottom(x)); yAxis.transition().duration(300).call(d3.axisLeft(y)); } ws.onmessage = (event) => { const data = JSON.parse(event.data); updateChart(data.value); };

تحديث DOM بكفاءة

قم بتحسين تحديثات DOM للعرض السلس في الوقت الفعلي:

class DOMBatcher { constructor() { this.updates = []; this.rafId = null; } schedule(callback) { this.updates.push(callback); if (!this.rafId) { this.rafId = requestAnimationFrame(() => { this.flush(); }); } } flush() { this.updates.forEach(callback => callback()); this.updates = []; this.rafId = null; } } const batcher = new DOMBatcher(); ws.onmessage = (event) => { const data = JSON.parse(event.data); batcher.schedule(() => { document.getElementById('metric1').textContent = data.metric1; document.getElementById('metric2').textContent = data.metric2; document.getElementById('metric3').textContent = data.metric3; }); };
تحذير: تجنب تحديث DOM على كل رسالة WebSocket إذا وصلت الرسائل أسرع من 60 مرة في الثانية. قم بتجميع التحديثات باستخدام requestAnimationFrame.

التمرين: مؤشر الأسهم المباشر

قم ببناء مؤشر أسهم في الوقت الفعلي:
  1. قم بإنشاء خادم WebSocket يبث أسعار الأسهم المحاكاة كل 500 مللي ثانية
  2. اعرض 5 أسهم مع تغيرات الأسعار المرمزة بالألوان (الأخضر للارتفاع، الأحمر للانخفاض)
  3. اعرض رسم بياني خطي لكل سهم باستخدام Canvas API
  4. قم بتنفيذ آلية تقييد لتحديث واجهة المستخدم بحد أقصى 10 مرات في الثانية
  5. أضف عرض أعلى/أدنى سعر خلال 24 ساعة
  6. إضافي: أضف تنبيهات صوتية عندما يتحرك السهم أكثر من 5٪

الملخص

  • توفر لوحات المعلومات في الوقت الفعلي رؤى فورية دون تحديث الصفحة
  • يتكامل Chart.js و D3.js بشكل جيد مع WebSockets للرسوم البيانية المباشرة
  • قيد التحديثات لمنع تحميل واجهة المستخدم (استخدم requestAnimationFrame)
  • قم بتجميع البيانات على الخادم لتقليل استخدام النطاق الترددي
  • قم بتجميع تحديثات DOM لأداء أفضل
  • ضع في اعتبارك تجربة المستخدم: الرسوم المتحركة السلسة والانتقالات الهادفة