2.3 · التدفقات (Streams) والمخازن المؤقتة (Buffer): معالجة الأحجام الكبيرة

المستوى 2 · قلب Node.js

2.3التدفقات (Streams) والمخازن المؤقتة (Buffer): معالجة الأحجام الكبيرة

الهدف: معالجة الأحجام الكبيرة بفعالية مع التدفقات (Streams) (قطعًا، دون إشباع الذاكرة) وإتقان .pipe() — علامة كود Node الاحترافي.
الوقت المقدّر: 11 دقيقة · قراءة ذاتية · إذا كان لديك 5 دقائق فقط: اقرأ «الأساسي في 30 ثانية» + المخطط.

الأساسي في 30 ثانية

  • قطعًا، لا دفعة واحدة: يعالج التدفق كل قطعة أولًا بأول — وتبقى الذاكرة ثابتة مهما كان الحجم.
  • تصل .pipe(): تتدفق القراءة نحو الكتابة تلقائيًا، مع تدبير التدفق — قاعدة 80/20 للتدفقات في سطر واحد.
  • الأحجام الكبيرة = التدفقات: الملفات الكبيرة والرفوع وتدفقات الشبكة — والملفات الصغيرة يكفيها readFile.

حالة مستمرة — ساليم ينسخ الأرشيف دون إسقاط الخادم

على ساليم أرشفة تقارير السنة: تصدير وحيد بحجم 2 Go. محاولته الأولى بـreadFile فجّرت ذاكرة خادم الفريق الصغير. فيتحوّل إلى التدفقات (Streams): قراءة قطعًا، ونسخ نحو الأرشيف عبر .pipe()، ثم خط أنابيب قراءة ← ضغط ← كتابة لتقسيم الحجم. لا تكاد الذاكرة تتحرك، وتبدأ المعالجة منذ القطعة الأولى. وفي 2.4، سيرسل خادمه الملفات الكبيرة للعملاء بالتدفق.

تتمة الخيط الرابط 2.2: تصبح تقارير ساليم حجمًا كبيرًا يجب معالجته — وتستمر في 2.4.

1. مشكلة الأحجام الكبيرة

تخيّل معالجة ملف بحجم 2 Go. فالمقاربة الساذجة تحمّله كاملًا في الذاكرة (يضع fs.readFile كل المحتوى في متغير) — لكن 2 Go في الذاكرة ضخم، بل مستحيل: فتتشبّع ذاكرة الوصول العشوائي. تحل التدفقات (Streams) هذه المشكلة: نعالج البيانات قطعًا (chunks)، أولًا بأول، كتيار ينساب. نقرأ قطعة ونعالجها ثم ننتقل للتالية — فلا تحوي الذاكرة أبدًا إلا قطعة صغيرة. فعّال في الذاكرة (أحجام أكبر من ذاكرة الوصول العشوائي) وفي الوقت (نبدأ منذ القطعة الأولى دون انتظار التحميل الكامل). وقد قاسها ساليم: تمرّ صادراته (2 Go) ببضعة Mo من ذاكرة الوصول العشوائي.

2. أنواع التدفقات والدالة .pipe()

أربعة أنواع: Readable (قراءة من مصدر: ملف، طلب شبكة)، وWritable (كتابة نحو وجهة)، وDuplex (الاثنان معًا، كمقبس)، وTransform (يحوّل أثناء المرور: ضغط، تشفير). والتدفقات (Streams) باعثات أحداث (EventEmitter) (القسم 2-2): نقرؤها عبر 'data' (قطعة مستلَمة) و'end' (النهاية) و'error' (مشكلة). لكن الأداة الأأنق هي .pipe(): إنها تصل القراءة بالكتابة، فتنساب البيانات تلقائيًا قطعةً قطعةً مع تدبير التدفق. ولنسخ ملف كبير: fs.createReadStream('source').pipe(fs.createWriteStream('destination')) — مهما كان الحجم. بل نتسلسل: lecture.pipe(compression).pipe(ecriture).

3. المخازن المؤقتة (Buffer) ومتى نستخدم التدفقات

