طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 28 · الوحدة 13: التنظيم وسهولة الصيانة والتوسع

المشروع الختامي: تطبيق ويب متكامل

كل ما تعلمته في تطبيق واحد يستخدمه الناس ⏱ نحو 60 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تخطط لتطبيق ويب متكامل من المتطلبات إلى النشر.
  • تدمج React و Express و Sequelize و JWT و Socket.io في مشروع واحد.
  • تعمل في فريق بـ Git ومراجعة الكود.
  • تقيّم مشروعك بمعايير الجودة والأمان والأداء.

١المشروع: تطبيق ويب متكامل

حان وقت جمع كل ما تعلمته في تطبيق واحد حقيقي، تبنيه في فريق من 2 إلى 3 طلاب خلال الأسابيع الأخيرة. اختر فكرة تخدم مستخدمين حقيقيين في كليتك أو مجتمعك، أو اختر من الأفكار المقترحة:

الفكرةالمستخدمون والأدوارميزة فورية مقترحة
مكتبة الكلية: إعارة الكتبطالب، أمين مكتبةإشعار عند توفر كتاب محجوز
حجز قاعات ومعاملمحاضر، مشرفتحديث الجدول لحظيًّا عند الحجز
متجر النادي الطلابيمشترٍ، بائع، مشرفتتبع حالة الطلب مباشرة
منصة مشاريع التخرجطالب، مشرف أكاديميدردشة الفريق مع المشرف
بلاغات الصيانةموظف، فني، مديرإشعار الفني فور البلاغ

٢المتطلبات الإلزامية

الجانبالمطلوبالدروس
الواجهةReact بمكوّنات وخصائص وحالة، و Router لعدة صفحات، وتصميم متجاوب بـ Tailwind1–9
إدارة الحالةContext أو Redux Toolkit لحالة مشتركة (المستخدم الحالي مثلًا)5–7
الخادمExpress بهيكلة: routes و controllers و services، ووسيط أخطاء موحّد10–12، 27
قاعدة البياناتSequelize بثلاثة نماذج على الأقل وعلاقة واحد إلى متعدد13–14
الأمانتسجيل ودخول بـ bcrypt و JWT، ودوران (أدوار) على الأقل، وحماية من XSS وحقن SQL15–16، 25–26
واجهة RESTمسارات بأسلوب REST ورموز حالة صحيحة وتوثيق مختصر17–18
ميزة فوريةSocket.io لميزة واحدة على الأقل19–20
الأداءتحسين واحد مقيس على الأقل (lazy، أو cache، أو فهرس)23–24
النشربناء الواجهة، وتشغيل التطبيق في Docker أو نشره على منصة سحابية، مع ‎.env.example21–22

٣نموذج مصغّر يعمل

هذا هيكل مصغّر لفكرة «مكتبة الكلية» يجمع React و Express و Sequelize و JWT في تطبيق واحد. نافذتان لمستخدمين يتشاركان الخادم نفسه: ادخل في النافذتين، واستعر من كل واحدة «أساسيات الويب» (نسخة واحدة) لترى قاعدة النسخ المتاحة تعمل. اعتبره نقطة انطلاق فقط:

import { useState, useEffect } from 'react';import { api, setToken } from './api.js'; export default function App({ user }) {  const [me, setMe] = useState(null);  const [books, setBooks] = useState([]);  const [msg, setMsg] = useState('');   const load = () => api.get('/api/books').then(setBooks);  useEffect(() => { load(); }, []);   async function login() {    const r = await api.post('/api/login', { name: user || 'زائر' });    setToken(r.token); setMe(r.name);  }   async function borrow(id) {    try { await api.post(`/api/books/${id}/borrow`); setMsg('✓ تمت الإعارة'); load(); }    catch (e) { setMsg('⚠ ' + e.message); }  }   return (    <div>      {me ? <p>👤 {me}</p> : <button onClick={login}>دخول باسم {user || 'زائر'}</button>}      <ul>        {books.map(b => (          <li key={b.id}>            {b.title} — متاح: {b.available}{' '}            <button disabled={!me || b.available === 0} onClick={() => borrow(b.id)}>استعر</button>          </li>        ))}      </ul>      <p className="msg">{msg}</p>    </div>  );}

٤المراحل والتسليمات

الأسبوعالمرحلةالتسليم
1الفكرة والمتطلباتوثيقة صفحة واحدة: المشكلة، والمستخدمون، والأدوار، وقصص المستخدم (أريد… لكي…)
2التصميممخطط قاعدة البيانات (الجداول والعلاقات)، وجدول مسارات API، ورسم أولي للشاشات
3–4الخادمالنماذج والمسارات والمصادقة، واختبار المسارات يدويًّا (Postman أو REST Client)
5–6الواجهةالصفحات والربط بالـ API، والحالة المشتركة، والتصميم المتجاوب
7الميزة الفورية والأمانSocket.io، ومراجعة XSS والحقن والصلاحيات
8الأداء والنشرتحسين مقيس، و Docker أو نشر سحابي، و README
9العرضعرض حي 10 دقائق + أسئلة، وتسليم المستودع

