مقدمة
الـ Streams في Node.js هي واحدة من أقوى الميزات للتعامل مع البيانات المتدفقة. تسمح بمعالجة البيانات قطعةً قطعة دون تحميلها بالكامل في الذاكرة.
أنواع Streams — الأنواع الأربعة
Readable — قابل للقراءة
مصدر البيانات يمكن قراءته على شكل chunks:
const { Readable } = require('stream');
const readable = new Readable({
read(size) {
this.push(Math.random().toString());
this.push(null); // انتهاء البيانات
}
});
readable.on('data', chunk => {
console.log('جملة:', chunk.toString());
});
Writable — قابل للكتابة
مستقبل البيانات يمكن الكتابة إليه:
const { Writable } = require('stream');
const writable = new Writable({
write(chunk, encoding, callback) {
console.log('كتابة:', chunk.toString());
callback();
}
});
writable.write('سلام');
writable.end();
Transform — تحويلي
يقرأ ويعدّل ويكتب البيانات في نفس الوقت:
const { Transform } = require('stream');
const upperCase = new Transform({
transform(chunk, encoding, callback) {
this.push(chunk.toString().toUpperCase());
callback();
}
});
process.stdin.pipe(upperCase).pipe(process.stdout);
Duplex — مزدوج
يدعم القراءة والكتابة بشكل مستقل:
const { Duplex } = require('stream');
const duplex = new Duplex({
read(size) {
this.push('بيانات مقروءة');
this.push(null);
},
write(chunk, encoding, callback) {
console.log('بيانات مكتوبة:', chunk.toString());
callback();
}
});
إنشاء Custom Streams
Transform Stream لمعالجة JSON
const { Transform } = require('stream');
class JSONParser extends Transform {
constructor() {
super({ readableObjectMode: true });
this.buffer = '';
}
_transform(chunk, encoding, callback) {
this.buffer += chunk.toString();
const lines = this.buffer.split('\n');
this.buffer = lines.pop(); // الباقي
for (const line of lines) {
try {
this.push(JSON.parse(line));
} catch (err) {
this.destroy(err);
return;
}
}
callback();
}
_flush(callback) {
if (this.buffer.trim()) {
try {
this.push(JSON.parse(this.buffer));
} catch (err) {
this.destroy(err);
return;
}
}
callback();
}
}
const parser = new JSONParser();
parser.on('data', obj => console.log(obj));
parser.write('{"name": "أحمد"}\n{"name": "سارة"}');
parser.end();
Backpressure — الضغط العكسي
الـ backpressure هو آلية تمنع إغراق الـ stream بمزيد من البيانات عندما يكون المستهلك بطيئًا:
const { Readable, Writable } = require('stream');
const source = new Readable({
highWaterMark: 16,
read() {
for (let i = 0; i < 100; i++) {
const canContinue = this.push(Buffer.alloc(1024));
if (!canContinue) {
console.log('تم تطبيق backpressure عند chunk', i);
break;
}
}
}
});
const destination = new Writable({
highWaterMark: 16,
write(chunk, encoding, callback) {
setTimeout(callback, 10); // محاكاة بطء
}
});
source.pipe(destination);
استخدام highWaterMark يتحكم في حجم الـ buffer الداخلي. يمكن مراقبة الـ backpressure باستخدام حدث drain:
const canContinue = writable.write(data);
if (!canContinue) {
writable.once('drain', () => {
console.log('المخزن فارغ، يمكن الاستمرار');
});
}
Pipeline vs Pipe — الأنبوب الآمن
pipe — التقليدي
const fs = require('fs');
const readStream = fs.createReadStream('input.txt');
const writeStream = fs.createWriteStream('output.txt');
readStream.pipe(writeStream);
readStream.on('error', err => console.error(err));
writeStream.on('error', err => console.error(err));
pipeline — الآمن (موصى به)
const { pipeline } = require('stream/promises');
const fs = require('fs');
async function safeCopy() {
try {
await pipeline(
fs.createReadStream('input.txt'),
fs.createWriteStream('output.txt')
);
console.log('تم النسخ بنجاح');
} catch (err) {
console.error('فشل النسخ:', err);
}
}
pipeline ينظّف جميع الـ streams تلقائيًا عند حدوث خطأ، على عكس pipe.
Object Mode — وضع الكائنات
الـ streams تتعامل عادةً مع buffers/strings. وضع الكائنات يسمح بتمرير كائنات JavaScript:
const { Transform } = require('stream');
const filterStream = new Transform({
readableObjectMode: true,
writableObjectMode: true,
transform(chunk, encoding, callback) {
if (chunk.age > 18) {
this.push(chunk);
}
callback();
}
});
filterStream.on('data', user => console.log('مؤهل:', user.name));
filterStream.write({ name: 'خالد', age: 25 });
filterStream.write({ name: 'نور', age: 16 });
filterStream.end();
Async Iteration with Streams — التكرار غير المتزامن
يمكن استخدام for await...of مع الـ streams مباشرة:
const fs = require('fs');
const readline = require('readline');
async function processFile(path) {
const readStream = fs.createReadStream(path, { encoding: 'utf8' });
const rl = readline.createInterface({ input: readStream });
for await (const line of rl) {
if (line.trim()) {
console.log('سطر:', line);
}
}
}
processFile('data.txt').catch(console.error);
Error Handling في Streams
التعامل الصحيح مع الأخطاء
const { pipeline } = require('stream/promises');
const zlib = require('zlib');
const fs = require('fs');
async function compressFile(input, output) {
try {
await pipeline(
fs.createReadStream(input),
zlib.createGzip(),
fs.createWriteStream(output)
);
console.log('تم الضغط بنجاح');
} catch (err) {
if (err.code === 'ENOENT') {
console.error('الملف غير موجود:', input);
} else if (err.code === 'Z_DATA_ERROR') {
console.error('بيانات مضغوطة تالفة');
} else {
console.error('خطأ غير متوقع:', err);
}
}
}
أحداث الأخطاء على Streams منفردة
const stream = fs.createReadStream('non-existent.txt');
stream.on('error', err => {
if (err.code === 'ENOENT') {
console.error('الملف غير موجود');
} else {
console.error('خطأ في القراءة:', err.message);
}
});
⚠️ تحذير: لا تنسَ تسجيل معالج
errorعلى كل stream. بدون معالج، سيتم إلقاء الخطأ كاستثناء غير معالج وسينهار التطبيق.
خلاصة
- الـ Streams تقسم البيانات إلى chunks لتوفير الذاكرة.
- Readable و Writable هما النوعان الأساسيان.
- Transform يعدّل البيانات أثناء تدفقها.
- Backpressure يمنع إغراق الـ stream.
- استخدم pipeline بدلاً من pipe للتنظيف التلقائي.
- Object Mode يسمح بتمرير كائنات بدلاً من buffers.
- التكرار غير المتزامن مع
for await...ofيوفّر واجهة نظيفة.
🎯 التالي: الأنماط السلوكية في Node.js