مفهوم مرتبط: المخازن المؤقتة (Buffer) — يعالج Node البيانات الثنائية (الملفات، الشبكة) عبر مناطق ذاكرة خام من البايتات، لأن جافاسكربت لا تملك أصلًا نوعًا ثنائيًا؛ وغالبًا ما تنقل التدفقات مخازن مؤقتة (القطع). ومتى نستخدم التدفقات: عند الأحجام الكبيرة — الملفات الكبيرة والرفوع/التنزيلات (فرفع كبير في الذاكرة يشبع الخادم: ثغرة أداء وأمان)، وتدفقات الشبكة المستمرة، والمعالجة المكثّفة، وخطوط أنابيب التحويل، وإرسال الملفات الكبيرة عبر خادم. أما الملفات الصغيرة فيكفيها readFile التقليدي: بسيط ومقروء. وتتطلب التدفقات تعلّمًا، لكنها علامة كود Node الاحترافي.

خطأ شائع في الفهم: «التدفقات معقّدة — سأحمّل كل شيء في الذاكرة، وستتحمّل ذاكرة الوصول العشوائي». خطأ. فتحميل ملف كبير كاملًا يستهلك حجمه الكامل في ذاكرة الوصول العشوائي، ويحجب المعالجة حتى تمام التحميل، ويَسقُط بعد تجاوز الذاكرة المتاحة — وعلى خادم مشترك، يكون عطلًا للجميع، كما اكتشف ساليم. فقاعدة 80/20 للتدفقات هي سطر .pipe() واحد: لا حاجة لإتقان الضغط الخلفي أو Transform المخصّص لنسخ الملفات الكبيرة أو تقديمها أو ضغطها. فالتعقيد المفترَض ليس عذرًا: يكفي للبدء المبدأ (قطعًا، أولًا بأول).
التيار بدل الدلو ذاكرة ثابتة المصدر ملف كبير،رفع، شبكةالقطع تُعالَج واحدةواحدة (مخازن مؤقتة).pipe() تنساب القراءةنحو الكتابةالوجهة أرشيف، ردّ،ملف مضغوط
ذاكرة ثابتة
نص المخطط (SEO / قارئات الشاشة / الترجمة): 1) مصدر ساليم (تصدير 2 Go) لا يتّسع في الذاكرة: وداعًا readFile. 2) يقرأ قطعًا: كل قطعة (غالبًا مخزن مؤقت (Buffer)) تُعالَج ثم تُنسى — وتبقى الذاكرة ثابتة. 3) يصل بـ.pipe(): تنساب القراءة نحو الكتابة، مع تدبير تلقائي للتدفق. 4) تستلم الوجهة الأرشيف (مضغوطًا عند الحاجة عبر Transform). الدرس: الأحجام الكبيرة = تدفق أولًا بأول؛ والملفات الصغيرة = يكفي readFile.

🔎 مثال ملموس — 🔎 مثال ملموس — أرشيف ساليم المضغوط

خط الأنابيب 80/20: اقرأ، اضغط، اكتب — دون تحميل كل شيء:

const fs = require('node:fs');
const zlib = require('node:zlib');

fs.createReadStream('export-2026.csv')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('export-2026.csv.gz'))
  .on('finish', () => console.log('Archive prête'));

القراءة: عولجت 2 Go ببضعة Mo من ذاكرة الوصول العشوائي، ومعالجة فورية.

الدرس: تسلسل التدفقات = خط أنابيب تحويل.

مفردات القسم

التدفق (Stream)
معالجة البيانات قطعًا (chunks)، أولًا بأول، بدل تحميل كل شيء في الذاكرة؛ فعّال للأحجام الكبيرة.

مثال بسيط: يؤرشف ساليم تصديره (2 Go) قطعةً قطعةً.

ما ليس كذلك: استدعاء readFile يحمّل الملف كاملًا في متغير.

رد فعل: حجم كبير = التفكير تدفقًا، لا تحميلًا كاملًا.

أنواع التدفقات
‏Readable (قراءة) وWritable (كتابة) وDuplex (الاثنان معًا، مثل المقبس) وTransform (يحوّل أثناء المرور: ضغط، تشفير).

مثال بسيط: إن gzip الخاص بساليم هو Transform بين القراءة والكتابة.

ما ليس كذلك: أربع صياغات قابلة للتبادل دون تمييز.

رد فعل: تحويل أثناء المرور = Transform.

.pipe()
تصل تدفق قراءة بتدفق كتابة: فتنساب البيانات تلقائيًا، قطعةً قطعةً، مع تدبير التدفق.

مثال بسيط: ‏createReadStream('source').pipe(createWriteStream('archive')).

ما ليس كذلك: حلقة يدوية تكدّس كل شيء قبل الكتابة.

