Пошук уроків, статей та іншого контенту
Зрозумійте призначення потоків, їхню подієву модель і переваги обробки даних частинами.
Потік (Stream) у Node.js — це об’єкт для послідовної обробки даних частинами.
Замість того щоб чекати, поки весь файл або відповідь мережі буде завантажена в пам’ять, програма отримує дані невеликими фрагментами — чанками (chunks).
Потоки корисні для роботи з:
великими файлами;
HTTP-запитами та відповідями;
мережевими з’єднаннями;
архівацією та розпакуванням;
передаванням даних між частинами програми.
Уявімо файл розміром 2 ГБ.
Якщо прочитати його за допомогою readFile, Node.js спочатку спробує завантажити весь файл у пам’ять:
const fs = require('node:fs');
fs.readFile('large-file.txt', 'utf8', (error, data) => {
if (error) {
console.error(error);
return;
}
console.log(data);
});Такий підхід зручний для невеликих файлів, але для великих обсягів даних може використати багато пам’яті.
Потік читає файл частинами:
const fs = require('node:fs');
const stream = fs.createReadStream('large-file.txt', {
encoding: 'utf8'
});
stream.on('data', (chunk) => {
console.log('Отримано частину:', chunk.length);
});
stream.on('end', () => {
console.log('Читання завершено');
});
stream.on('error', (error) => {
console.error('Помилка читання:', error.message);
});У цьому прикладі файл не завантажується повністю в пам’ять. Програма поступово отримує його частини та може одразу їх обробляти.
Чанк — це окрема частина даних, яку потік передає програмі.
Чанк може бути:
рядком;
об’єктом Buffer;
іншою формою даних залежно від типу потоку та його налаштувань.
Для роботи з файлами Node.js часто використовує Buffer — об’єкт для представлення бінарних даних.
Розмір чанка не обов’язково відповідає логічній структурі даних. Наприклад, один рядок може бути розділений між двома чанками. Тому не варто припускати, що кожен чанк містить повне повідомлення або повний рядок.
У Node.js є чотири основні типи потоків.
Readable — потік, з якого можна читати дані.
Приклади:
файл, який читається через fs.createReadStream;
вхідні дані HTTP-запиту;
дані, що надходять із мережі.
Writable — потік, у який можна записувати дані.
Приклади:
файл, який записується через fs.createWriteStream;
HTTP-відповідь;
стандартний потік виведення process.stdout.
Duplex може одночасно читати й записувати дані.
Прикладом є мережеве з’єднання, через яке програма може і надсилати, і отримувати дані.
Transform — спеціальний різновид Duplex, який отримує дані, змінює їх і передає далі.
Приклади:
стиснення файлу;
розпакування;
зміна формату тексту.
На цьому етапі достатньо запам’ятати:
Readable — читаємо;
Writable — записуємо;
Transform — змінюємо дані під час проходження через потік.
Потоки в Node.js працюють асинхронно та повідомляють програму про важливі події.
Для Readable часто використовують такі події:
data — надійшов новий чанк;
end — усі дані прочитано;
error — сталася помилка;
close — потік закрито.
const { Readable } = require('node:stream');
async function* generateChunks() {
const chunks = [
'Перший фрагмент\n',
'Другий фрагмент\n',
'Третій фрагмент\n'
];
for (const chunk of chunks) {
await new Promise((resolve) => setTimeout(resolve, 300));
yield chunk;
}
}
const stream = Readable.from(generateChunks());
stream.on('data', (chunk) => {
console.log('Отримано:', chunk);
});
stream.on('end', () => {
console.log('Усі дані оброблено');
});
stream.on('error', (error) => {
console.error('Помилка:', error.message);
});Щоб запустити приклад:
Збережіть код у файл streams.js.
Виконайте команду:
node streams.jsПодія data спрацює кілька разів — по одному разу для кожного чанка. Після отримання всіх частин спрацює подія end.
Для успішного читання даних типовий порядок такий:
Потік створюється.
Надходять один або кілька чанків.
Для кожного чанка спрацьовує data.
Після завершення читання спрацьовує end.
Якщо під час роботи виникає проблема, потік може створити подію error. Її варто обробляти, інакше помилка може завершити процес Node.js.
const fs = require('node:fs');
const stream = fs.createReadStream('unknown-file.txt');
stream.on('data', (chunk) => {
console.log(chunk);
});
stream.on('end', () => {
console.log('Читання завершено');
});
stream.on('error', (error) => {
console.error('Не вдалося прочитати файл:', error.message);
});Головна перевага потоків — можливість обробляти дані поступово.
Наприклад, під час читання великого файлу можна:
отримати один чанк;
обробити його;
записати результат;
перейти до наступного чанка.
У пам’яті при цьому не обов’язково зберігається весь файл.
Це особливо важливо для серверів, де одночасно можуть виконуватися десятки або сотні операцій. Якщо кожен запит завантажуватиме великі дані повністю, програма швидко споживатиме доступну пам’ять.
Для запису частин даних використовується Writable-потік. Наприклад, fs.createWriteStream створює потік для запису у файл.
const fs = require('node:fs');
const output = fs.createWriteStream('output.txt');
output.write('Перший рядок\n');
output.write('Другий рядок\n');
output.end('Останній рядок\n');
output.on('finish', () => {
console.log('Запис завершено');
});
output.on('error', (error) => {
console.error('Помилка запису:', error.message);
});Метод write додає дані до потоку, а метод end повідомляє, що нових даних більше не буде.
Після завершення запису спрацьовує подія finish.
Readable-потік можна з’єднати з Writable-потоком за допомогою методу pipe.
const fs = require('node:fs');
const source = fs.createReadStream('input.txt');
const destination = fs.createWriteStream('copy.txt');
source.pipe(destination);
destination.on('finish', () => {
console.log('Файл скопійовано');
});
source.on('error', (error) => {
console.error('Помилка читання:', error.message);
});
destination.on('error', (error) => {
console.error('Помилка запису:', error.message);
});pipe автоматично передає отримані чанки з одного потоку в інший.
У результаті файл input.txt копіюється у copy.txt, не завантажуючись повністю в пам’ять.
Потоки можуть мати різну швидкість:
джерело може швидко читати дані;
приймач може повільно їх обробляти.
Якщо джерело постійно надсилатиме дані швидше, ніж приймач може їх обробити, у пам’яті накопичуватиметься черга.
Механізм, який допомагає узгодити ці швидкості, називається зворотним тиском (backpressure).
Метод pipe враховує цей механізм і допомагає не передавати приймачу більше даних, ніж він здатен обробити.
Саме тому pipe зазвичай безпечніший за ручне безперервне читання та записування великих обсягів даних.
простий код;
зручно для невеликих файлів;
весь файл зберігається в пам’яті;
обробка починається після повного читання.
дані обробляються частинами;
можна почати роботу ще до завершення читання;
використовується менше пам’яті;
потрібно враховувати події та помилки.
Потік може розділити дані на багато чанків. Не слід обробляти перший чанк як повний результат.
Якщо потрібно виконати дію після отримання всіх даних, використовуйте подію end.
errorПотік може завершитися з помилкою: файл може не існувати, доступ може бути заборонений, а з’єднання — перерване.
Додавайте обробник:
stream.on('error', (error) => {
console.error(error.message);
});dataДані надходять асинхронно. Вони можуть бути недоступними одразу після створення потоку.
Обробляйте їх у відповідній події або використовуйте інший підтримуваний спосіб читання потоку.
Readable-потік може працювати в різних режимах. Для початкових прикладів краще обрати один підхід, наприклад слухати події data, end і error.
Потік обробляє дані частинами, а не обов’язково завантажує їх повністю.
Окрема частина даних називається чанком.
Потоки допомагають зменшити використання пам’яті та почати обробку раніше.
Readable використовується для читання, а Writable — для запису.
Подія data повідомляє про новий чанк.
Подія end означає завершення читання.
Подію error потрібно обробляти окремо.
Метод pipe передає дані з одного потоку в інший і допомагає узгодити швидкість їх обробки.