٥معايير التقييم

المعيارالوزنممتاز يعني
اكتمال الوظائف25%كل قصص المستخدم الأساسية تعمل دون أخطاء
جودة الخادم وهيكلته20%طبقات واضحة، ورموز حالة صحيحة، وأخطاء موحّدة، وتحقق من المدخلات
الواجهة وتجربة المستخدم15%متجاوبة، وحالات التحميل والخطأ معروضة، وعربية سليمة الاتجاه
الأمان15%كلمات مرور مجزّأة، وصلاحيات في الخادم، ولا XSS ولا حقن، ولا أسرار في المستودع
الميزة الفورية والأداء10%تعمل بين مستخدمين، وتحسين مقيس موثّق
النشر والتوثيق10%يعمل بأمر واحد (Docker) أو برابط، و README واضح، و ‎.env.example
العمل الجماعي و Git5%مساهمة كل عضو ظاهرة في تاريخ المستودع

توقّع وتحقّق

أين تُفرض قاعدة «لا إعارة إن لم تتوفر نسخة»؟

الخادم مصدر الحقيقة.

أول تسليم في المشروع:

ابدأ بالمشكلة.

ما الذي لا يُرفع إلى المستودع؟

الأسرار.

الخلاصة

تحدٍّ برمجي: قاعدة الإعارة في الخادم

متقدم

جزء من المشروع: مسار الإعارة والإرجاع في الذاكرة. أكمل القواعد.

المطلوب:
  • POST /api/loans بالجسم {"bookId", "user"}: 404 إن لم يوجد الكتاب، و409 {"error": "لا نسخ متاحة"} إن نفدت النسخ، و409 إن استعاره المستخدم نفسه، وإلا 201 والإعارة {"id", "bookId", "user"}.
  • DELETE /api/loans/:id يُرجع الكتاب (204)، أو 404.
  • GET /api/books يعرض لكل كتاب available = النسخ − الإعارات الحالية.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. POST /api/loans {"bookId": 2, "user": "ريم"}
  2. POST /api/loans {"bookId": 2, "user": "فهد"}
  3. POST /api/loans {"bookId": 1, "user": "ريم"}
  4. POST /api/loans {"bookId": 1, "user": "ريم"}
  5. POST /api/loans {"bookId": 9, "user": "ريم"}
  6. GET /api/books
  7. DELETE /api/loans/1
  8. DELETE /api/loans/1
  9. GET /api/books
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • الإعارة الأولى تنجح
  • … بالحالة 201
  • النسخة الوحيدة نفدت ← «لا نسخ متاحة»
  • التكرار للمستخدم نفسه ← 409
  • كتاب غير موجود ← 404
  • available محسوبة لكل كتاب
  • الإرجاع ← 204
  • إرجاع المرجَع ← 404
  • بعد الإرجاع صار «أساسيات الويب» متاحًا

💡 تلميح 1
دالة مساعدة تعدّ الإعارات الحالية لكتاب: loans.filter(l => l.bookId === id).length.
💡 تلميح 2
رتّب الفحوص: الوجود، ثم النسخ، ثم التكرار.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

const express = require('express');const app = express();app.use(express.json()); const books = [{ id: 1, title: 'تعلم JavaScript', copies: 2 }, { id: 2, title: 'أساسيات الويب', copies: 1 }];let loans = [];let nextId = 1;const out = (bookId) => loans.filter(l => l.bookId === bookId).length; app.get('/api/books', (req, res) => res.json(books.map(b => ({ id: b.id, title: b.title, available: b.copies - out(b.id) })))); app.post('/api/loans', (req, res) => {  const { bookId, user } = req.body;  const book = books.find(b => b.id === bookId);  if (!book) return res.status(404).json({ error: 'غير موجود' });  if (out(bookId) >= book.copies) return res.status(409).json({ error: 'لا نسخ متاحة' });  if (loans.some(l => l.bookId === bookId && l.user === user)) return res.status(409).json({ error: 'استعرته مسبقًا' });  const loan = { id: nextId++, bookId, user };  loans.push(loan);  res.status(201).json(loan);}); app.delete('/api/loans/:id', (req, res) => {  const n = loans.length;  loans = loans.filter(l => l.id !== Number(req.params.id));  loans.length < n ? res.sendStatus(204) : res.status(404).json({ error: 'غير موجود' });}); app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس