با Web Streams API برای پردازش کارآمد داده در جاوا اسکریپت آشنا شوید. نحوه ایجاد، تبدیل و مصرف استریمها برای بهبود عملکرد و مدیریت حافظه را بیاموزید.
Web Streams API: خطوط لوله پردازش کارآمد داده در جاوا اسکریپت
Web Streams API یک مکانیزم قدرتمند برای مدیریت دادههای جریانی (streaming data) در جاوا اسکریپت فراهم میکند که امکان ساخت برنامههای وب کارآمد و واکنشگرا را میدهد. به جای بارگذاری کل مجموعه دادهها در حافظه به یکباره، استریمها به شما اجازه میدهند تا دادهها را به صورت تدریجی پردازش کنید، که این امر مصرف حافظه را کاهش داده و عملکرد را بهبود میبخشد. این ویژگی به خصوص هنگام کار با فایلهای بزرگ، درخواستهای شبکه یا فیدهای داده زنده بسیار مفید است.
Web Streams چه هستند؟
در هسته خود، Web Streams API سه نوع اصلی از استریمها را فراهم میکند:
- ReadableStream: نمایانگر یک منبع داده است، مانند یک فایل، اتصال شبکه یا دادههای تولید شده.
- WritableStream: نمایانگر یک مقصد برای دادهها است، مانند یک فایل، اتصال شبکه یا یک پایگاه داده.
- TransformStream: نمایانگر یک خط لوله تبدیل بین یک ReadableStream و یک WritableStream است. این استریم میتواند دادهها را حین عبور از جریان، تغییر داده یا پردازش کند.
این انواع استریم با هم کار میکنند تا خطوط لوله پردازش داده کارآمدی ایجاد کنند. دادهها از یک ReadableStream جریان مییابند، از طریق TransformStreamهای اختیاری عبور میکنند و در نهایت به یک WritableStream میرسند.
مفاهیم و اصطلاحات کلیدی
- Chunks (تکهها): دادهها در واحدهای گسستهای به نام چانک پردازش میشوند. یک چانک میتواند هر مقدار جاوا اسکریپتی باشد، مانند یک رشته، عدد یا شیء.
- Controllers (کنترلکنندهها): هر نوع استریم یک شیء کنترلکننده متناظر دارد که متدهایی برای مدیریت استریم فراهم میکند. به عنوان مثال، ReadableStreamController به شما اجازه میدهد دادهها را به استریم اضافه کنید (enqueue)، در حالی که 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()
را تعریف میکند که یک عدد را به استریم اضافه میکند (enqueue) و سپس دوباره خود را پس از یک تأخیر کوتاه فراخوانی میکند. متد controller.close()
زمانی فراخوانی میشود که شمارنده به ۱۰ برسد، که نشان میدهد استریم به پایان رسیده است.
مصرف یک ReadableStream
برای مصرف داده از یک ReadableStream، میتوانید از یک ReadableStreamDefaultReader
استفاده کنید. این reader متدهایی برای خواندن چانکها از استریم فراهم میکند. مهمترین این متدها، متد read()
است که یک promise را برمیگرداند که با یک شیء حاوی چانک داده و یک پرچم (flag) که نشاندهنده پایان استریم است، resolve میشود.
در اینجا یک مثال از مصرف داده از 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()
یک چانک از استریم را میخواند، آن را در کنسول لاگ میکند و سپس دوباره خود را فراخوانی میکند تا زمانی که استریم به پایان برسد.
ایجاد یک WritableStream
شما میتوانید با استفاده از سازنده WritableStream()
یک WritableStream ایجاد کنید. این سازنده یک شیء به عنوان آرگومان میگیرد که میتواند چندین متد برای کنترل رفتار استریم تعریف کند. مهمترین این متدها، متد write()
است که زمانی فراخوانی میشود که یک چانک داده آماده نوشته شدن باشد، متد close()
که هنگام بسته شدن استریم فراخوانی میشود، و متد abort()
که هنگام لغو شدن استریم فراخوانی میشود.
در اینجا یک مثال از ایجاد WritableStream که هر چانک داده را در کنسول لاگ میکند، آورده شده است:
const writableStream = new WritableStream({
write(chunk) {
console.log('Writing:', chunk);
return Promise.resolve(); // Indicate success
},
close() {
console.log('Stream closed');
},
abort(err) {
console.error('Stream aborted:', err);
},
});
در این مثال، متد write()
چانک را در کنسول لاگ میکند و یک promise برمیگرداند که پس از نوشته شدن موفقیتآمیز چانک، resolve میشود. متدهای close()
و abort()
به ترتیب پیامهایی را در کنسول لاگ میکنند که استریم بسته یا لغو میشود.
نوشتن در یک WritableStream
برای نوشتن داده در یک WritableStream، میتوانید از یک WritableStreamDefaultWriter
استفاده کنید. این writer متدهایی برای نوشتن چانکها در استریم فراهم میکند. مهمترین این متدها، متد write()
است که یک چانک داده را به عنوان آرگومان میگیرد و یک promise برمیگرداند که پس از نوشته شدن موفقیتآمیز چانک، resolve میشود.
در اینجا یک مثال از نوشتن داده در 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) {
// Optional: Perform any final operations when the stream is closing
},
});
در این مثال، متد transform()
چانک را به حروف بزرگ تبدیل کرده و آن را به صف کنترلکننده اضافه میکند (enqueue). متد 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)
فشار معکوس (Backpressure) یک مکانیزم حیاتی در Web Streams است که از سرریز شدن یک مصرفکننده کند توسط یک تولیدکننده سریع جلوگیری میکند. زمانی که مصرفکننده قادر به همگام شدن با نرخ تولید داده نباشد، میتواند به تولیدکننده سیگنال دهد که سرعت خود را کاهش دهد. این کار از طریق کنترلکننده استریم و اشیاء reader/writer انجام میشود.
زمانی که صف داخلی یک ReadableStream پر باشد، متد pull()
تا زمانی که فضا در صف خالی شود، فراخوانی نخواهد شد. به طور مشابه، متد write()
یک WritableStream میتواند یک promise را برگرداند که تنها زمانی resolve میشود که استریم آماده پذیرش دادههای بیشتر باشد.
با مدیریت صحیح فشار معکوس، میتوانید اطمینان حاصل کنید که خطوط لوله پردازش داده شما قوی و کارآمد هستند، حتی زمانی که با نرخهای داده متغیر سروکار دارید.
موارد استفاده و مثالها
۱. پردازش فایلهای بزرگ
Web Streams API برای پردازش فایلهای بزرگ بدون بارگذاری کامل آنها در حافظه ایدهآل است. شما میتوانید فایل را به صورت چانک بخوانید، هر چانک را پردازش کرده و نتایج را در فایل یا استریم دیگری بنویسید.
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) {
// Example: Convert each line to uppercase
const lines = chunk.split('\n');
lines.forEach(line => controller.enqueue(line.toUpperCase() + '\n'));
}
});
await readableStream.pipeThrough(transformStream).pipeTo(writableStream);
console.log('File processing complete!');
}
// Example Usage (Node.js required)
// const fs = require('fs');
// processFile('input.txt', 'output.txt');
۲. مدیریت درخواستهای شبکه
شما میتوانید از Web Streams API برای پردازش دادههای دریافتی از درخواستهای شبکه، مانند پاسخهای API یا رویدادهای ارسالی از سرور (server-sent events) استفاده کنید. این به شما امکان میدهد تا به محض رسیدن دادهها، پردازش آنها را شروع کنید، به جای اینکه منتظر بمانید تا کل پاسخ دانلود شود.
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);
// Process the received data
console.log('Received:', text);
}
} catch (error) {
console.error('Error reading from stream:', error);
} finally {
reader.releaseLock();
}
}
// Example Usage
// fetchAndProcessData('https://example.com/api/data');
۳. فیدهای داده زنده (Real-Time)
Web Streams همچنین برای مدیریت فیدهای داده زنده، مانند قیمت سهام یا خوانش سنسورها، مناسب هستند. شما میتوانید یک ReadableStream را به یک منبع داده متصل کرده و دادههای ورودی را به محض رسیدن پردازش کنید.
// Example: Simulating a real-time data feed
const readableStream = new ReadableStream({
start(controller) {
let intervalId = setInterval(() => {
const data = Math.random(); // Simulate sensor reading
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();
// Stop the stream after 10 seconds
setTimeout(() => {readableStream.cancel()}, 10000);
مزایای استفاده از Web Streams API
- بهبود عملکرد: پردازش تدریجی دادهها، کاهش مصرف حافظه و بهبود واکنشگرایی.
- مدیریت حافظه بهبود یافته: جلوگیری از بارگذاری کل مجموعه دادهها در حافظه، به ویژه برای فایلهای بزرگ یا استریمهای شبکه.
- تجربه کاربری بهتر: شروع سریعتر پردازش و نمایش دادهها، فراهم کردن یک تجربه کاربری تعاملیتر و واکنشگراتر.
- پردازش داده سادهشده: ایجاد خطوط لوله پردازش داده ماژولار و قابل استفاده مجدد با استفاده از TransformStreams.
- پشتیبانی از فشار معکوس: مدیریت نرخهای داده متغیر و جلوگیری از سرریز شدن مصرفکنندهها.
ملاحظات و بهترین شیوهها
- مدیریت خطا: پیادهسازی مدیریت خطای قوی برای رسیدگی به خطاهای استریم و جلوگیری از رفتار غیرمنتظره برنامه.
- مدیریت منابع: آزاد کردن صحیح منابع زمانی که استریمها دیگر مورد نیاز نیستند برای جلوگیری از نشت حافظه. از
reader.releaseLock()
استفاده کنید و اطمینان حاصل کنید که استریمها در زمان مناسب بسته یا لغو میشوند. - کدگذاری و کدگشایی: برای مدیریت دادههای متنی از
TextEncoderStream
وTextDecoderStream
استفاده کنید تا از کدگذاری صحیح کاراکترها اطمینان حاصل شود. - سازگاری مرورگر: قبل از استفاده از Web Streams API، سازگاری مرورگرها را بررسی کنید و استفاده از polyfillها را برای مرورگرهای قدیمیتر در نظر بگیرید.
- تست: خطوط لوله پردازش داده خود را به طور کامل آزمایش کنید تا اطمینان حاصل شود که تحت شرایط مختلف به درستی عمل میکنند.
نتیجهگیری
Web Streams API یک روش قدرتمند و کارآمد برای مدیریت دادههای جریانی در جاوا اسکریپت فراهم میکند. با درک مفاهیم اصلی و استفاده از انواع مختلف استریم، میتوانید برنامههای وب قوی و واکنشگرایی بسازید که میتوانند فایلهای بزرگ، درخواستهای شبکه و فیدهای داده زنده را به راحتی مدیریت کنند. پیادهسازی فشار معکوس و پیروی از بهترین شیوهها برای مدیریت خطا و منابع، تضمین میکند که خطوط لوله پردازش داده شما قابل اعتماد و با کارایی بالا باشند. با ادامه تکامل برنامههای وب و مدیریت دادههای پیچیدهتر، Web Streams API به ابزاری ضروری برای توسعهدهندگان در سراسر جهان تبدیل خواهد شد.