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

بناء تطبيق دردشة في الوقت الفعلي

20 دقيقة الدرس 13 من 35

بناء تطبيق دردشة في الوقت الفعلي

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

هندسة تطبيق الدردشة

يتكون تطبيق الدردشة الكامل من عدة مكونات رئيسية:

  • مصادقة المستخدم: التحقق من المستخدمين قبل السماح لهم بالدردشة
  • إدارة الاتصال: تتبع المستخدمين المتصلين وحالتهم
  • بث الرسائل: توزيع الرسائل على جميع المستخدمين المتصلين
  • مؤشرات الكتابة: عرض متى يكتب المستخدمون
  • سجل الرسائل: تخزين واسترجاع الرسائل السابقة
  • حضور المستخدم: عرض حالة متصل/غير متصل

إعداد الدردشة من جانب الخادم

لنبدأ بإعداد البنية من جانب الخادم لتطبيق الدردشة الخاص بنا:

// server.js - خادم الدردشة const express = require('express'); const app = express(); const http = require('http').createServer(app); const io = require('socket.io')(http); // تخزين المستخدمين المتصلين const users = new Map(); // socket.id => كائن المستخدم // تخزين رسائل الدردشة في الذاكرة (استخدم قاعدة بيانات في الإنتاج) const messages = []; // برمجيات وسيطة للمصادقة io.use((socket, next) => { const username = socket.handshake.auth.username; if (!username) { return next(new Error('اسم المستخدم مطلوب')); } socket.username = username; next(); }); io.on('connection', (socket) => { console.log(`المستخدم ${socket.username} متصل`); // إضافة المستخدم للمستخدمين المتصلين users.set(socket.id, { id: socket.id, username: socket.username, joinedAt: Date.now() }); // إرسال معرف المقبس للمستخدم socket.emit('connected', { id: socket.id, username: socket.username }); // إخطار جميع المستخدمين بانضمام شخص ما socket.broadcast.emit('userJoined', { id: socket.id, username: socket.username, timestamp: Date.now() }); // إرسال قائمة المستخدمين المتصلين حاليًا socket.emit('onlineUsers', Array.from(users.values())); // إرسال سجل الرسائل socket.emit('messageHistory', messages); // معالجة الرسائل الجديدة socket.on('sendMessage', (message) => { const messageData = { id: Date.now() + Math.random(), userId: socket.id, username: socket.username, message: message, timestamp: Date.now() }; // تخزين الرسالة messages.push(messageData); // بث لجميع المستخدمين بما في ذلك المرسل io.emit('newMessage', messageData); }); // معالجة مؤشر الكتابة socket.on('typing', () => { socket.broadcast.emit('userTyping', { username: socket.username, userId: socket.id }); }); socket.on('stopTyping', () => { socket.broadcast.emit('userStoppedTyping', { userId: socket.id }); }); // معالجة قطع الاتصال socket.on('disconnect', () => { console.log(`المستخدم ${socket.username} قطع الاتصال`); // إزالة المستخدم من القائمة المتصلة users.delete(socket.id); // إخطار الآخرين بمغادرة المستخدم socket.broadcast.emit('userLeft', { id: socket.id, username: socket.username, timestamp: Date.now() }); }); }); const PORT = 3000; http.listen(PORT, () => { console.log(`خادم الدردشة يعمل على المنفذ ${PORT}`); });
ملاحظة: يخزن هذا المثال الرسائل في الذاكرة. في الإنتاج، استخدم قاعدة بيانات مثل MongoDB أو PostgreSQL للحفاظ على سجل الدردشة.

واجهة الدردشة من جانب العميل

الآن لننشئ بنية HTML لواجهة الدردشة:

<!DOCTYPE html> <html lang="ar" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>دردشة في الوقت الفعلي</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Arial, sans-serif; background: #f0f2f5; } .chat-container { max-width: 800px; margin: 20px auto; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); display: flex; height: 600px; } .users-panel { width: 200px; border-left: 1px solid #e0e0e0; padding: 20px; } .users-panel h3 { margin-bottom: 15px; color: #333; } .user-item { padding: 8px; margin: 5px 0; background: #f5f5f5; border-radius: 4px; font-size: 14px; } .chat-panel { flex: 1; display: flex; flex-direction: column; } .chat-header { padding: 20px; border-bottom: 1px solid #e0e0e0; background: #0084ff; color: white; } .messages { flex: 1; padding: 20px; overflow-y: auto; } .message { margin: 10px 0; padding: 10px; background: #f1f3f4; border-radius: 8px; max-width: 70%; } .message.own { margin-right: auto; background: #0084ff; color: white; } .message-header { font-size: 12px; font-weight: bold; margin-bottom: 5px; } .message-text { font-size: 14px; } .message-time { font-size: 11px; opacity: 0.7; margin-top: 5px; } .typing-indicator { padding: 10px 20px; font-size: 13px; font-style: italic; color: #666; height: 30px; } .input-area { padding: 20px; border-top: 1px solid #e0e0e0; display: flex; gap: 10px; } #messageInput { flex: 1; padding: 12px; border: 1px solid #e0e0e0; border-radius: 20px; outline: none; font-size: 14px; } #sendButton { padding: 12px 30px; background: #0084ff; color: white; border: none; border-radius: 20px; cursor: pointer; font-weight: bold; } #sendButton:hover { background: #0073e6; } .system-message { text-align: center; color: #666; font-size: 13px; margin: 10px 0; font-style: italic; } </style> </head> <body> <div class="chat-container"> <div class="users-panel"> <h3>المستخدمون المتصلون</h3> <div id="usersList"></div> </div> <div class="chat-panel"> <div class="chat-header"> <h2>غرفة الدردشة</h2> </div> <div id="messages" class="messages"></div> <div id="typingIndicator" class="typing-indicator"></div> <div class="input-area"> <input type="text" id="messageInput" placeholder="اكتب رسالة..."> <button id="sendButton">إرسال</button> </div> </div> </div> <script src="/socket.io/socket.io.js"></script> <script src="chat.js"></script> </body> </html>

منطق الدردشة من جانب العميل

نفذ منطق JavaScript للتعامل مع وظائف الدردشة:

// chat.js - منطق الدردشة من جانب العميل let socket; let currentUser; let typingTimeout; let usersTyping = new Set(); // الحصول على اسم المستخدم const username = prompt('أدخل اسم المستخدم:') || `مستخدم${Math.floor(Math.random() * 1000)}`; // الاتصال بالخادم socket = io('http://localhost:3000', { auth: { username: username } }); // عناصر DOM const messagesDiv = document.getElementById('messages'); const messageInput = document.getElementById('messageInput'); const sendButton = document.getElementById('sendButton'); const usersList = document.getElementById('usersList'); const typingIndicator = document.getElementById('typingIndicator'); // تم إنشاء الاتصال socket.on('connected', (user) => { currentUser = user; console.log('متصل كـ:', user); }); // عرض المستخدمين المتصلين socket.on('onlineUsers', (users) => { usersList.innerHTML = users.map(user => `<div class="user-item">${user.username}</div>` ).join(''); }); // تحميل سجل الرسائل socket.on('messageHistory', (history) => { history.forEach(msg => displayMessage(msg)); scrollToBottom(); }); // تلقي رسالة جديدة socket.on('newMessage', (message) => { displayMessage(message); scrollToBottom(); }); // إشعار انضمام المستخدم socket.on('userJoined', (user) => { displaySystemMessage(`${user.username} انضم إلى الدردشة`); // إضافة المستخدم لقائمة المتصلين const userDiv = document.createElement('div'); userDiv.className = 'user-item'; userDiv.id = `user-${user.id}`; userDiv.textContent = user.username; usersList.appendChild(userDiv); }); // إشعار مغادرة المستخدم socket.on('userLeft', (user) => { displaySystemMessage(`${user.username} غادر الدردشة`); // إزالة المستخدم من قائمة المتصلين const userDiv = document.getElementById(`user-${user.id}`); if (userDiv) userDiv.remove(); // إزالة من مؤشر الكتابة إن وُجد usersTyping.delete(user.userId); updateTypingIndicator(); }); // مؤشر الكتابة socket.on('userTyping', (user) => { usersTyping.add(user.username); updateTypingIndicator(); }); socket.on('userStoppedTyping', (user) => { usersTyping.delete(user.username); updateTypingIndicator(); }); // إرسال رسالة function sendMessage() { const message = messageInput.value.trim(); if (message) { socket.emit('sendMessage', message); messageInput.value = ''; socket.emit('stopTyping'); } } // عرض الرسالة في الدردشة function displayMessage(message) { const messageDiv = document.createElement('div'); messageDiv.className = message.userId === currentUser.id ? 'message own' : 'message'; const time = new Date(message.timestamp).toLocaleTimeString('ar'); messageDiv.innerHTML = ` <div class="message-header">${message.username}</div> <div class="message-text">${escapeHtml(message.message)}</div> <div class="message-time">${time}</div> `; messagesDiv.appendChild(messageDiv); } // عرض رسالة النظام function displaySystemMessage(text) { const messageDiv = document.createElement('div'); messageDiv.className = 'system-message'; messageDiv.textContent = text; messagesDiv.appendChild(messageDiv); } // تحديث مؤشر الكتابة function updateTypingIndicator() { if (usersTyping.size === 0) { typingIndicator.textContent = ''; } else if (usersTyping.size === 1) { typingIndicator.textContent = `${Array.from(usersTyping)[0]} يكتب...`; } else { typingIndicator.textContent = `${usersTyping.size} أشخاص يكتبون...`; } } // معالجة اكتشاف الكتابة messageInput.addEventListener('input', () => { socket.emit('typing'); clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { socket.emit('stopTyping'); }, 2000); }); // إرسال رسالة عند النقر على الزر sendButton.addEventListener('click', sendMessage); // إرسال رسالة عند الضغط على Enter messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendMessage(); } }); // دوال مساعدة function scrollToBottom() { messagesDiv.scrollTop = messagesDiv.scrollHeight; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }
نصيحة: قم دائمًا بتنظيف المحتوى الذي ينشئه المستخدم قبل عرضه في HTML لمنع هجمات XSS (البرمجة النصية عبر المواقع).

إضافة طوابع زمنية وتنسيق

حسّن الرسائل بتنسيق أفضل للطوابع الزمنية:

// دوال مساعدة لتنسيق الوقت function formatTimestamp(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; // أقل من دقيقة واحدة if (diff < 60000) { return 'الآن'; } // أقل من ساعة واحدة if (diff < 3600000) { const minutes = Math.floor(diff / 60000); return `منذ ${minutes} دقيقة${minutes > 1 ? '' : ''}`; } // اليوم if (date.toDateString() === now.toDateString()) { return date.toLocaleTimeString('ar', { hour: '2-digit', minute: '2-digit' }); } // أمس const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); if (date.toDateString() === yesterday.toDateString()) { return 'أمس ' + date.toLocaleTimeString('ar', { hour: '2-digit', minute: '2-digit' }); } // أقدم return date.toLocaleDateString('ar') + ' ' + date.toLocaleTimeString('ar', { hour: '2-digit', minute: '2-digit' }); }

استمرارية الرسائل مع قاعدة البيانات

في الإنتاج، خزن الرسائل في قاعدة بيانات للاستمرارية:

// استخدام MongoDB مع Mongoose const mongoose = require('mongoose'); const messageSchema = new mongoose.Schema({ userId: String, username: String, message: String, timestamp: { type: Date, default: Date.now } }); const Message = mongoose.model('Message', messageSchema); // الاتصال بـ MongoDB mongoose.connect('mongodb://localhost/chat', { useNewUrlParser: true, useUnifiedTopology: true }); // حفظ الرسالة في قاعدة البيانات socket.on('sendMessage', async (message) => { const newMessage = new Message({ userId: socket.id, username: socket.username, message: message }); await newMessage.save(); io.emit('newMessage', { id: newMessage._id, userId: newMessage.userId, username: newMessage.username, message: newMessage.message, timestamp: newMessage.timestamp }); }); // تحميل سجل الرسائل من قاعدة البيانات socket.on('connection', async (socket) => { // الحصول على آخر 100 رسالة const messages = await Message.find() .sort({ timestamp: -1 }) .limit(100) .exec(); socket.emit('messageHistory', messages.reverse()); });
تمرين: حسّن تطبيق الدردشة بهذه الميزات:
  1. إضافة دعم الرموز التعبيرية باستخدام مكتبة منتقي الرموز التعبيرية
  2. تنفيذ تحرير الرسائل (نقرة مزدوجة لتحرير رسائلك الخاصة)
  3. إضافة حذف الرسائل (قائمة النقر بزر الماوس الأيمن لرسائلك الخاصة)
  4. عرض صور رمزية للمستخدمين بجانب الرسائل (استخدم Gravatar أو خدمة نائبة)
  5. إضافة "إيصالات القراءة" تعرض متى تُرى الرسائل من قبل الآخرين
  6. تنفيذ زر "انتقل إلى الأسفل" يظهر عند التمرير لأعلى
  7. إضافة إشعارات صوتية للرسائل الجديدة
  8. إنشاء وظيفة بحث للعثور على الرسائل القديمة

اختبر التطبيق بنوافذ متصفح متعددة لمحاكاة مستخدمين مختلفين.