استكشف واجهة برمجة تطبيقات تدفقات الويب للمعالجة الفعالة للبيانات في جافاسكريبت. تعلم كيفية إنشاء التدفقات وتحويلها واستهلاكها لتحسين الأداء وإدارة الذاكرة.
واجهة برمجة تطبيقات تدفقات الويب (Web Streams API): خطوط أنابيب فعالة لمعالجة البيانات في جافاسكريبت
توفر واجهة برمجة تطبيقات تدفقات الويب (Web Streams API) آلية قوية للتعامل مع تدفق البيانات في جافاسكريبت، مما يتيح إنشاء تطبيقات ويب فعالة وسريعة الاستجابة. بدلاً من تحميل مجموعات البيانات بأكملها في الذاكرة دفعة واحدة، تسمح التدفقات بمعالجة البيانات بشكل تدريجي، مما يقلل من استهلاك الذاكرة ويحسن الأداء. هذا مفيد بشكل خاص عند التعامل مع الملفات الكبيرة أو طلبات الشبكة أو تغذيات البيانات في الوقت الفعلي.
ما هي تدفقات الويب؟
في جوهرها، توفر واجهة برمجة تطبيقات تدفقات الويب ثلاثة أنواع رئيسية من التدفقات:
- ReadableStream: يمثل مصدرًا للبيانات، مثل ملف أو اتصال شبكة أو بيانات مولدة.
- WritableStream: يمثل وجهة للبيانات، مثل ملف أو اتصال شبكة أو قاعدة بيانات.
- TransformStream: يمثل خط أنابيب تحويلي بين ReadableStream و WritableStream. يمكنه تعديل البيانات أو معالجتها أثناء تدفقها عبر الدفق.
تعمل أنواع التدفقات هذه معًا لإنشاء خطوط أنابيب فعالة لمعالجة البيانات. تتدفق البيانات من ReadableStream، عبر TransformStreams الاختيارية، وأخيرًا إلى WritableStream.
المفاهيم والمصطلحات الرئيسية
- القطع (Chunks): تتم معالجة البيانات في وحدات منفصلة تسمى القطع. يمكن أن تكون القطعة أي قيمة جافاسكريبت، مثل سلسلة نصية أو رقم أو كائن.
- وحدات التحكم (Controllers): لكل نوع من أنواع التدفق كائن تحكم مقابل يوفر طرقًا لإدارة الدفق. على سبيل المثال، يسمح ReadableStreamController بوضع البيانات في قائمة انتظار الدفق، بينما يسمح WritableStreamController بمعالجة القطع الواردة.
- الأنابيب (Pipes): يمكن ربط التدفقات معًا باستخدام طريقتي
pipeTo()
وpipeThrough()
. تربطpipeTo()
دفقًا قابلاً للقراءة (ReadableStream) بدفق قابل للكتابة (WritableStream)، بينما تربطpipeThrough()
دفقًا قابلاً للقراءة (ReadableStream) بدفق تحويلي (TransformStream)، ثم بدفق قابل للكتابة (WritableStream). - الضغط العكسي (Backpressure): آلية تسمح للمستهلك بإرسال إشارة إلى المنتج بأنه غير جاهز لتلقي المزيد من البيانات. هذا يمنع إرهاق المستهلك ويضمن معالجة البيانات بمعدل مستدام.
إنشاء ReadableStream
يمكنك إنشاء ReadableStream باستخدام المنشئ ReadableStream()
. يأخذ المنشئ كائنًا كوسيط، والذي يمكنه تحديد عدة طرق للتحكم في سلوك الدفق. أهم هذه الطرق هي طريقة start()
، التي يتم استدعاؤها عند إنشاء الدفق، وطريقة pull()
، التي يتم استدعاؤها عندما يحتاج الدفق إلى المزيد من البيانات.
فيما يلي مثال على إنشاء ReadableStream يولد سلسلة من الأرقام:
const readableStream = new ReadableStream({
start(controller) {
let counter = 0;
function push() {
if (counter >= 10) {
controller.close();
return;
}
controller.enqueue(counter++);
setTimeout(push, 100);
}
push();
},
});
في هذا المثال، تقوم طريقة start()
بتهيئة عداد وتحديد دالة push()
التي تضع رقمًا في قائمة انتظار الدفق ثم تستدعي نفسها مرة أخرى بعد تأخير قصير. يتم استدعاء طريقة controller.close()
عندما يصل العداد إلى 10، مما يشير إلى انتهاء الدفق.
استهلاك ReadableStream
لاستهلاك البيانات من ReadableStream، يمكنك استخدام ReadableStreamDefaultReader
. يوفر القارئ طرقًا لقراءة القطع من الدفق. أهم هذه الطرق هي طريقة read()
، التي تعيد وعدًا (promise) يتم حله بكائن يحتوي على قطعة البيانات وعلامة تشير إلى ما إذا كان الدفق قد انتهى.
فيما يلي مثال على استهلاك البيانات من ReadableStream الذي تم إنشاؤه في المثال السابق:
const reader = readableStream.getReader();
async function read() {
const { done, value } = await reader.read();
if (done) {
console.log('Stream complete');
return;
}
console.log('Received:', value);
read();
}
read();
في هذا المثال، تقرأ دالة read()
قطعة من الدفق، وتسجلها في وحدة التحكم (console)، ثم تستدعي نفسها مرة أخرى حتى ينتهي الدفق.
إنشاء WritableStream
يمكنك إنشاء WritableStream باستخدام المنشئ WritableStream()
. يأخذ المنشئ كائنًا كوسيط، والذي يمكنه تحديد عدة طرق للتحكم في سلوك الدفق. أهم هذه الطرق هي طريقة write()
، التي يتم استدعاؤها عندما تكون قطعة من البيانات جاهزة للكتابة، وطريقة close()
، التي يتم استدعاؤها عند إغلاق الدفق، وطريقة abort()
، التي يتم استدعاؤها عند إحباط الدفق.
فيما يلي مثال على إنشاء WritableStream يسجل كل قطعة من البيانات في وحدة التحكم:
const writableStream = new WritableStream({
write(chunk) {
console.log('Writing:', chunk);
return Promise.resolve(); // للإشارة إلى النجاح
},
close() {
console.log('Stream closed');
},
abort(err) {
console.error('Stream aborted:', err);
},
});
في هذا المثال، تقوم طريقة write()
بتسجيل القطعة في وحدة التحكم وتعيد وعدًا يتم حله عند كتابة القطعة بنجاح. تقوم طريقتا close()
و abort()
بتسجيل رسائل في وحدة التحكم عند إغلاق الدفق أو إحباطه، على التوالي.
الكتابة إلى WritableStream
للكتابة إلى WritableStream، يمكنك استخدام WritableStreamDefaultWriter
. يوفر الكاتب طرقًا لكتابة القطع إلى الدفق. أهم هذه الطرق هي طريقة write()
، التي تأخذ قطعة من البيانات كوسيط وتعيد وعدًا يتم حله عند كتابة القطعة بنجاح.
فيما يلي مثال على كتابة البيانات إلى WritableStream الذي تم إنشاؤه في المثال السابق:
const writer = writableStream.getWriter();
async function writeData() {
await writer.write('Hello, world!');
await writer.close();
}
writeData();
في هذا المثال، تكتب دالة writeData()
السلسلة النصية "Hello, world!" إلى الدفق ثم تغلق الدفق.
إنشاء TransformStream
يمكنك إنشاء TransformStream باستخدام المنشئ TransformStream()
. يأخذ المنشئ كائنًا كوسيط، والذي يمكنه تحديد عدة طرق للتحكم في سلوك الدفق. أهم هذه الطرق هي طريقة transform()
، التي يتم استدعاؤها عندما تكون قطعة من البيانات جاهزة للتحويل، وطريقة flush()
، التي يتم استدعاؤها عند إغلاق الدفق.
فيما يلي مثال على إنشاء TransformStream يحول كل قطعة من البيانات إلى أحرف كبيرة:
const transformStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
flush(controller) {
// اختياري: قم بأي عمليات نهائية عند إغلاق الدفق
},
});
في هذا المثال، تقوم طريقة transform()
بتحويل القطعة إلى أحرف كبيرة وتضعها في قائمة انتظار وحدة التحكم. يتم استدعاء طريقة flush()
عند إغلاق الدفق ويمكن استخدامها لتنفيذ أي عمليات نهائية.
استخدام TransformStreams في خطوط الأنابيب
تكون TransformStreams أكثر فائدة عند ربطها معًا لإنشاء خطوط أنابيب لمعالجة البيانات. يمكنك استخدام طريقة pipeThrough()
لربط ReadableStream بـ TransformStream، ثم بـ WritableStream.
فيما يلي مثال على إنشاء خط أنابيب يقرأ البيانات من ReadableStream، ويحولها إلى أحرف كبيرة باستخدام TransformStream، ثم يكتبها إلى WritableStream:
const readableStream = new ReadableStream({
start(controller) {
controller.enqueue('hello');
controller.enqueue('world');
controller.close();
},
});
const transformStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
});
const writableStream = new WritableStream({
write(chunk) {
console.log('Writing:', chunk);
return Promise.resolve();
},
});
readableStream.pipeThrough(transformStream).pipeTo(writableStream);
في هذا المثال، تربط طريقة pipeThrough()
الـ readableStream
بالـ transformStream
، ثم تربط طريقة pipeTo()
الـ transformStream
بالـ writableStream
. تتدفق البيانات من ReadableStream، عبر TransformStream (حيث يتم تحويلها إلى أحرف كبيرة)، ثم إلى WritableStream (حيث يتم تسجيلها في وحدة التحكم).
الضغط العكسي (Backpressure)
الضغط العكسي هو آلية حاسمة في تدفقات الويب تمنع المنتج السريع من إرهاق المستهلك البطيء. عندما يكون المستهلك غير قادر على مواكبة معدل إنتاج البيانات، يمكنه إرسال إشارة إلى المنتج للإبطاء. يتم تحقيق ذلك من خلال وحدة تحكم الدفق وكائنات القارئ/الكاتب.
عندما تكون قائمة الانتظار الداخلية لـ ReadableStream ممتلئة، لن يتم استدعاء طريقة pull()
حتى تتوفر مساحة في قائمة الانتظار. بالمثل، يمكن لطريقة write()
في WritableStream أن تعيد وعدًا يتم حله فقط عندما يكون الدفق جاهزًا لقبول المزيد من البيانات.
من خلال التعامل مع الضغط العكسي بشكل صحيح، يمكنك ضمان أن تكون خطوط أنابيب معالجة البيانات الخاصة بك قوية وفعالة، حتى عند التعامل مع معدلات بيانات متفاوتة.
حالات الاستخدام والأمثلة
1. معالجة الملفات الكبيرة
تعتبر واجهة برمجة تطبيقات تدفقات الويب مثالية لمعالجة الملفات الكبيرة دون تحميلها بالكامل في الذاكرة. يمكنك قراءة الملف على شكل قطع، ومعالجة كل قطعة، وكتابة النتائج إلى ملف أو دفق آخر.
async function processFile(inputFile, outputFile) {
const readableStream = fs.createReadStream(inputFile).pipeThrough(new TextDecoderStream());
const writableStream = fs.createWriteStream(outputFile).pipeThrough(new TextEncoderStream());
const transformStream = new TransformStream({
transform(chunk, controller) {
// مثال: تحويل كل سطر إلى أحرف كبيرة
const lines = chunk.split('\n');
lines.forEach(line => controller.enqueue(line.toUpperCase() + '\n'));
}
});
await readableStream.pipeThrough(transformStream).pipeTo(writableStream);
console.log('File processing complete!');
}
// مثال على الاستخدام (يتطلب Node.js)
// const fs = require('fs');
// processFile('input.txt', 'output.txt');
2. التعامل مع طلبات الشبكة
يمكنك استخدام واجهة برمجة تطبيقات تدفقات الويب لمعالجة البيانات المستلمة من طلبات الشبكة، مثل استجابات واجهات برمجة التطبيقات أو الأحداث المرسلة من الخادم. يتيح لك هذا بدء معالجة البيانات بمجرد وصولها، بدلاً من انتظار تنزيل الاستجابة بأكملها.
async function fetchAndProcessData(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const text = decoder.decode(value);
// معالجة البيانات المستلمة
console.log('Received:', text);
}
} catch (error) {
console.error('Error reading from stream:', error);
} finally {
reader.releaseLock();
}
}
// مثال على الاستخدام
// fetchAndProcessData('https://example.com/api/data');
3. تغذيات البيانات في الوقت الفعلي
تعد تدفقات الويب مناسبة أيضًا للتعامل مع تغذيات البيانات في الوقت الفعلي، مثل أسعار الأسهم أو قراءات أجهزة الاستشعار. يمكنك توصيل ReadableStream بمصدر بيانات ومعالجة البيانات الواردة عند وصولها.
// مثال: محاكاة تغذية بيانات في الوقت الفعلي
const readableStream = new ReadableStream({
start(controller) {
let intervalId = setInterval(() => {
const data = Math.random(); // محاكاة قراءة جهاز استشعار
controller.enqueue(`Data: ${data.toFixed(2)}`);
}, 1000);
this.cancel = () => {
clearInterval(intervalId);
controller.close();
};
},
cancel() {
this.cancel();
}
});
const reader = readableStream.getReader();
async function readStream() {
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('Stream closed.');
break;
}
console.log('Received:', value);
}
} catch (error) {
console.error('Error reading from stream:', error);
} finally {
reader.releaseLock();
}
}
readStream();
// إيقاف الدفق بعد 10 ثوانٍ
setTimeout(() => {readableStream.cancel()}, 10000);
فوائد استخدام واجهة برمجة تطبيقات تدفقات الويب
- أداء محسن: معالجة البيانات بشكل تدريجي، مما يقلل من استهلاك الذاكرة ويحسن الاستجابة.
- إدارة محسنة للذاكرة: تجنب تحميل مجموعات البيانات بأكملها في الذاكرة، وهو أمر مفيد بشكل خاص للملفات الكبيرة أو تدفقات الشبكة.
- تجربة مستخدم أفضل: بدء معالجة وعرض البيانات في وقت أقرب، مما يوفر تجربة مستخدم أكثر تفاعلية واستجابة.
- معالجة مبسطة للبيانات: إنشاء خطوط أنابيب لمعالجة البيانات قابلة للتعديل وإعادة الاستخدام باستخدام TransformStreams.
- دعم الضغط العكسي: التعامل مع معدلات البيانات المتفاوتة ومنع إرهاق المستهلكين.
اعتبارات وأفضل الممارسات
- معالجة الأخطاء: تنفيذ معالجة أخطاء قوية للتعامل مع أخطاء الدفق بأمان ومنع سلوك التطبيق غير المتوقع.
- إدارة الموارد: تحرير الموارد بشكل صحيح عندما لا تكون هناك حاجة للتدفقات لتجنب تسرب الذاكرة. استخدم
reader.releaseLock()
وتأكد من إغلاق التدفقات أو إحباطها عند الاقتضاء. - التشفير وفك التشفير: استخدم
TextEncoderStream
وTextDecoderStream
للتعامل مع البيانات النصية لضمان ترميز الأحرف الصحيح. - توافق المتصفح: تحقق من توافق المتصفح قبل استخدام واجهة برمجة تطبيقات تدفقات الويب، وفكر في استخدام polyfills للمتصفحات القديمة.
- الاختبار: اختبر خطوط أنابيب معالجة البيانات الخاصة بك بدقة للتأكد من أنها تعمل بشكل صحيح في ظل ظروف مختلفة.
الخاتمة
توفر واجهة برمجة تطبيقات تدفقات الويب طريقة قوية وفعالة للتعامل مع تدفق البيانات في جافاسكريبت. من خلال فهم المفاهيم الأساسية واستخدام أنواع التدفقات المختلفة، يمكنك إنشاء تطبيقات ويب قوية وسريعة الاستجابة يمكنها التعامل مع الملفات الكبيرة وطلبات الشبكة وتغذيات البيانات في الوقت الفعلي بسهولة. سيضمن تنفيذ الضغط العكسي واتباع أفضل الممارسات لمعالجة الأخطاء وإدارة الموارد أن تكون خطوط أنابيب معالجة البيانات الخاصة بك موثوقة وعالية الأداء. مع استمرار تطور تطبيقات الويب والتعامل مع بيانات متزايدة التعقيد، ستصبح واجهة برمجة تطبيقات تدفقات الويب أداة أساسية للمطورين في جميع أنحاء العالم.