بناء تطبيق دردشة فوري متقدم باستخدام Node.js وSocket.io وMySQL وSequelize

Amine
23/09/2024

مقدمة

في عالمنا الرقمي المتسارع، أصبحت تطبيقات الدردشة الفورية جزءًا أساسيًا من التواصل اليومي. في هذا المقال، سنستكشف كيفية بناء تطبيق دردشة فوري متقدم باستخدام Node.js وSocket.io مع استخدام MySQL وقاعدة بيانات علائقية بمساعدة Sequelize. سنقوم ببناء تطبيق دردشة يتيح للمستخدمين التسجيل وتسجيل الدخول، وإرسال الرسائل بأسماء المستخدمين، بالإضافة إلى إنشاء غرف دردشة متعددة وتخزين الرسائل. إذا كنت ترغب في تطوير مهاراتك في برمجة الويب وتطبيقات الوقت الحقيقي، فهذا المقال موجه لك.

المتطلبات المسبقة

  • معرفة أساسية بـ JavaScript.
  • معرفة بأساسيات Node.js و npm.
  • تثبيت Node.js و npm على جهازك.
  • فهم أساسي لـ HTML و CSS.
  • MySQL مثبت ومهيأ على جهازك أو الوصول إلى خادم MySQL.
  • معرفة بأساسيات MySQL.
  • تثبيت Sequelize CLI (اختياري ولكن يوصى به).

خطوة 1: إعداد بيئة العمل

إذا لم تقم بذلك بالفعل، قم بإنشاء مجلد جديد للمشروع وانتقل إليه عبر سطر الأوامر:

mkdir sequelize-chat-app
cd sequelize-chat-app

تهيئة مشروع Node.js جديد:

npm init -y

تثبيت التبعيات اللازمة:

npm install express socket.io sequelize mysql2 bcryptjs express-session connect-session-sequelize

شرح التبعيات:

  • express: إطار عمل لتطبيقات الويب.
  • socket.io: مكتبة للتواصل في الوقت الحقيقي.
  • sequelize: ORM للتعامل مع قواعد البيانات العلائقية.
  • mysql2: برنامج تشغيل MySQL لـ Node.js.
  • bcryptjs: لتشفير كلمات المرور.
  • express-session: لإدارة الجلسات.
  • connect-session-sequelize: لتخزين الجلسات في قاعدة البيانات باستخدام Sequelize.

خطوة 2: إعداد قاعدة البيانات

تأكد من أن لديك قاعدة بيانات MySQL تعمل. يمكنك إنشاء قاعدة بيانات جديدة باسم chat_app:

CREATE DATABASE chat_app;

خطوة 3: إعداد Sequelize

1. تهيئة Sequelize

أنشئ ملفًا باسم config.js لإعداد اتصال Sequelize بقاعدة البيانات:

// config.js
const { Sequelize } = require('sequelize');

const sequelize = new Sequelize('chat_app', 'اسم_المستخدم', 'كلمة_المرور', {
  host: 'localhost',
  dialect: 'mysql',
  logging: false
});

module.exports = sequelize;

استبدل اسم_المستخدم و كلمة_المرور بمعلومات تسجيل الدخول الخاصة بقاعدة البيانات لديك.

2. إنشاء النماذج (Models)

أنشئ مجلدًا باسم models داخل مجلد المشروع.

نموذج المستخدم

إنشاء ملف models/User.js:

// models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config');

const User = sequelize.define('User', {
  username: { type: DataTypes.STRING, unique: true, allowNull: false },
  password: { type: DataTypes.STRING, allowNull: false }
});

module.exports = User;

نموذج الرسالة

إنشاء ملف models/Message.js:

// models/Message.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config');

const Message = sequelize.define('Message', {
  room: { type: DataTypes.STRING, allowNull: false },
  user: { type: DataTypes.STRING, allowNull: false },
  msg: { type: DataTypes.TEXT, allowNull: false },
  timestamp: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
});

module.exports = Message;

تزامن النماذج مع قاعدة البيانات

