دليل تحليلي مصمم لترسيخ المفاهيم البرمجية لنظام التشغيل، وإدارة الذاكرة، وقنوات الإدخال/الإخراج (I/O Streams)، وصولاً إلى بناء ومعايرة خوادم الويب الموزعة.
لا يمكن كتابة خادم ويب دون فهم قنوات البيانات (Streams). ولا يمكن قراءة البيانات دون فهم معمارية حلقة الأحداث (Event Loop). يعالج هذا الدليل المفاهيم تِباعاً من الأسفل للأعلى لضمان البناء السليم.
جافاسكريبت مصممة كأصل تزامني أحادي الخيط داخل المتصفح. لتشغيلها على خوادم المعالجة، نستخدم محرك V8 مدمجاً في Node.js. في هذا المستوى، نربط المترجمات للتحويل بين أنظمة الوحدات الحديثة والقديمة.
تستخدم المشاريع الحديثة نمط ES6 Modules مثل import. ولكن، بيئة اختبارات المصحح الآلي تبحث عن أساليب الربط التقليدية لـ CommonJS. نقوم ببرمجة Babel للترجمة الوسيطة لحل هذا التضارب.
أداة التدقيق الإملائي البرمجي ليست خياراً جمالياً؛ إنها صمام الأمان لبيئة تشغيل خالية من مشاكل الذاكرة والتداخل المتغير. تفحص ESLint الكود ضد التغيرات غير المستعملة والنهايات السائبة للمسارات.
إذا استخدمت صيغة export default في ملفات المشروع دون تهيئة Babel بالكامل للبيئة التنفيذية للاختبارات، سيفشل مصحح بيئة الاختبار الآلية (Jest) في فك تشفير الكود وسيُرجع خطأ Unexpected token 'export'. الحل دائماً يكمن في التأكد من تفعيل babel.config.js ومطابقة التصدير النهائي باستخدام التوافقية العكسية module.exports = myFunction عند التصدير للملفات الأساسية المطلوبة للاختبار.
تتواصل العمليات البرمجية مع نظام التشغيل عبر ثلاثة مجارٍ رئيسية: المدخلات القياسية (stdin)، المخرجات القياسية (stdout)، والرسائل المباشرة عبر واجهة سطر الأوامر (process.argv).
أي متغير تمرره أثناء التشغيل يتم حفظه كنص في مصفوفة ممتدة. تذكر دائماً: العنصر الأول والثاني محجوزان للنظام والمسار التنفيذي، المتغير الفعلي يبدأ من الدليل رقم 2.
const args = process.argv; if (args.length < 3) { process.stdout.write("Error: Missing args\n"); process.exit(1); }
تظل القناة القياسية للمدخلات مفتوحة للاستماع كدفق بيانات مستمر. نستخدم الأحداث للتحكم بلحظة انتهاء تدفق البيانات لاستكمال معالجة العمليات الأخرى.
process.stdin.setEncoding('utf8'); process.stdin.on('readable', () => { const chunk = process.stdin.read(); if (chunk !== null) { process.stdout.write(`Data: \${chunk}`); } });
بيانات الإدخال عبر دفق stdin تصل دائماً على هيئة بايتات خام من النوع Buffer ما لم يتم فك تشفيرها صراحة بـ utf8. علاوة على ذلك، في أنظمة التشغيل المختلفة، تنتهي الأسطر بمحارف غير مرئية: محرف الانتقال لسطر جديد (\n) في يونكس، أو العودة لبداية السطر مع سطر جديد (\r\n) في ويندوز. إذا لم تقم بإزالتها باستخدام التابع .trim()، ستفشل عمليات المقارنة النصية الشرطية بالكامل في مشروعك (مثل التحقق من كلمة "exit").
في الأنظمة ذات الكفاءة العالية، لا يتم حجب السلسلة الأساسية للتطبيق مطلقاً لقراءة البيانات. نقارن هنا بشكل بنيوي بين الطريقتين لنرى كيف يمكن لقراءة ملف واحد متزامن أن يعطل استجابة الخادم لآلاف المستخدمين.
تجنب استخدام النسخ المتزامنة كلياً في الدوال التنفيذية للخوادم. لتنفيذ معالجة صحيحة وسريعة، نعتمد على استدعاءات الوعود واستيراد واجهات التفاعل غير المتزامنة بشكل صارم.
const fs = require('fs').promises; function readDatabase(path) { return fs.readFile(path, 'utf8') .then(data => parseCSV(data)) .catch(() => { throw new Error('Cannot load the database'); }); }
أغلب الأخطاء البرمجية تحدث عند تقسيم الأسطر بسبب محارف السطر الجديد أو تصفية الحقول الفارغة الناتجة عن ترويسات محرر ملفات الـ CSV.
function parseCSV(data) { const lines = data .split('\n') .filter(line => line.trim() !== ''); const header = lines.shift(); // إبعاد ترويسة الجدول return lines.map(line => line.split(',')); }
تفريغ البيانات النصية الكبيرة في الذاكرة عبر التابع .split() يستهلك الذاكرة العشوائية بشدة. لضمان أداء مستقر، نقوم بتنظيف المصفوفة الناتجة من البداية عبر التابع .filter() للتخلص من الحقول المشوهة والأسطر الفارغة تماماً، وبذلك نمنع تداخل السجلات والحقول الـ undefined التي تعطل وظائف الفلترة والإحصاء لعدد الطلاب.
قبل أن تظهر أطر العمل لتسهيل المهمة، كان المطورون يتخاطبون مباشرة مع مأخذ الشبكة عبر بروتوكول HTTP الخام. هنا نقوم ببناء خادم يدوي يوضح دورة الطلب والاستجابة وكيفية صياغة الرؤوس اليدوية.
المفتاح هنا هو التوجيه القائم على المسارات الفرعية. يجب أن نحدد بدقة حالة الرمز (statusCode) ونوع المحتوى المرسل لكي يعرف المتصفح طريقة التفسير الصحيحة للبيانات المستقبلة.
const http = require('http'); const server = http.createServer((req, res) => { if (req.url === '/') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello Holberton School!'); } else { res.statusCode = 404; res.end('Not Found'); } }); server.listen(1245);
إذا تمت معالجة طلب العميل بنجاح دون استدعاء التابع المنهي للبث res.end()، فسيظل المقبص مفتوحاً في وضع معلق للاتصال. المتصفح لن يظهر له أي أخطاء صريحة، ولكنه سيظل يعرض مؤشر التحميل إلى الأبد حتى يتم إنهاء الجلسة بسبب تجاوز المهلة المسموح بها (Timeout)، مما يدمر أداء السيرفر ويستهلك خيوط المعالجة.
ينقلنا Express من كتابة شروط التوجيه اليدوية الطويلة والبدائية إلى عالم التوجيه البسيط والمنظم وقنوات التحقق البرمجية (Middlewares).
تتعامل بيئة Express مع صياغة أنواع الاستجابة بشكل تلقائي بالكامل، بالإضافة لإتاحة التوجيه المعتمد على المسارات المتشعبة وفصل الأجزاء المتكررة لملفات مستقلة.
import express from 'express'; const app = express(); app.get('/students/:id', (req, res) => { // استخراج المتغيرات البرمجية من مسار الطلب تلقائياً const studentId = req.params.id; res.json({ id: studentId, status: 'active' }); });
تعمل خوارزمية البحث في Express على معالجة المسارات خطياً من الأعلى للأسفل وبأولوية التطابق الأول. إذا قمت بصياغة مسار عام يعالج جميع الأخطاء أو المسارات الاستثنائية ووضعته في أعلى شجرة السجل، فسيقوم بتعطيل وحجب كافة المسارات الشرعية التي تليه. احرص دائماً على صياغة وبناء مساراتك من الأكثر تخصصاً في الأعلى وصولاً للمسارات الأكثر عمومية مثل الـ 404 في أسفل الملف.
في هذا الجزء التفاعلي، سنقوم بمحاكاة دورة حياة الخادم الفعلي أثناء بدء التشغيل، قراءة قاعدة البيانات، استدعاء التابع غير المتزامن، واستقبال طلبات العملاء عبر واجهة الطرفية المصغرة.
توضح اللوحة التفاعلية المقابلة السجل المباشر للعمليات التنفيذية لخادم المطور المصغر. راقب التغير الفعلي والتحديث التلقائي الذي يتم عبر أداة Nodemon ومستخرجات الأحداث من محرك المعالجة.
قبل تسليم المشروع بشكل نهائي للمصحح الآلي، ينصح بتنفيذ فحص التدقيق الإملائي عبر تشغيل الأمر npm run lint لضمان خلو الملفات من المتغيرات البرمجية الضائعة والالتزام بقواعد التصدير الصارمة المعينة سلفاً.