تنظيم الخادم: Router والمتحكمات
كل ملف بمسؤولية واحدة: مسارات، ومتحكمات، وبيانات ⏱ نحو 40 دقيقة.
- تقسّم خادم
Expressإلى مسارات ومتحكمات وبيانات. - تستخدم
module.exportsوrequireبين ملفات المشروع. - تركّب
Routerعلى بادئة بـapp.use. - تقرأ الإعدادات من ملف .
env.
١حين يكبر الملف
خادم فيه عشرون مسارًا في ملف واحد يصبح صعب القراءة والتعديل، وخصوصًا حين يعمل عليه فريق. الحل: نقسّم الكود حسب المسؤولية، تمامًا كما قسّمنا واجهة React إلى مكوّنات:
| المجلد | مسؤوليته | يعرف عن |
|---|---|---|
server.js | إنشاء التطبيق، والوسطاء العامة، وربط المسارات، والتشغيل | المسارات فقط |
routes/ | أي (طريقة + مسار) يذهب إلى أي دالة | المتحكمات |
controllers/ | منطق الطلب: قراءة req، والتحقق، وإرسال الرد | النماذج / البيانات |
data/ أو models/ | التعامل مع البيانات (لاحقًا قاعدة البيانات) | لا شيء من HTTP |
shop-api/
├── server.js
├── routes/
│ └── products.js
├── controllers/
│ └── productController.js
└── data/
└── products.js
هذا تطبيق مبسّط لنمط MVC: النموذج Model للبيانات، والمتحكم Controller للمنطق؛ أما «العرض» View فهو هنا ردود JSON التي تعرضها React.
٢module.exports و require
كل ملف في Node وحدة مستقلة: متغيراته لا يراها غيره، إلا ما يضعه في module.exports. والملف الآخر يستورده بـ require('./مسار') (المسار النسبي يبدأ بـ ./ أو ../، واللاحقة .js اختيارية):
| التصدير | الاستيراد |
|---|---|
module.exports = router; (شيء واحد) | const router = require('./routes/products'); |
exports.list = …; exports.show = …; (عدة أشياء) | const ctrl = require('./controllers/productController'); ثم ctrl.list |
٣express.Router: تطبيق مصغّر
express.Router() يصنع مجموعة مسارات مستقلة نركّبها على بادئة واحدة بـ app.use('/api/products', router). داخل ملف المسارات نكتب '/' و '/:id' فقط، والبادئة تُضاف تلقائيًّا. تنقّل بين تبويبات الملفات الأربعة:
// server.js: نقطة البداية، قصيرة وواضحةconst express = require('express');const productRoutes = require('./routes/products'); const app = express();app.use(express.json()); app.use('/api/products', productRoutes); // كل مسارات الملف تبدأ بهذه البادئة app.use((req, res) => res.status(404).json({ error: 'المسار غير موجود' })); app.listen(3000, () => console.log('المتجر يعمل على 3000'));
// routes/products.js: أي مسار يذهب إلى أي دالةconst express = require('express');const router = express.Router();const ctrl = require('../controllers/productController'); router.get('/', ctrl.list);router.get('/:id', ctrl.show);router.post('/', ctrl.create);router.delete('/:id', ctrl.remove); module.exports = router;
// controllers/productController.js: منطق كل عمليةconst Product = require('../data/products'); exports.list = (req, res) => { res.json(Product.all());}; exports.show = (req, res) => { const p = Product.find(Number(req.params.id)); if (!p) return res.status(404).json({ error: 'المنتج غير موجود' }); res.json(p);}; exports.create = (req, res) => { const { name, price } = req.body; if (!name || typeof price !== 'number') { return res.status(400).json({ error: 'الاسم والسعر (رقم) مطلوبان' }); } res.status(201).json(Product.add({ name, price }));}; exports.remove = (req, res) => { if (!Product.remove(Number(req.params.id))) return res.status(404).json({ error: 'المنتج غير موجود' }); res.sendStatus(204);};
// data/products.js: بيانات في الذاكرة (في الوحدة 5 تحل محلها قاعدة بيانات)let products = [ { id: 1, name: 'لوحة مفاتيح', price: 150 }, { id: 2, name: 'فأرة', price: 60 },];let nextId = 3; module.exports = { all: () => products, find: (id) => products.find(p => p.id === id), add: (data) => { const p = { id: nextId++, ...data }; products.push(p); return p; }, remove: (id) => { const n = products.length; products = products.filter(p => p.id !== id); return products.length < n; },};
سبعة طلبات تمر على الملفات الأربعة. لاحظ أن DELETE رد بـ 204 بلا محتوى، وأن الطلب الأخير لمسار غير موجود وصل إلى وسيط 404 في server.js.
٤الخطأ الأشهر: نسيان التصدير
إن نسيت module.exports = router فإن require يعيد كائنًا فارغًا {}، ويرفض Express تركيبه:
const express = require('express');const app = express();app.use('/api/users', require('./routes/users'));app.listen(3000);
const router = require('express').Router();router.get('/', (req, res) => res.json([]));// نسينا: module.exports = router;
الخادم لم يبدأ أصلًا. الرسالة تقول إن app.use يحتاج دالة وسيطة لكنه استلم Object. أضف السطر الناقص في المختبر.
٥الإعدادات خارج الكود: ملف .env
المنفذ، وكلمة سر قاعدة البيانات، ومفاتيح التوقيع لا تُكتب داخل الكود (فهي تختلف بين جهازك والخادم الحقيقي، ولا يجوز رفعها إلى GitHub). نضعها في ملف .env، وتقرؤها حزمة dotenv إلى process.env:
require('dotenv').config();const express = require('express');const app = express(); const PORT = process.env.PORT || 3000; app.get('/api/info', (req, res) => { res.json({ shop: process.env.SHOP_NAME, mode: process.env.NODE_ENV });}); app.listen(PORT, () => console.log('يعمل على المنفذ ' + PORT));
PORT=4000SHOP_NAME=متجر الكلية
توقّع وتحقّق
مع app.use('/api/books', router) و router.get('/:id', …)، ما المسار الكامل؟
أين يُكتب منطق التحقق من البيانات وإرسال الرد؟
ماذا يعيد require لملف لم يصدّر شيئًا؟
module.exports الافتراضي.لماذا نضع كلمات السر في .env ؟
الخلاصة
server.jsيجمّع، وroutesتربط، وcontrollersتنفّذ، وdata/modelsتتعامل مع البيانات.express.Router()+module.exports = router+app.use(prefix, router).- المسار النسبي في
requireيبدأ بـ./أو../. - الأسرار والإعدادات في .
envعبرdotenv، ولا تُرفع إلى المستودع.
تحدٍّ برمجي: تقسيم خادم المهام
متوسطخادم مهام كُتب في ملف واحد، والمطلوب نقل مساراته إلى Router ومتحكم مع بقاء سلوكه كما هو.
- ملف
routes/tasks.jsفيهexpress.Router()ويصدّره. - ملف
controllers/taskController.jsيصدّر الدالتينlistوcreate. server.jsيركّب المسارات بـapp.use('/api/tasks', …)ولا يحويapp.getأوapp.post.- السلوك نفسه: القائمة، والإضافة بـ 201، ورفض المهمة الفارغة بـ 400.
👀 اعرض الاستجابات المطلوبة
GET /api/tasksPOST /api/tasks{"title": "كتابة الاختبارات"}POST /api/tasks{}GET /api/tasks
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- القائمة تعمل
- الإضافة ترد بالمهمة الجديدة
- … بالحالة 201
- المهمة الفارغة ← 400
- القائمة صارت مهمتين
routes/tasks.jsيستخدم Router ويصدّره- المتحكم يصدّر list و create
- server.js بلا app.get أو app.post
💡 تلميح 1
'/' فقط، والبادئة في server.js.💡 تلميح 2
حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express();app.use(express.json()); app.use('/api/tasks', require('./routes/tasks')); app.listen(3000);
const express = require('express');const router = express.Router();const ctrl = require('../controllers/taskController'); router.get('/', ctrl.list);router.post('/', ctrl.create); module.exports = router;
const tasks = [{ id: 1, title: 'تصميم الواجهة' }]; exports.list = (req, res) => res.json(tasks); exports.create = (req, res) => { if (!req.body.title) return res.status(400).json({ error: 'العنوان مطلوب' }); const t = { id: tasks.length + 1, title: req.body.title }; tasks.push(t); res.status(201).json(t);};