الدرس 1 من 14 · أساسيات Node.js
Node.js: محرك V8، حلقة الأحداث، الإصدارات وREPL
الهدف
أن تفهم ما هو 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
المبدأ:
- ينفّذ Node سكربتك كله من الأعلى إلى الأسفل (الشيفرة المتزامنة).
- تُسنَد العمليات غير المتزامنة (المؤقّتات، الشبكة، القرص) إلى libuv مع دالة يُعاد استدعاؤها (callback).
- عندما تفرغ مكدّسة الاستدعاءات (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، دون شبكة)، ستقوم بما يلي:
- كتابة سكربت أول وتشغيله؛
- تسجيل إصدار Node المثبّت؛
- قراءة معاملات سطر الأوامر لحساب مجموع بالدرهم؛
- التحقق بنفسك من ترتيب التنفيذ الذي تفرضه حلقة الأحداث.
لإنشاء ملف دون محرّر، استعمل echo '…' > fichier أو cat > fichier <<'EOF' … EOF.
اختبار قصير
- ما المكوّنات الكبرى الثلاثة لـ Node.js؟
- لماذا يستطيع خادم Node إدارة آلاف الاتصالات بخيط واحد؟
- بأيّ ترتيب تظهر
console.log('a')وsetTimeout(() => console.log('b'), 0)وconsole.log('c')المكتوبة بهذا الترتيب؟ - تبدأ واجهة API لرياض في أكتوبر 2026: Node 25 أم Node 24؟ لماذا؟
- ما فائدة الملف
.nvmrc؟
الأجوبة
- المحرك V8 (تنفيذ JavaScript)، وlibuv (حلقة الأحداث والإدخال/الإخراج غير المتزامن) والمكتبة القياسية (
node:fs،node:http…). - لأن عمليات الإدخال/الإخراج لا تعطّل: يسندها Node إلى النظام، ويعالج طلبات أخرى، ثم يستأنف كل معالجة عندما تصل النتيجة (حلقة الأحداث).
a، ثمc، ثمb: لا يُنفَّذ المؤقّت إلا بعد انتهاء السكربت المتزامن.- Node 24: إنه إصدار زوجي LTS، مُصان حتى 2028. أما Node 25 فهو فردي، دون دعم طويل.
- لتحديد إصدار Node لمشروع: يقرؤه
nvm use، ويعمل الفريق كله بالإصدار نفسه.
مصادر مرجعية: الوثائق الرسمية لـ Node.js (nodejs.org، رخصة MIT) ولـ Express (expressjs.com، CC BY-SA 3.0). نص أصلي من MasterLinux.