Node.js: بناء واجهة API وتسليمها
خروج

الدرس 1 من 14 · أساسيات Node.js

Node.js: محرك V8، حلقة الأحداث، الإصدارات وREPL

45 د Node.js 22 (Debian, git, npm, python3)

الهدف

أن تفهم ما هو Node.js حقًا ولماذا يناسب جيدًا واجهات API (متجر إلكتروني، حجوزات، توصيل)، وأن تثبّت إصدارًا LTS باستعمال nvm، وأن تتعوّد على اختبار الأفكار داخل REPL وبسكربتات صغيرة.

أهم النقاط

  • Node.js هو بيئة تشغيل (runtime) لـ JavaScript خارج المتصفح: المحرك V8 (محرك Chrome) + المكتبة libuv (عمليات الإدخال/الإخراج غير المتزامنة) + مكتبة قياسية (node:fs، node:http، node:crypto…).
  • يعمل برنامج Node في عملية (process) واحدة وخيط (thread) JavaScript واحد. لا يتوقّف أثناء قراءة من القرص أو طلب عبر الشبكة: يواصل العمل ثم يستأنف عندما يصل الجواب، بفضل حلقة الأحداث (event loop).
  • النتيجة: يتحمّل خادم Node آلاف الاتصالات المتزامنة… ما دامت شيفرتك لا تقوم بـحساب طويل ومتزامن يعطّل الجميع.
  • في الإنتاج، استعمل إصدارًا LTS (رقم زوجي: 22، 24…). أما الإصدارات الفردية فلا تعيش إلا بضعة أشهر.
  • يثبّت nvm إصدارات Node ويبدّل بينها لكل مستخدم، دون المساس بالنظام؛ ويحدّد الملف .nvmrc إصدار مشروع ما.
  • يُستعمل REPL (node دون معاملات) وnode -e وnode -p لتجربة سطر من الشيفرة في بضع ثوانٍ.

أساسيات اللغة (المتغيرات، الدوال، المصفوفات، الوعود promises) مشروحة في مسار «Développement web : HTML, CSS et JavaScript» (تطوير الويب). هنا نتحدّث عمّا يضيفه Node حولها.

مكوّنات Node.js

            votre code JavaScript (serveur.js)
                         │
   ┌─────────────────────┴──────────────────────┐
   │  bibliothèque standard : node:fs, node:http,│
   │  node:path, node:crypto, node:stream…       │
   ├──────────────────────┬──────────────────────┤
   │  V8 : compile et     │  libuv : boucle      │
   │  exécute le JS       │  d'événements, E/S   │
   │  (JIT, ramasse-      │  asynchrones, pool   │
   │  miettes)            │  de fils pour le     │
   │                      │  disque, DNS, crypto │
   └──────────────────────┴──────────────────────┘
                 système d'exploitation