في ملف server.js (الذي سننشئه في الخطوة التالية)، سنقوم بتزامن النماذج مع قاعدة البيانات لإنشاء الجداول تلقائيًا.

خطوة 4: إنشاء خادم Express مع نظام المصادقة

أنشئ ملف server.js وأضف الكود التالي:

// server.js
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
const session = require('express-session');
const SequelizeStore = require('connect-session-sequelize')(session.Store);
const bcrypt = require('bcryptjs');

const sequelize = require('./config');
const User = require('./models/User');
const Message = require('./models/Message');

// إعداد الجلسات
const sessionStore = new SequelizeStore({
  db: sequelize,
});

const sessionMiddleware = session({
  secret: 'سر سري للغاية',
  resave: false,
  saveUninitialized: false,
  store: sessionStore
});

app.use(express.static(__dirname + '/public'));
app.use(express.urlencoded({ extended: true }));
app.use(sessionMiddleware);

// مشاركة الجلسة مع Socket.io
io.use((socket, next) => {
  sessionMiddleware(socket.request, socket.request.res || {}, next);
});

// تزامن النماذج مع قاعدة البيانات
sequelize.sync({ alter: true }).then(() => {
  console.log('تم تزامن قاعدة البيانات');
});

app.get('/', (req, res) => {
  if (req.session.userId) {
    res.sendFile(__dirname + '/public/chat.html');
  } else {
    res.redirect('/login');
  }
});

app.get('/login', (req, res) => {
  res.sendFile(__dirname + '/public/login.html');
});

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = await User.findOne({ where: { username } });
  if (user) {
    const valid = await bcrypt.compare(password, user.password);
    if (valid) {
      req.session.userId = user.id;
      req.session.username = user.username;
      return res.redirect('/');
    }
  }
  res.redirect('/login');
});

app.get('/register', (req, res) => {
  res.sendFile(__dirname + '/public/register.html');
});

app.post('/register', async (req, res) => {
  const { username, password } = req.body;
  const hash = await bcrypt.hash(password, 10);
  try {
    const user = await User.create({ username, password: hash });
    req.session.userId = user.id;
    req.session.username = user.username;
    res.redirect('/');
  } catch (e) {
    res.redirect('/register');
  }
});

app.get('/logout', (req, res) => {
  req.session.destroy();
  res.redirect('/login');
});

// Socket.io

io.on('connection', (socket) => {
  const session = socket.request.session;
  if (!session.userId) {
    return socket.disconnect(true);
  }

  const username = session.username;
  let currentRoom = 'عام';

  socket.join(currentRoom);
  console.log(`${username} متصل`);

  // إرسال الرسائل السابقة
  Message.findAll({ where: { room: currentRoom }, order: [['timestamp', 'ASC']] })
    .then((messages) => {
      socket.emit('load messages', messages);
    });

  // إشعار الانضمام
  socket.broadcast.to(currentRoom).emit('chat message', {
    user: 'النظام',
    msg: `${username} انضم إلى الغرفة`,
    timestamp: Date.now()
  });

  socket.on('chat message', (msg) => {
    Message.create({
      room: currentRoom,
      user: username,
      msg
    }).then((message) => {
      io.to(currentRoom).emit('chat message', {
        user: username,
        msg,
        timestamp: message.timestamp
      });
    });
  });

  socket.on('switch room', (newRoom) => {
    socket.leave(currentRoom);

    // إشعار المغادرة
    socket.broadcast.to(currentRoom).emit('chat message', {
      user: 'النظام',
      msg: `${username} غادر الغرفة`,
      timestamp: Date.now()
    });

    currentRoom = newRoom;
    socket.join(currentRoom);

    // إرسال الرسائل السابقة
    Message.findAll({ where: { room: currentRoom }, order: [['timestamp', 'ASC']] })
      .then((messages) => {
        socket.emit('load messages', messages);
      });

    // إشعار الانضمام
    socket.broadcast.to(currentRoom).emit('chat message', {
      user: 'النظام',
      msg: `${username} انضم إلى الغرفة`,
      timestamp: Date.now()
    });
  });

  socket.on('disconnect', () => {
    console.log(`${username} قطع الاتصال`);
    socket.broadcast.to(currentRoom).emit('chat message', {
      user: 'النظام',
      msg: `${username} قطع الاتصال`,
      timestamp: Date.now()
    });
  });
});