رد فعل: من مصدر إلى وجهة = .pipe() أولًا.

المخزن المؤقت (Buffer)
منطقة ذاكرة خام من البايتات للبيانات الثنائية (الملفات، الشبكة)، التي لا تملكها جافاسكربت أصلًا؛ وتنقلها التدفقات.

مثال بسيط: تصل كل قطعة من تصدير ساليم كمخزن مؤقت (Buffer).

ما ليس كذلك: سلسلة محارف عادية.

رد فعل: بيانات ثنائية = التفكير في المخزن المؤقت (Buffer).

متى نستخدم التدفقات
متى عالجنا أحجامًا كبيرة (ملفات كبيرة، رفوع/تنزيلات، تدفقات شبكة)؛ أما الملفات الصغيرة فيكفيها readFile.

مثال بسيط: يبثّ ساليم تصدير 2 Go بالتدفق لكنه يقرأ الجرد (بضعة Ko) بـreadFile.

ما ليس كذلك: بثّ منهجي، حتى لملف إعدادات.

رد فعل: حجم التدفق واستمراريته = معيار الاختيار.

في التطبيق — تطبيق عملي — معالجة الأحجام الكبيرة بالتدفقات

  1. انسخ ملفًا بتدفق: ‏fs.createReadStream('source').pipe(fs.createWriteStream('dest')) — دون تحميل كل شيء في الذاكرة.
  2. اقرأ تدفقًا «يدويًا» بالأحداث: ‏.on('data', chunk => ...) و.on('end', ...) و.on('error', ...).
  3. سلسل عدة تدفقات بـpipe (مثل قراءة ← ضغط gzip ← كتابة) لخط أنابيب معالجة.
  4. قارن: قِس الذاكرة المستخدمة لمعالجة ملف كبير بـreadFile (كل شيء في الذاكرة) مقابل التدفق (قطعًا).
أصبحت تعالج الأحجام الكبيرة بفعالية مع التدفقات (Streams) (قطعًا، دون إشباع الذاكرة) وتتقن .pipe() لتصل القراءة بالكتابة — علامة كود Node الاحترافي.

نص جاهز — تبرير التدفق أمام الفريق (30 ثانية)

«محاولتي الأولى حمّلت التصدير كاملًا في الذاكرة فأشبعت الخادم. وبالتدفق، نعالج قطعةً قطعةً: لا تكاد الذاكرة تتحرك مهما كان الحجم، ونبدأ منذ القطعة الأولى. ويكفي سطر pipe واحد للنسخ، ونتسلسل في الضغط دون تخزين أي شيء بين المراحل.»

يُقال مع قياس الذاكرة أمام العين (readFile مقابل التدفق)؛ فالرقم يقنع أكثر من المبدأ.

نموذج نسخ ولصق — نسخ متين وخط أنابيب gzip

const fs = require('node:fs');
const zlib = require('node:zlib');
const { pipeline } = require('node:stream/promises');

async function copier(source, destination) {
  await pipeline(
    fs.createReadStream(source),
    fs.createWriteStream(destination)
  );
  console.log('Copié : ' + destination);
}

async function compresser(source, destination) {
  await pipeline(
    fs.createReadStream(source),
    zlib.createGzip(),
    fs.createWriteStream(destination)
  );
  console.log('Compressé : ' + destination);
}

copier('export-2026.csv', 'archive-2026.csv').catch(console.error);

ملف صغير

جرد ببضعة Ko: يكفي readFile — فالتدفق هنا تعقيد مجاني.

تصدير كبير للفريق

يضغط ساليم التصدير السنوي في خط أنابيب: قراءة ← gzip ← كتابة، بذاكرة ثابتة.

رفع مستخدم

استقبال ملف يرسله عميل بالتدفق: وإلا أشبع رفع كبير ذاكرة الخادم.

قراءة يدوية

الحاجة للتفاعل مع كل قطعة (عدّ الأسطر): ‏.on('data'/'end'/'error') على Readable.

حذر: الرفع المعالَج في الذاكرة يعرّض الخادم للإشباع (يرسل عميل ملفًا ضخمًا فتنهار ذاكرة الوصول العشوائي): عالج دائمًا الملفات الواردة بالتدفق وحدّد أحجامًا قصوى. يعطي هذا الدليل حركات المعالجة، لا تدقيق أمان — فاعرض الحدود على مرجعك.

