طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 11 · الوحدة 4: إتقان إطار الخادم

الوسيطات ومعالجة الأخطاء

سلسلة الدوال التي يمر بها كل طلب ⏱ نحو 40 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح سلسلة الوسطاء ودور next.
  • تكتب وسيطًا عامًّا ووسيطًا لمسار واحد.
  • تستخدم الوسطاء الجاهزة: json و static و cors و morgan.
  • تعالج الأخطاء و404 في مكان واحد بـ JSON موحّد.

١ما الوسيط؟

الطلب في Express لا يذهب مباشرة إلى معالج المسار، بل يمر على سلسلة من الدوال بالترتيب الذي كُتبت به. كل دالة في السلسلة وسيط Middleware يستقبل ثلاثة معاملات: (req, res, next)، ويفعل واحدًا من أمرين:

تشبه نقاط التفتيش في مطار: الأمن، ثم الجوازات، ثم البوابة. أي نقطة يمكنها أن توقفك، أو تسمح لك بالمرور إلى التالية.

const express = require('express');const app = express(); // وسيط يعمل مع كل طلب: يطبع الطريقة والمسارapp.use((req, res, next) => {  console.log(`${req.method} ${req.url}`);  next();                       // مرّر إلى التالي}); app.get('/', (req, res) => res.send('الرئيسية'));app.get('/about', (req, res) => res.send('من نحن')); app.listen(3000);

افتح أي طلب في السجل: الطرفية تُظهر أن الوسيط طبع سطرًا لكل طلب، حتى الطلب الذي انتهى بـ 404.

٢نسيان next

إن لم يرسل الوسيط ردًّا ولم يستدعِ next() يبقى الطلب معلّقًا: المتصفح ينتظر ولا يصله شيء:

const express = require('express');const app = express(); app.use((req, res, next) => {  console.log('وصل طلب');  // نسينا next()}); app.get('/', (req, res) => res.send('لن تصل')); app.listen(3000);

المختبر أوقف الانتظار بعد 3 ثوانٍ وأبلغ أن الخادم لم يرد. أضف next(); وجرّب من جديد.

٣الترتيب مهم، والوسيط على مسار واحد

app.use(fn) يطبّق الوسيط على كل الطلبات التي تأتي بعده في الكود. ويمكن أيضًا وضع وسيط على مسار بعينه، بين المسار والمعالج. هنا وسيط يتحقق من مفتاح سري في الترويسة قبل المسارات المحمية:

const express = require('express');const app = express(); function requireKey(req, res, next) {  if (req.headers['x-api-key'] !== 'tvtc-2025') {    return res.status(401).json({ error: 'مفتاح غير صالح' });  }  next();} app.get('/api/public', (req, res) => res.json({ msg: 'للجميع' })); // الوسيط هنا يحمي هذا المسار فقطapp.get('/api/secret', requireKey, (req, res) => res.json({ msg: 'سرّي' })); app.listen(3000);

الطلب الثاني بلا مفتاح أوقفه الوسيط بالحالة 401 ولم يصل إلى المعالج. الثالث مرّ لأن المفتاح صحيح.

٤وسيطات جاهزة

كثير من الوسطاء مكتوب مسبقًا. استخدمنا منها express.json()، وهذه أشهر البقية:

الوسيطالتثبيتوظيفته
express.json()مدمجيقرأ جسم JSON إلى req.body
express.urlencoded()مدمجيقرأ بيانات النماذج HTML
express.static('public')مدمجيقدّم ملفات ثابتة (HTML و CSS والصور) من مجلد
cors()npm i corsيسمح لواجهة على نطاق آخر (مثل Vite على 5173) بطلب الخادم
morgan('dev')npm i morganيسجّل كل طلب في الطرفية
helmet()npm i helmetيضيف ترويسات أمان (الوحدة 11)
const express = require('express');const cors = require('cors');const morgan = require('morgan');const path = require('path');const app = express(); app.use(morgan('tiny'));app.use(cors({ origin: 'http://localhost:5173' }));app.use(express.static(path.join(__dirname, 'public'))); app.get('/api/time', (req, res) => res.json({ now: 'الآن' })); app.listen(3000);

الطلب الأول قدّمه express.static من الملف public/index.html دون أن نكتب مسارًا. وفي الثاني أضاف cors الترويسة Access-Control-Allow-Origin. أما morgan فطبع سطرًا لكل طلب في الطرفية مع زمن الاستجابة.

٥معالجة الأخطاء في مكان واحد

حين يرمي معالجٌ خطأً، يلتقطه Express ويرسل صفحة 500 عامة. الأفضل أن نكتب وسيط أخطاء واحدًا في آخر الملف يرد بـ JSON موحّد. علامته المميزة: أربعة معاملات (err, req, res, next). للوصول إليه من معالج عادي نستدعي next(err)، أما في الدوال async فيكفي أن نرمي الخطأ (ميزة في Express 5):

const express = require('express');const app = express(); app.get('/api/divide/:a/:b', (req, res, next) => {  const a = Number(req.params.a), b = Number(req.params.b);  if (b === 0) {    const err = new Error('لا يمكن القسمة على صفر');    err.status = 400;    return next(err);              // اذهب إلى وسيط الأخطاء  }  res.json({ result: a / b });}); app.get('/api/crash', async (req, res) => {  throw new Error('عطل في قاعدة البيانات');  // Express 5 يلتقطه}); // 404 لكل مسار لم يُعرَّف (بعد كل المسارات)app.use((req, res) => {  res.status(404).json({ error: 'المسار غير موجود' });}); // وسيط الأخطاء: أربعة معاملات، وفي النهاية دائمًاapp.use((err, req, res, next) => {  console.error(err.message);  res.status(err.status || 500).json({ error: err.message });}); app.listen(3000);

كل الأخطاء صارت تصل إلى الواجهة بالشكل نفسه {"error": …}، فتعرضها React بسهولة. وطبع وسيط الأخطاء رسالته في الطرفية ليراها المطوّر.

٦مختبرك

أضف وسيطًا يقيس زمن كل طلب ويطبعه، ثم وسيطًا يرفض أي طلب POST لا يحمل الترويسة Content-Type: application/json.

🧪 مختبر Expressعدّل الملفات، ثم أرسل طلبًا يدويًّا أو افتح الرابط في المتصفح المصغّر. الخادم يعمل داخل صفحتك.

توقّع وتحقّق

ماذا يحدث إن لم يستدعِ الوسيط next() ولم يرسل ردًّا؟

لا رد.

كيف يعرف Express أن الوسيط معالج أخطاء؟

عدد المعاملات.

أين نضع وسيط 404 العام؟

يلتقط ما لم يطابق شيئًا.

أي وسيط يسمح لواجهة على منفذ آخر بطلب الخادم؟

Cross-Origin.

الخلاصة

تحدٍّ برمجي: بوابة الإدارة

متوسط

خادم فيه مسارات عامة ومسارات للإدارة محمية بمفتاح، وأخطاء موحّدة.

المطلوب:
  • وسيط logger لكل الطلبات يطبع في الطرفية METHOD URL (مثل GET /api/news).
  • وسيط adminOnly على مسار GET /api/admin/stats فقط: إن لم تكن الترويسة x-role تساوي admin يرد بالحالة 403 و {"error": "للمشرفين فقط"}.
  • أي مسار غير معرَّف يرد بالحالة 404 و {"error": "غير موجود"}.
  • وسيط أخطاء في النهاية يرد بالحالة 500 و {"error": رسالة الخطأ}؛ وGET /api/fail يرمي خطأً رسالته «تعطّل».
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. GET /api/news
  2. GET /api/admin/stats
  3. GET /api/admin/stats
  4. GET /api/xyz
  5. GET /api/fail
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • الأخبار تعمل
  • الإحصاءات بلا دور ← 403
  • … ومعها {"error": "للمشرفين فقط"}
  • مع x-role: admin تصل الإحصاءات
  • مسار غير معرَّف ← {"error": "غير موجود"}
  • … بالحالة 404
  • الخطأ يصل بصيغة JSON {"error": "تعطّل"}
  • الطرفية تُظهر سطرًا لكل طلب

💡 تلميح 1
وسيط الأخطاء يجب أن يكون بأربعة معاملات وآخر شيء قبل listen.
💡 تلميح 2
الطلب الأخير يرد بالحالة 500، وهذا متوقع: التحقق يفحص أن الجسم JSON.

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

const express = require('express');const app = express(); function logger(req, res, next) {  console.log(req.method + ' ' + req.url);  next();}function adminOnly(req, res, next) {  if (req.headers['x-role'] !== 'admin') return res.status(403).json({ error: 'للمشرفين فقط' });  next();} app.use(logger);app.get('/api/news', (req, res) => res.json(['خبر 1', 'خبر 2']));app.get('/api/admin/stats', adminOnly, (req, res) => res.json({ users: 120 }));app.get('/api/fail', (req, res) => { throw new Error('تعطّل'); }); app.use((req, res) => res.status(404).json({ error: 'غير موجود' }));app.use((err, req, res, next) => res.status(500).json({ error: err.message })); app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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