http.listen(3000, () => {
  console.log('الخادم يعمل على المنفذ 3000');
});

شرح الكود

  • الاتصال بقاعدة البيانات: قمنا بإعداد اتصال مع MySQL باستخدام Sequelize في config.js.
  • النماذج (Models): أنشأنا نماذج User و Message باستخدام Sequelize، والتي تمثل الجداول في قاعدة البيانات.
  • إدارة الجلسات: استخدمنا express-session مع connect-session-sequelize لتخزين الجلسات في قاعدة البيانات.
  • المصادقة: أنشأنا مسارات للتسجيل وتسجيل الدخول وتسجيل الخروج، مع تشفير كلمات المرور باستخدام bcryptjs.
  • Socket.io مع الجلسات: قمنا بمشاركة الجلسة مع Socket.io للتحقق من تسجيل دخول المستخدم.
  • غرف الدردشة: سمحنا للمستخدمين بالانضمام إلى غرف دردشة مختلفة وتخزين الرسائل الخاصة بكل غرفة.
  • تخزين الرسائل: قمنا بتخزين الرسائل في قاعدة البيانات باستخدام Sequelize وجلب الرسائل السابقة عند الانضمام إلى غرفة.
  • تزامن النماذج: استخدمنا sequelize.sync({ alter: true }) لتزامن النماذج مع قاعدة البيانات وإنشاء الجداول تلقائيًا.

خطوة 5: إنشاء الواجهة الأمامية

الواجهة الأمامية ستبقى كما هي من المقال السابق، مع بعض التعديلات الطفيفة إذا لزم الأمر. داخل مجلد public، تأكد من وجود الملفات التالية:

1. login.html

<!DOCTYPE html>
<html lang="ar">
<head>
  <meta charset="UTF-8">
  <title>تسجيل الدخول</title>
  <style>
    body { font-family: Arial, sans-serif; direction: rtl; text-align: right; }
    form { max-width: 300px; margin: auto; }
    input { width: 100%; padding: 10px; margin: 5px 0; }
    button { width: 100%; padding: 10px; }
  </style>
</head>
<body>
  <h2>تسجيل الدخول</h2>
  <form action="/login" method="POST">
    <input type="text" name="username" placeholder="اسم المستخدم" required>
    <input type="password" name="password" placeholder="كلمة المرور" required>
    <button type="submit">تسجيل الدخول</button>
  </form>
  <p>ليس لديك حساب؟ <a href="/register">إنشاء حساب جديد</a></p>
</body>
</html>

2. register.html

<!DOCTYPE html>
<html lang="ar">
<head>
  <meta charset="UTF-8">
  <title>إنشاء حساب</title>
  <style>
    body { font-family: Arial, sans-serif; direction: rtl; text-align: right; }
    form { max-width: 300px; margin: auto; }
    input { width: 100%; padding: 10px; margin: 5px 0; }
    button { width: 100%; padding: 10px; }
  </style>
</head>
<body>
  <h2>إنشاء حساب جديد</h2>
  <form action="/register" method="POST">
    <input type="text" name="username" placeholder="اسم المستخدم" required>
    <input type="password" name="password" placeholder="كلمة المرور" required>
    <button type="submit">إنشاء حساب</button>
  </form>
  <p>لديك حساب بالفعل؟ <a href="/login">تسجيل الدخول</a></p>
</body>
</html>

3. chat.html