(votre code JavaScript = شيفرة JavaScript الخاصة بك؛ bibliothèque standard = المكتبة القياسية؛ V8: يترجم JS وينفّذه (JIT، جامع النفايات)؛ libuv: حلقة الأحداث، الإدخال/الإخراج غير المتزامن، مجمّع خيوط للقرص وDNS والتشفير؛ système d'exploitation = نظام التشغيل.)

  • يحوّل V8 شيفرة JavaScript الخاصة بك إلى شيفرة آلة أثناء التنفيذ (الترجمة «في الوقت المناسب» JIT) ويدير الذاكرة (جامع النفايات garbage collector). ولهذا فإن Node سريع.
  • توفّر libuv حلقة الأحداث وتتحاور مع النظام (المقابس sockets، الملفات، المؤقّتات). بعض العمليات البطيئة (قراءة الملفات، تجزئة كلمات المرور hashing، تحليل أسماء DNS) تُرسَل إلى مجمّع خيوط (thread pool) صغير مخفي، ثم تعود النتيجة إلى الحلقة.
  • المكتبة القياسية غنية: يمكن كتابة خادم HTTP كامل، وقراءة الملفات، والتشفير، والضغط… دون تثبيت أي شيء. تُستورد الوحدات المدمجة بالبادئة node: (node:http)، وهي موصى بها لتجنّب أي خلط مع حزمة npm تحمل الاسم نفسه.

ما الذي يتغيّر مقارنة بالمتصفح

المتصفح Node.js
window، document، DOM غائبة: لا توجد صفحة لعرضها
localStorage الملفات (node:fs)، قواعد البيانات
بيئة معزولة (sandbox): لا وصول إلى القرص وصول كامل إلى القرص والشبكة والعمليات
إصدار JS يفرضه متصفح الزائر أنت تختار إصدار Node، وبالتالي الميزات المتاحة
globalThis = window globalThis = الكائن العام لـ Node؛ وprocess يعطي البيئة

ما يوجد في الجهتين: console، setTimeout، fetch، URL، JSON، Promise، structuredClone، crypto.randomUUID()…

النقطة الأخيرة في الجدول ميزة حقيقية: على الخادم، لا حاجة لانتظار أن يحدّث الزبائن متصفحاتهم. إذا كان مشروعك يعمل على Node 22، فكل الصياغة الحديثة متاحة.

البرنامج الأول

mkdir -p ~/essais && cd ~/essais
echo 'console.log("Marhaba depuis Node.js");' > bonjour.js
node bonjour.js            # Marhaba depuis Node.js

يصف الكائن process البرنامج الجاري تنفيذه:

// infos.js
console.log(process.version);        // 'v22.x.y'
console.log(process.versions.v8);    // إصدار محرك V8 المدمج
console.log(process.platform);       // 'linux', 'win32', 'darwin'
console.log(process.argv);           // [مسار node، مسار السكربت، المعاملات…]
console.log(process.cwd());          // المجلد الحالي
console.log(process.env.HOME);       // متغيرات البيئة
process.exitCode = 0;                // رمز الخروج (0 = نجاح)

يحتوي process.argv أولًا على مسار node، ثم مسار السكربت، ثم معاملاتك أنت، دائمًا على شكل نص:

node total.js 120 35.5
# process.argv = ['/usr/bin/node', '/root/total.js', '120', '35.5']
# process.argv.slice(2).map(Number) = [120, 35.5]

حلقة الأحداث

تخيّل خادم تطبيق توصيل في الدار البيضاء. يصل طلب: «قائمة طلبيات عامل التوصيل رقم 12». يجب استعلام قاعدة البيانات، وهو ما يستغرق 20 ms. الخادم الذي يعمل بمبدأ «خيط لكل طلب» يُجمّد خيطًا طوال هذه الـ 20 ms. أما Node، فإنه يطلق الاستعلام نحو القاعدة، وينتقل إلى الطلب الموالي، ويستأنف الأول عندما تجيب القاعدة.

console.log('A : requête reçue');
setTimeout(() => console.log('C : la base a répondu'), 20);  // يحاكي عملية إدخال/إخراج
console.log('B : je traite déjà la requête suivante');
// A، ثم B، ثم C

المبدأ:

  1. ينفّذ Node سكربتك كله من الأعلى إلى الأسفل (الشيفرة المتزامنة).
  2. تُسنَد العمليات غير المتزامنة (المؤقّتات، الشبكة، القرص) إلى libuv مع دالة يُعاد استدعاؤها (callback).
  3. عندما تفرغ مكدّسة الاستدعاءات (call stack)، تأخذ الحلقة الأحداث المنتهية وتستدعي دوالها، واحدة تلو الأخرى.

إذن حتى setTimeout(fn, 0) ينتظر انتهاء السكربت الجاري:

console.log('1 debut');
setTimeout(() => console.log('3 minuteur'), 0);
console.log('2 fin du script');

المراحل، بتبسيط

في كل دورة، تمرّ الحلقة بـمراحل (phases) بترتيب ثابت:

المرحلة ما يُنفَّذ
timers دوال setTimeout وsetInterval التي حلّ أجلها
poll عمليات الإدخال/الإخراج المنتهية (الشبكة، الملفات)؛ ويمكن للحلقة أن تنتظر فيها
check دوال setImmediate
close عمليات الإغلاق (socket.on('close'))

بين دالتي استدعاء، يفرغ Node أيضًا طابورين ذوي أولوية: طابور process.nextTick، ثم طابور المهام الصغرى (microtasks) (دوال .then الخاصة بالوعود).

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promesse'));
process.nextTick(() => console.log('nextTick'));
console.log('synchrone');
// synchrone، nextTick، promesse، ثم timeout وimmediate
// (قد يختلف ترتيب هذين الأخيرين في السكربت الرئيسي)

لا تحتاج إلى حفظ كل مرحلة. احفظ القاعدة الذهبية.

القاعدة الذهبية: لا تعطّل الحلقة

لا يوجد إلا خيط واحد لشيفرة JavaScript الخاصة بك. حساب يستغرق ثانيتين يعطّل كل الطلبات لمدة ثانيتين:

import { createServer } from 'node:http';

createServer((req, res) => {
  if (req.url === '/rapport') {
    const fin = Date.now() + 2000;
    while (Date.now() < fin) {}          // سيّئ: ثانيتان من الحساب المتزامن
  }
  res.end('ok\n');
}).listen(3000);
// بينما يطلب زبون /rapport، ينتظر الآخرون، حتى من أجل '/'

ما يجب تجنّبه في خادم:

  • دوال الإدخال/الإخراج المتزامنة (fs.readFileSync، crypto.pbkdf2Sync…) أثناء معالجة طلب: خصّصها لمرحلة الإقلاع؛
  • الحلقات الكبيرة على ملايين العناصر، وJSON.parse لملفات ضخمة؛
  • التعابير النمطية (regular expressions) المكتوبة بشكل سيّئ على نصوص يقدّمها المستخدم.

يعرض الخيار node --trace-sync-io serveur.js تحذيرًا في كل مرة تُستدعى فيها واجهة API متزامنة بعد الإقلاع: مفيد لتعقّب حالات التعطيل قبل الإنتاج. أما للحسابات الثقيلة الحقيقية (توليد PDF، معالجة الصور)، فنستعمل worker_threads أو خدمة منفصلة.

إصدارات Node.js

يصدر إصدار رئيسي (major) جديد كل ستة أشهر:

  • في أبريل: إصدار زوجي (22، 24، 26…)؛ يصبح LTS (Long Term Support، الدعم طويل الأمد) في أكتوبر؛
  • في أكتوبر: إصدار فردي (23، 25…)، يُصان لمدة ستة أشهر تقريبًا فقط، لتجربة المستجدات.

يمرّ إصدار LTS بثلاث حالات:

الحالة المدة التقريبية ما يتلقّاه
Current 6 أشهر كل المستجدات
Active LTS 12 شهرًا تصحيحات، وتحسينات حذرة
Maintenance LTS 18 شهرًا تصحيحات أمنية وأخطاء حرجة

أي حوالي 30 شهرًا من العمر. الوضع في تاريخ هذا الدرس (أكتوبر 2026):

الإصدار الحالة نهاية العمر
Node 20 انتهى عمره أبريل 2026: يجب الترحيل
Node 22 Maintenance LTS أبريل 2027
Node 24 Active LTS أبريل 2028
Node 26 Current، يصبح LTS في نهاية أكتوبر 2026 أبريل 2029

تحقّق دائمًا من الجدول الرسمي على موقع Node.js قبل الاختيار: التواريخ الدقيقة منشورة فيه، والوتيرة قد تتغيّر. يستعمل التمرين التطبيقي في هذا المسار Node 22.

عمليًا، لواجهة API في الإنتاج: أحدث إصدار LTS نشط، أو الذي قبله إذا كانت مكتبة ما غير متوافقة بعد. لا إصدار فرديًا أبدًا. فـ Express 5، مثلًا، يتطلّب Node 18 على الأقل.

تثبيت Node.js

باستعمال nvm (موصى به في التطوير)

يثبّت nvm (Node Version Manager، مدير إصدارات Node) برنامج Node في مجلدك الشخصي. تتعايش عدة إصدارات، دون sudo.

# تثبيت nvm (السكربت الرسمي لمشروع nvm-sh/nvm على GitHub؛
# تحقّق من آخر إصدار في صفحة المشروع)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
source ~/.bashrc              # إعادة تحميل الـ shell

nvm install --lts             # آخر إصدار LTS
nvm install 22                # فرع محدّد
nvm ls                        # الإصدارات المثبّتة
nvm use 22                    # استعمال Node 22 في هذه الطرفية
nvm alias default 22          # الإصدار الافتراضي للطرفيات الجديدة
node --version && npm --version

في كل مشروع، يحدّد ملف .nvmrc الإصدار المنتظر:

echo 22 > .nvmrc
nvm use                       # يقرأ .nvmrc: الفريق كله يستعمل الإصدار نفسه

على Windows، استعمل nvm-windows أو fnm، أو المثبّت الرسمي من nodejs.org.

على خادم

  • حزم التوزيعة (apt install nodejs) تكون في الغالب قديمة: تحقّق من الإصدار قبل الاعتماد عليها.
  • توفّر مستودعات NodeSource حزم .deb/.rpm لكل فرع LTS، تُحدَّث بواسطة apt upgrade.
  • في حاوية (container)، انطلق من الصورة الرسمية node:22-slim أو node:22-alpine (مسار Docker).

REPL والأوامر السريعة

عند تشغيله دون معاملات، يفتح node الـ REPL (Read-Eval-Print Loop، حلقة القراءة والتقييم والطباعة): يقرأ تعبيرًا، ويقيّمه، ويعرض النتيجة، ثم يعيد الكرّة.

$ node
Welcome to Node.js v22.x.y.
> 120 * 1.2
144
> _ + 10              // _ = آخر نتيجة
154
> const tva = 0.2
undefined
> [100, 250].map(p => p * (1 + tva))
[ 120, 300 ]
> 0.1 + 0.2
0.30000000000000004   // الأسعار بالدرهم تُحسب بالسنتيمات الصحيحة!
> .help               // أوامر REPL
> .editor             // كتابة عدة أسطر، Ctrl+D للتنفيذ
> .exit               // أو Ctrl+D

يُكمل المفتاح Tab الأسماء: اكتب fs. ثم Tab لرؤية دوال الوحدة fs، المحمّلة تلقائيًا في REPL.

دون فتح REPL:

node -e 'console.log(new Date().toISOString())'      # تنفيذ سطر واحد
node -p '1500 * 0.2'                                 # التنفيذ مع عرض النتيجة: 300
node -p 'process.versions'                           # إصدارات V8 وlibuv وOpenSSL…
node --check serveur.js                              # التحقق من الصياغة دون تنفيذ
node --watch serveur.js                              # إعادة التشغيل عند كل تعديل (التطوير)

أخطاء متكررة

  • تعطيل حلقة الأحداث بحساب طويل أو بدالة …Sync داخل طلب: يتجمّد الخادم كله، بالنسبة لكل الزبائن.
  • استعمال إصدار فردي أو منتهي العمر في الإنتاج: لا مزيد من التصحيحات الأمنية. اتبع LTS.
  • تثبيت Node باستعمال sudo apt دون التحقق من الإصدار: قد تحصل على فرع متجاوز. تحقّق بواسطة node --version.
  • نسيان أن process.argv يحتوي على نصوص: '120' + '35' يعطي '12035'. حوّل باستعمال Number().

التطبيق العملي (Lab)

في التمرين (Node 22، دون شبكة)، ستقوم بما يلي:

  1. كتابة سكربت أول وتشغيله؛
  2. تسجيل إصدار Node المثبّت؛
  3. قراءة معاملات سطر الأوامر لحساب مجموع بالدرهم؛
  4. التحقق بنفسك من ترتيب التنفيذ الذي تفرضه حلقة الأحداث.

لإنشاء ملف دون محرّر، استعمل echo '…' > fichier أو cat > fichier <<'EOF' … EOF.

اختبار قصير

  1. ما المكوّنات الكبرى الثلاثة لـ Node.js؟
  2. لماذا يستطيع خادم Node إدارة آلاف الاتصالات بخيط واحد؟
  3. بأيّ ترتيب تظهر console.log('a') وsetTimeout(() => console.log('b'), 0) وconsole.log('c') المكتوبة بهذا الترتيب؟
  4. تبدأ واجهة API لرياض في أكتوبر 2026: Node 25 أم Node 24؟ لماذا؟
  5. ما فائدة الملف .nvmrc؟
الأجوبة
  1. المحرك V8 (تنفيذ JavaScript)، وlibuv (حلقة الأحداث والإدخال/الإخراج غير المتزامن) والمكتبة القياسية (node:fs، node:http…).
  2. لأن عمليات الإدخال/الإخراج لا تعطّل: يسندها Node إلى النظام، ويعالج طلبات أخرى، ثم يستأنف كل معالجة عندما تصل النتيجة (حلقة الأحداث).
  3. a، ثم c، ثم b: لا يُنفَّذ المؤقّت إلا بعد انتهاء السكربت المتزامن.
  4. Node 24: إنه إصدار زوجي LTS، مُصان حتى 2028. أما Node 25 فهو فردي، دون دعم طويل.
  5. لتحديد إصدار Node لمشروع: يقرؤه nvm use، ويعمل الفريق كله بالإصدار نفسه.

مصادر مرجعية: الوثائق الرسمية لـ Node.js (nodejs.org، رخصة MIT) ولـ Express (expressjs.com، CC BY-SA 3.0). نص أصلي من MasterLinux.

تمارين تطبيقية

تُنجَز على Node.js 22 (Debian, git, npm, python3)

الطرفية عبر الإنترنت قادمة قريبًا. في انتظار ذلك، أنجِز هذه المهام على جهازك بنظام لينكس أو على آلة افتراضية أو WSL.

  1. أنشئ /root/bonjour.js الذي يعرض بالضبط السطر Marhaba depuis Node.js، ثم شغّله باستعمال node /root/bonjour.js
    عرض تلميحecho 'console.log("Marhaba depuis Node.js");' > /root/bonjour.js
    عرض الحل
    echo 'console.log("Marhaba depuis Node.js");' > /root/bonjour.js

    التحقق : test "$(node /root/bonjour.js 2>/dev/null)" = "Marhaba depuis Node.js"

  2. سجّل إصدار Node.js المثبّت في التمرين (المخرج الدقيق لـ node --version، مثلًا v22.x.y) في الملف /root/version.txt
    عرض تلميحأعد توجيه مخرج الأمر: node --version > /root/version.txt
    عرض الحل
    node --version > /root/version.txt

    التحقق : test -s /root/version.txt && test "$(cat /root/version.txt)" = "$(node --version)"

  3. أنشئ /root/total.js: يجمع الأسعار المُمرَّرة كمعاملات (process.argv) ويعرض «Total : X MAD» برقمين بعد الفاصلة. يجب أن يعرض node /root/total.js 120 35.5 4.5 السطر Total : 160.00 MAD، ودون معاملات Total : 0.00 MAD
    عرض تلميحيعطي process.argv.slice(2) المعاملات كنصوص: حوّلها باستعمال map(Number)، واجمعها باستعمال reduce، ثم total.toFixed(2)
    عرض الحل
    printf '%s\n' 'const prix = process.argv.slice(2).map(Number);' 'const total = prix.reduce((somme, p) => somme + p, 0);' 'console.log(`Total : ${total.toFixed(2)} MAD`);' > /root/total.js

    التحقق : test "$(node /root/total.js 120 35.5 4.5 2>/dev/null)" = "Total : 160.00 MAD" && test "$(node /root/total.js 2>/dev/null)" = "Total : 0.00 MAD"

  4. لاحظ حلقة الأحداث: أنشئ /root/ordre.js الذي يعرض «1 debut»، ويبرمج باستعمال setTimeout(…, 0) عرض «3 minuteur»، ثم يعرض «2 fin du script». يجب أن يُكتب استدعاء setTimeout قبل السطر الذي يعرض «2 fin du script»، ويجب أن يكون المخرج بالترتيب 1، 2، 3
    عرض تلميحconsole.log("1 debut"); setTimeout(() => console.log("3 minuteur"), 0); console.log("2 fin du script");
    عرض الحل
    printf '%s\n' 'console.log("1 debut");' 'setTimeout(() => console.log("3 minuteur"), 0);' 'console.log("2 fin du script");' > /root/ordre.js

    التحقق : f=/root/ordre.js; test "$(node $f 2>/dev/null | tr '\n' '|')" = "1 debut|2 fin du script|3 minuteur|" && awk '/setTimeout/{if(!s)s=NR} /2 fin du script/{f=NR} END{exit !(s && f && s<f)}' $f

المراجع الرسمية المعتمدة: Introduction to Node.js — https://nodejs.org/en/learn/getting-started/introduction-to-nodejs ; Installation d'Express (prérequis Node.js) — https://expressjs.com/fr/5x/starter/installing/ ; Meilleures pratiques de production : performance et fiabilité — https://expressjs.com/fr/advanced/best-practice-performance. نص أصلي من MasterLinux.