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

الأحداث ومعالجة الأحداث

17 دقيقة الدرس 8 من 35

الأحداث ومعالجة الأحداث

الأحداث هي آلية الاتصال الأساسية في Socket.io. فهم كيفية إنشاء الأحداث وإرسالها والاستماع إليها ومعالجتها بشكل صحيح أمر ضروري لبناء تطبيقات في الوقت الفعلي قوية.

الأحداث المضمنة

يوفر Socket.io عدة أحداث مضمنة لا يجب عليك إرسالها يدويًا أبدًا:

// الأحداث المضمنة على جانب العميل socket.on('connect', () => { // يتم تشغيله عند الاتصال بنجاح }); socket.on('disconnect', (reason) => { // يتم تشغيله عند قطع الاتصال }); socket.on('connect_error', (error) => { // يتم تشغيله عند فشل الاتصال }); // الأحداث المضمنة على جانب الخادم io.on('connection', (socket) => { // يتم تشغيله عند اتصال العميل socket.on('disconnect', (reason) => { // يتم تشغيله عند قطع اتصال العميل }); socket.on('disconnecting', () => { // يتم تشغيله قبل قطع الاتصال (الغرف لا تزال متاحة) }); });
تحذير: لا ترسل أبدًا الأحداث المضمنة يدويًا مثل connect أو disconnect أو connection وما إلى ذلك. هذه محجوزة بواسطة Socket.io وإرسالها سيسبب سلوكًا غير متوقع.

الأحداث المخصصة

يمكنك إنشاء أحداث مخصصة بأي اسم (باستثناء الأسماء المحجوزة):

// الخادم io.on('connection', (socket) => { // إرسال حدث مخصص للعميل socket.emit('welcomeMessage', 'مرحبًا أيها المستخدم الجديد!'); socket.emit('serverTime', new Date().toISOString()); socket.emit('userData', { id: 123, name: 'أحمد' }); // الاستماع للأحداث المخصصة من العميل socket.on('chatMessage', (message) => { console.log('تم الاستلام:', message); }); socket.on('userAction', (action, data) => { console.log(`المستخدم نفذ ${action}:`, data); }); }); // العميل socket.emit('chatMessage', 'مرحبًا بالجميع!'); socket.emit('userAction', 'click', { button: 'submit' }); socket.on('welcomeMessage', (msg) => { console.log(msg); }); socket.on('userData', (user) => { console.log('بيانات المستخدم:', user); });

اصطلاحات تسمية الأحداث

اتبع أفضل الممارسات لتسمية الأحداث:

// جيد - واضح ووصفي socket.emit('chatMessage', data); socket.emit('userJoined', username); socket.emit('gameStateUpdate', state); socket.emit('notificationReceived', notification); // جيد - أحداث بفضاءات أسماء للتنظيم socket.emit('chat:message', data); socket.emit('chat:typing', isTyping); socket.emit('game:start', gameId); socket.emit('game:move', moveData); // تجنب - عام جدًا socket.emit('data', data); socket.emit('update', update); socket.emit('event', event); // تجنب - أسماء محجوزة socket.emit('connect', data); // محجوز! socket.emit('disconnect', data); // محجوز!
نصيحة: استخدم نقطتين أو شرطة سفلية لفضاء أسماء الأحداث ذات الصلة (مثل chat:message، chat:typing) للحفاظ على تنظيم الكود، خاصة في التطبيقات الكبيرة.

إقرارات الأحداث (ردود الاستدعاء)

يمكنك طلب إقرار عند إرسال الأحداث، مشابهًا لطلب HTTP واستجابته:

// العميل - الإرسال مع رد استدعاء socket.emit('createUser', { name: 'أحمد', email: 'ahmad@example.com' }, (response) => { if (response.success) { console.log('تم إنشاء المستخدم بمعرف:', response.userId); } else { console.error('خطأ:', response.error); } }); // الخادم - استدعاء رد الاستدعاء io.on('connection', (socket) => { socket.on('createUser', (userData, callback) => { // التحقق من البيانات if (!userData.name || !userData.email) { callback({ success: false, error: 'حقول مطلوبة مفقودة' }); return; } // إنشاء المستخدم const userId = Math.random().toString(36).substr(2, 9); // إرسال الاستجابة عبر رد الاستدعاء callback({ success: true, userId: userId }); }); });