<!DOCTYPE html>
<html lang="ar">
<head>
  <meta charset="UTF-8">
  <title>تطبيق الدردشة</title>
  <style>
    body { font-family: Arial, sans-serif; direction: rtl; text-align: right; }
    #messages { list-style-type: none; padding: 0; margin-bottom: 50px; }
    #messages li { padding: 5px 10px; }
    #messages li:nth-child(odd) { background: #f1f1f1; }
    form { display: flex; position: fixed; bottom: 0; width: 100%; }
    input { flex: 1; padding: 10px; border: none; }
    button { padding: 10px; background: #4CAF50; color: #fff; border: none; }
    .username { font-weight: bold; }
    .system-message { color: gray; }
  </style>
</head>
<body>
  <a href="/logout">تسجيل الخروج</a>
  <select id="room-select">
    <option value="عام">عام</option>
    <option value="رياضة">رياضة</option>
    <option value="تكنولوجيا">تكنولوجيا</option>
  </select>
  <ul id="messages"></ul>
  <form id="form">
    <input id="input" autocomplete="off" placeholder="اكتب رسالتك..." />
    <button>إرسال</button>
  </form>

  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io();

    const form = document.getElementById('form');
    const input = document.getElementById('input');
    const roomSelect = document.getElementById('room-select');
    const messagesList = document.getElementById('messages');

    form.addEventListener('submit', function(e) {
      e.preventDefault();
      if (input.value) {
        socket.emit('chat message', input.value);
        input.value = '';
      }
    });

    roomSelect.addEventListener('change', function() {
      messagesList.innerHTML = '';
      socket.emit('switch room', roomSelect.value);
    });

    socket.on('load messages', function(messages) {
      messagesList.innerHTML = '';
      messages.forEach(function(data) {
        displayMessage(data);
      });
    });

    socket.on('chat message', function(data) {
      displayMessage(data);
    });

    function displayMessage(data) {
      const item = document.createElement('li');
      if (data.user === 'النظام') {
        item.classList.add('system-message');
        item.textContent = data.msg;
      } else {
        item.innerHTML = '<span class="username">' + data.user + ':</span> ' + data.msg;
      }
      messagesList.appendChild(item);
      window.scrollTo(0, document.body.scrollHeight);
    }
  </script>
</body>
</html>

خطوة 6: تشغيل التطبيق

لتشغيل التطبيق، نفذ الأمر التالي في سطر الأوامر:

node server.js

افتح المتصفح وانتقل إلى http://localhost:3000/ لتجربة تطبيق الدردشة. يمكنك إنشاء حسابات متعددة في نوافذ مختلفة لمشاهدة الرسائل والتفاعل بين المستخدمين في الوقت الحقيقي.

خطوة 7: تحسينات إضافية

  • التحقق من المدخلات: تأكد من التحقق من بيانات المستخدم المدخلة لمنع الهجمات مثل XSS و SQL Injection.
  • تشفير الاتصال: استخدم HTTPS لتأمين الاتصالات بين العميل والخادم.
  • التعامل مع الأخطاء: أضف معالجة أفضل للأخطاء في جميع أجزاء التطبيق.
  • التصميم والواجهة: قم بتحسين تصميم الواجهة الأمامية لجعل التطبيق أكثر جاذبية وسهولة في الاستخدام.
  • الإخطارات: أضف إشعارات سطح المكتب لإعلام المستخدمين بالرسائل الجديدة.

الخاتمة

في هذا المقال، قمنا بتعديل تطبيق الدردشة الفوري لاستخدام MySQL مع Sequelize بدلاً من MongoDB. تعرفنا على كيفية إعداد Sequelize، وإنشاء النماذج، وتعديل خادم Express ونظام المصادقة ليتوافق مع MySQL. باستخدام Sequelize، يمكنك بسهولة تبديل قاعدة البيانات إلى أي قاعدة بيانات علائقية أخرى مثل PostgreSQL أو SQLite مع تعديلات بسيطة.

هذا المشروع يوفر لك فهمًا أعمق لكيفية بناء تطبيقات ويب متقدمة باستخدام Node.js و Socket.io وقواعد البيانات العلائقية. ندعوك لتجربة إضافة المزيد من الميزات والتعرف أكثر على إمكانيات Sequelize و Node.js.

إذا كان لديك أي أسئلة أو تعليقات، لا تتردد في مشاركتها أدناه. نسعد بمشاركتك ومساعدتك في رحلتك في تطوير الويب!

التعليقات

اترك تعليقاً