Дізнайтеся про Web Streams API для ефективної обробки даних у JavaScript. Навчіться створювати, трансформувати та споживати потоки для покращення продуктивності та керування пам'яттю.
Web Streams API: Ефективні конвеєри обробки даних у JavaScript
Web Streams API надає потужний механізм для обробки потокових даних у JavaScript, дозволяючи створювати ефективні та чутливі веб-додатки. Замість завантаження цілих наборів даних у пам'ять одночасно, потоки дозволяють обробляти дані інкрементально, зменшуючи споживання пам'яті та підвищуючи продуктивність. Це особливо корисно при роботі з великими файлами, мережевими запитами або потоками даних у реальному часі.
Що таке Web Streams?
За своєю суттю, Web Streams API надає три основні типи потоків:
- ReadableStream: Представляє джерело даних, таке як файл, мережеве з'єднання або згенеровані дані.
- WritableStream: Представляє місце призначення для даних, таке як файл, мережеве з'єднання або база даних.
- TransformStream: Представляє конвеєр трансформації між ReadableStream та WritableStream. Він може змінювати або обробляти дані під час їх проходження через потік.
Ці типи потоків працюють разом для створення ефективних конвеєрів обробки даних. Дані течуть від ReadableStream, через необов'язкові TransformStreams, і, нарешті, до WritableStream.
Ключові поняття та термінологія
- Фрагменти (Chunks): Дані обробляються окремими одиницями, що називаються фрагментами. Фрагмент може бути будь-яким значенням JavaScript, таким як рядок, число або об'єкт.
- Контролери (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()
, який повертає проміс, що вирішується з об'єктом, який містить фрагмент даних та прапорець, що вказує на завершення потоку.
Ось приклад споживання даних з ReadableStream, створеного в попередньому прикладі:
const reader = readableStream.getReader();
async function read() {
const { done, value } = await reader.read();
if (done) {
console.log('Потік завершено');
return;
}
console.log('Отримано:', value);
read();
}
read();
У цьому прикладі функція read()
читає фрагмент з потоку, виводить його в консоль, а потім викликає себе знову, доки потік не буде завершено.
Створення WritableStream
Ви можете створити WritableStream за допомогою конструктора WritableStream()
. Конструктор приймає об'єкт як аргумент, який може визначати кілька методів для керування поведінкою потоку. Найважливішими з них є метод write()
, який викликається, коли фрагмент даних готовий до запису, метод close()
, який викликається при закритті потоку, і метод abort()
, який викликається при перериванні потоку.
Ось приклад створення WritableStream, який виводить кожен фрагмент даних у консоль:
const writableStream = new WritableStream({
write(chunk) {
console.log('Запис:', chunk);
return Promise.resolve(); // Позначити успіх
},
close() {
console.log('Потік закрито');
},
abort(err) {
console.error('Потік перервано:', err);
},
});
У цьому прикладі метод write()
виводить фрагмент у консоль і повертає проміс, який вирішується, коли фрагмент успішно записано. Методи close()
та abort()
виводять повідомлення в консоль, коли потік закривається або переривається відповідно.
Запис у WritableStream
Щоб записати дані у WritableStream, ви можете використовувати WritableStreamDefaultWriter
. Письменник надає методи для запису фрагментів у потік. Найважливішим з них є метод write()
, який приймає фрагмент даних як аргумент і повертає проміс, що вирішується, коли фрагмент успішно записано.
Ось приклад запису даних у WritableStream, створений у попередньому прикладі:
const writer = writableStream.getWriter();
async function writeData() {
await writer.write('Привіт, світе!');
await writer.close();
}
writeData();
У цьому прикладі функція writeData()
записує рядок "Привіт, світе!" у потік, а потім закриває його.
Створення 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('Запис:', chunk);
return Promise.resolve();
},
});
readableStream.pipeThrough(transformStream).pipeTo(writableStream);
У цьому прикладі метод pipeThrough()
з'єднує readableStream
з transformStream
, а потім метод pipeTo()
з'єднує transformStream
з writableStream
. Дані течуть від ReadableStream, через TransformStream (де вони перетворюються у верхній регістр), а потім до WritableStream (де вони виводяться в консоль).
Зворотний тиск
Зворотний тиск — це ключовий механізм у Web Streams, який запобігає перевантаженню повільного споживача швидким виробником. Коли споживач не встигає за швидкістю виробництва даних, він може сигналізувати виробнику про необхідність уповільнення. Це досягається за допомогою контролера потоку та об'єктів читача/письменника.
Коли внутрішня черга ReadableStream заповнена, метод pull()
не буде викликаний, доки в черзі не з'явиться вільне місце. Аналогічно, метод write()
WritableStream може повертати проміс, який вирішується лише тоді, коли потік готовий прийняти більше даних.
Правильно обробляючи зворотний тиск, ви можете забезпечити надійність та ефективність ваших конвеєрів обробки даних, навіть при роботі зі змінною швидкістю даних.
Сценарії використання та приклади
1. Обробка великих файлів
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) {
// Приклад: перетворення кожного рядка у верхній регістр
const lines = chunk.split('\n');
lines.forEach(line => controller.enqueue(line.toUpperCase() + '\n'));
}
});
await readableStream.pipeThrough(transformStream).pipeTo(writableStream);
console.log('Обробку файлу завершено!');
}
// Приклад використання (потрібен Node.js)
// const fs = require('fs');
// processFile('input.txt', 'output.txt');
2. Обробка мережевих запитів
Ви можете використовувати 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);
// Обробити отримані дані
console.log('Отримано:', text);
}
} catch (error) {
console.error('Помилка читання з потоку:', error);
} finally {
reader.releaseLock();
}
}
// Приклад використання
// fetchAndProcessData('https://example.com/api/data');
3. Потоки даних у реальному часі
Web Streams також підходять для обробки потоків даних у реальному часі, таких як ціни на акції або показники датчиків. Ви можете підключити ReadableStream до джерела даних і обробляти вхідні дані по мірі їх надходження.
// Приклад: симуляція потоку даних у реальному часі
const readableStream = new ReadableStream({
start(controller) {
let intervalId = setInterval(() => {
const data = Math.random(); // Симуляція показника з датчика
controller.enqueue(`Дані: ${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('Потік закрито.');
break;
}
console.log('Отримано:', value);
}
} catch (error) {
console.error('Помилка читання з потоку:', error);
} finally {
reader.releaseLock();
}
}
readStream();
// Зупинити потік через 10 секунд
setTimeout(() => {readableStream.cancel()}, 10000);
Переваги використання Web Streams API
- Підвищена продуктивність: Обробляйте дані інкрементально, зменшуючи споживання пам'яті та покращуючи чутливість.
- Покращене керування пам'яттю: Уникайте завантаження цілих наборів даних у пам'ять, що особливо корисно для великих файлів або мережевих потоків.
- Кращий користувацький досвід: Починайте обробляти та відображати дані раніше, забезпечуючи більш інтерактивний та чутливий користувацький досвід.
- Спрощена обробка даних: Створюйте модульні та багаторазові конвеєри обробки даних за допомогою TransformStreams.
- Підтримка зворотного тиску: Обробляйте змінні швидкості даних і запобігайте перевантаженню споживачів.
Рекомендації та найкращі практики
- Обробка помилок: Впроваджуйте надійну обробку помилок для коректного реагування на збої потоків та запобігання несподіваній поведінці програми.
- Керування ресурсами: Правильно звільняйте ресурси, коли потоки більше не потрібні, щоб уникнути витоків пам'яті. Використовуйте
reader.releaseLock()
і переконайтеся, що потоки закриваються або перериваються належним чином. - Кодування та декодування: Використовуйте
TextEncoderStream
таTextDecoderStream
для обробки текстових даних, щоб забезпечити правильне кодування символів. - Сумісність з браузерами: Перевіряйте сумісність браузерів перед використанням Web Streams API та розглядайте можливість використання поліфілів для старих браузерів.
- Тестування: Ретельно тестуйте ваші конвеєри обробки даних, щоб переконатися в їх правильній роботі за різних умов.
Висновок
Web Streams API надає потужний та ефективний спосіб обробки потокових даних у JavaScript. Розуміючи основні концепції та використовуючи різні типи потоків, ви можете створювати надійні та чутливі веб-додатки, які здатні легко обробляти великі файли, мережеві запити та потоки даних у реальному часі. Впровадження зворотного тиску та дотримання найкращих практик з обробки помилок та керування ресурсами забезпечить надійність та високу продуктивність ваших конвеєрів обробки даних. Оскільки веб-додатки продовжують розвиватися та обробляти все більш складні дані, Web Streams API стане незамінним інструментом для розробників у всьому світі.