✓ ممارسات جيدة

  • ابدأ بـ.pipe() (أو pipeline): قاعدة 80/20 للتدفقات في سطر واحد.
  • استمع دائمًا لـerror على تدفق يُقرأ يدويًا.
  • احفظ التدفقات للأحجام الكبيرة؛ وأبقِ readFile للملفات الصغيرة.

✗ أخطاء شائعة

  • تحميل ملف كبير بـreadFile (ذاكرة مشبَعة، سقوط).
  • نسيان مستمع error على تدفق يدوي.
  • بثّ منهجي، حتى للملفات الصغيرة جدًا.

النقاط الأساسية

  • تعالج التدفقات (Streams) البيانات قطعًا (chunks)، أولًا بأول، بدل تحميل كل شيء في الذاكرة — فعّالة للأحجام الكبيرة (ذاكرة ووقتًا).
  • الأنواع: Readable (قراءة) وWritable (كتابة) وDuplex وTransform (يحوّل أثناء المرور: ضغط، تشفير). والتدفقات باعثات أحداث (.on('data'/'end'/'error')).
  • تصل .pipe() تدفق قراءة بتدفق كتابة (تنساب البيانات تلقائيًا، قطعةً قطعةً): ‏readStream.pipe(writeStream). ونتسلسل (قراءة←ضغط←كتابة).
  • استخدم التدفقات متى عالجت أحجامًا كبيرة (ملفات كبيرة، رفوع، تدفقات شبكة)؛ أما الملفات الصغيرة فيكفيها readFile. علامة كود Node الفعّال.

تحدي 48 ساعة — طبّق غدًا

خذ ملفًا كبيرًا حقيقيًا (تصدير، سجل، فيديو) وانسخه بـ.pipe()، ثم اضغطه في خط أنابيب قراءة ← gzip ← كتابة. وقِس الذاكرة في الحالتين وقارن مع readFile.

قائمة الخروج من القسم
  • أستطيع شرح مبدأ القطع والذاكرة الثابتة.
  • أعرف النسخ والضغط بـpipe/pipeline.
  • قِست فرق الذاكرة بين التدفق وreadFile على ملف حقيقي.
تقييم ذاتي 1-5: هل أختار التدفق مقابل readFile حسب الحجم؟هل أحمي تدفقاتي بمستمع error؟

أسئلة شائعة

متى يجب فعلًا استخدام التدفقات بدل readFile/writeFile التقليدي؟

متى اقتضى الحجم أو الاستمرارية: فيحمّل readFile الملف كاملًا في الذاكرة — مثالي لبضعة Ko أو Mo، لكنه كارثي لمئات Mo أو Go (ذاكرة مشبَعة، بل سقوط). استخدم التدفقات (Streams) للملفات الكبيرة (السجلات والصادرات والوسائط)، والرفوع/التنزيلات (وإلا أشبع رفع كبير الخادم)، والتدفقات المستمرة (ردّ ضخم، بيانات حيّة)، والمعالجة المكثّفة (ملف CSV ضخم سطرًا سطرًا) وخطوط الأنابيب (اقرأ ← اضغط ← اكتب دون تخزين وسيط). قاعدة ساليم: صغير وظرفي = readFile؛ وكبير أو مستمر = تدفق.

تبدو التدفقات معقّدة: من أين أبدأ لتعلّمها تدريجيًا؟

في 4 مراحل، من البسيط إلى الكامل. 1) المبدأ: المعالجة قطعًا تنساب (الأنبوب، لا الدلو) — وهو الجوهر. 2) الدالة .pipe() للحالات البسيطة: ‏createReadStream('source').pipe(createWriteStream('dest')) — قاعدة 80/20، مع تدبير التدفق مشمولًا. 3) التسلسل: ‏lecture.pipe(gzip).pipe(ecriture)، ثم pipeline() من وحدة stream لخطوط أنابيب متينة مع تدبير جيّد للأخطاء. 4) القراءة اليدوية: ‏.on('data'/'end'/'error') عندما يجب التفاعل مع كل قطعة. وكساليم: ابدأ بالأنبوب، والباقي عندما تأتي الحاجة.

تحقّق من فهمك

على ساليم نسخ تصدير 2 Go دون إشباع ذاكرة الخادم. ماذا يفعل؟

موارد إضافية (اختياري، للتعمّق أكثر)

اختبار المستوى 2أسئلة جديدة وأصعب للتحقق من مكتسباتك