يمكن أيضًا استخدام الإقرارات من الخادم إلى العميل:

// الخادم - الإرسال مع رد استدعاء socket.emit('confirmAction', 'حذف جميع الرسائل؟', (confirmed) => { if (confirmed) { console.log('المستخدم أكد الحذف'); // المتابعة بالحذف } else { console.log('المستخدم ألغى'); } }); // العميل - الاستجابة لرد الاستدعاء socket.on('confirmAction', (message, callback) => { const confirmed = confirm(message); callback(confirmed); });

مهلة للإقرارات

حدد مهلة للتعامل مع الحالات التي لا يستجيب فيها الطرف الآخر:

// Socket.io 4.0+ socket.timeout(5000).emit('requestData', (err, response) => { if (err) { // حدثت مهلة console.error('انتهت مهلة الطلب'); } else { console.log('تم استلام الاستجابة:', response); } }); // معالجة المهلة يدويًا const timeoutId = setTimeout(() => { console.error('مهلة يدوية'); }, 5000); socket.emit('requestData', (response) => { clearTimeout(timeoutId); console.log('الاستجابة:', response); });

الأحداث المتطايرة

الأحداث المتطايرة لا يتم تخزينها مؤقتًا وستفقد إذا لم يكن العميل جاهزًا لاستقبالها. مفيدة للبيانات في الوقت الفعلي التي تصبح قديمة بسرعة:

// الخادم - إرسال حدث متطاير socket.volatile.emit('cursorPosition', { x: 100, y: 200 }); socket.volatile.emit('gameTickUpdate', gameState); // إذا لم يكن العميل جاهزًا، يتم إسقاط هذه الأحداث // جيد للتحديثات عالية التردد حيث يكون الفقدان العرضي مقبولاً
ملاحظة: استخدم الأحداث المتطايرة للتحديثات عالية التردد مثل مواضع المؤشر أو تحديثات اللعبة أو بيانات المستشعر حيث يكون فقدان بعض التحديثات مقبولاً. للأحداث الحرجة مثل رسائل الدردشة أو المعاملات، استخدم emit العادي.

أحداث الأخطاء

تعامل مع الأخطاء بشكل صحيح مع أحداث أخطاء مخصصة:

// الخادم - إرسال الأخطاء io.on('connection', (socket) => { socket.on('performAction', (data) => { try { // معالجة الإجراء if (!data.userId) { throw new Error('معرف المستخدم مطلوب'); } // نجاح socket.emit('actionSuccess', { message: 'اكتمل الإجراء' }); } catch (error) { // إرسال الخطأ للعميل socket.emit('actionError', { message: error.message, code: 'ACTION_FAILED' }); } }); // معالجة الأخطاء على مستوى السوكيت socket.on('error', (error) => { console.error('خطأ في السوكيت:', error); }); }); // العميل - الاستماع للأخطاء socket.on('actionError', (error) => { alert(`خطأ: ${error.message}`); console.error('رمز الخطأ:', error.code); }); socket.on('error', (error) => { console.error('خطأ في الاتصال:', error); });

مستمعو الالتقاط الشامل (onAny)

استمع لجميع الأحداث باستخدام مستمع التقاط شامل، مفيد للتسجيل وتصحيح الأخطاء:

// الخادم - الاستماع لجميع الأحداث الواردة io.on('connection', (socket) => { socket.onAny((eventName, ...args) => { console.log(`حدث مستلم: ${eventName}`); console.log('المعاملات:', args); }); // الاستماع لجميع الأحداث الصادرة socket.onAnyOutgoing((eventName, ...args) => { console.log(`حدث مرسل: ${eventName}`); console.log('المعاملات:', args); }); }); // العميل - الاستماع لجميع الأحداث من الخادم socket.onAny((eventName, ...args) => { console.log(`حدث مستلم: ${eventName}`, args); }); // إزالة مستمع الالتقاط الشامل socket.offAny(listenerFunction);
نصيحة: استخدم onAny() أثناء التطوير لتصحيح الأخطاء، لكن كن حذرًا في الإنتاج لأنه يمكن أن يؤثر على الأداء إذا قمت بتسجيل كل حدث.

إزالة مستمعي الأحداث

نظف المستمعين لمنع تسرب الذاكرة:

// إزالة مستمع محدد const messageHandler = (msg) => { console.log('الرسالة:', msg); }; socket.on('chatMessage', messageHandler); socket.off('chatMessage', messageHandler); // إزالة // إزالة جميع المستمعين لحدث socket.removeAllListeners('chatMessage'); // إزالة جميع المستمعين لجميع الأحداث socket.removeAllListeners(); // مستمع لمرة واحدة (يزيل نفسه تلقائيًا بعد أول تشغيل) socket.once('welcome', (msg) => { console.log('رسالة الترحيب مستلمة مرة واحدة:', msg); });

أفضل ممارسات بيانات الأحداث

// جيد - إرسال بيانات منظمة socket.emit('chatMessage', { userId: '12345', username: 'أحمد', message: 'مرحبًا!', timestamp: Date.now(), room: 'عام' }); // جيد - استخدام أشكال بيانات متسقة socket.emit('notification', { type: 'info', // info, warning, error title: 'رسالة جديدة', message: 'لديك رسالة جديدة', timestamp: Date.now() }); // تجنب - إرسال الكثير من البيانات socket.emit('userUpdate', entireUserDatabase); // كبير جدًا! // تجنب - هياكل بيانات غير متسقة socket.emit('message', 'just a string'); // أحيانًا سلسلة نصية socket.emit('message', { text: 'object' }); // أحيانًا كائن

مثال كامل لمعالجة الأحداث

// الخادم const express = require('express'); const { createServer } = require('http'); const { Server } = require('socket.io'); const app = express(); const httpServer = createServer(app); const io = new Server(httpServer); io.on('connection', (socket) => { console.log('مستخدم متصل:', socket.id); // إرسال ترحيب مع إقرار socket.emit('welcome', 'مرحبًا بك في الدردشة!', (response) => { console.log('العميل أقر:', response); }); // معالجة رسائل الدردشة socket.on('chat:message', (data, callback) => { // التحقق if (!data.message || data.message.trim() === '') { callback({ success: false, error: 'رسالة فارغة' }); return; } // البث للجميع io.emit('chat:message', { id: socket.id, message: data.message, timestamp: new Date() }); callback({ success: true }); }); // معالجة مؤشر الكتابة (متطاير) socket.on('chat:typing', (isTyping) => { socket.volatile.broadcast.emit('chat:typing', { userId: socket.id, isTyping: isTyping }); }); // الالتقاط الشامل لتصحيح الأخطاء socket.onAny((eventName, ...args) => { console.log(`📥 ${eventName}:`, args); }); // معالجة الأخطاء socket.on('error', (error) => { console.error('خطأ في السوكيت:', error); }); socket.on('disconnect', () => { console.log('مستخدم قطع الاتصال:', socket.id); }); }); httpServer.listen(3000);
تمرين: أنشئ تطبيق Socket.io مع معالجة الأحداث:
  • تنفيذ أحداث مخصصة لرسائل الدردشة وكتابة المستخدم وحالة المستخدم
  • استخدام الإقرارات لتأكيد تسليم الرسالة
  • إضافة أحداث أخطاء لفشل التحقق
  • تنفيذ أحداث متطايرة لمؤشرات الكتابة
  • استخدام مستمع التقاط شامل لتسجيل جميع الأحداث أثناء التطوير
  • إنشاء مستمع حدث لمرة واحدة لرسالة الترحيب
  • اتباع اصطلاحات التسمية مع أحداث بفضاءات أسماء (chat:message, user:typing)