Odkryj Web Streams API do wydajnego przetwarzania danych w JavaScript. Dowiedz si臋, jak tworzy膰, przekszta艂ca膰 i konsumowa膰 strumienie dla lepszej wydajno艣ci i zarz膮dzania pami臋ci膮.
Web Streams API: Wydajne potoki przetwarzania danych w JavaScript
Web Streams API dostarcza pot臋偶ny mechanizm do obs艂ugi danych strumieniowych w JavaScript, umo偶liwiaj膮c tworzenie wydajnych i responsywnych aplikacji internetowych. Zamiast wczytywa膰 ca艂e zbiory danych do pami臋ci naraz, strumienie pozwalaj膮 przetwarza膰 dane przyrostowo, co zmniejsza zu偶ycie pami臋ci i poprawia wydajno艣膰. Jest to szczeg贸lnie przydatne podczas pracy z du偶ymi plikami, 偶膮daniami sieciowymi lub strumieniami danych w czasie rzeczywistym.
Czym s膮 Web Streams?
W swojej istocie Web Streams API dostarcza trzy g艂贸wne typy strumieni:
- ReadableStream: Reprezentuje 藕r贸d艂o danych, takie jak plik, po艂膮czenie sieciowe lub generowane dane.
- WritableStream: Reprezentuje miejsce docelowe dla danych, takie jak plik, po艂膮czenie sieciowe lub baza danych.
- TransformStream: Reprezentuje potok transformacji mi臋dzy ReadableStream a WritableStream. Mo偶e modyfikowa膰 lub przetwarza膰 dane w miar臋 ich przep艂ywu przez strumie艅.
Te typy strumieni wsp贸艂pracuj膮 ze sob膮, tworz膮c wydajne potoki przetwarzania danych. Dane przep艂ywaj膮 z ReadableStream, przez opcjonalne TransformStreams, a na ko艅cu do WritableStream.
Kluczowe poj臋cia i terminologia
- Chunki (fragmenty): Dane s膮 przetwarzane w dyskretnych jednostkach nazywanych chunkami. Chunk mo偶e by膰 dowoln膮 warto艣ci膮 JavaScript, tak膮 jak ci膮g znak贸w, liczba czy obiekt.
- Kontrolery: Ka偶dy typ strumienia ma odpowiadaj膮cy mu obiekt kontrolera, kt贸ry dostarcza metody do zarz膮dzania strumieniem. Na przyk艂ad ReadableStreamController pozwala na kolejkowanie danych do strumienia, podczas gdy WritableStreamController pozwala na obs艂ug臋 przychodz膮cych chunk贸w.
- Potoki (Pipes): Strumienie mo偶na 艂膮czy膰 ze sob膮 za pomoc膮 metod
pipeTo()
ipipeThrough()
.pipeTo()
艂膮czy ReadableStream z WritableStream, podczas gdypipeThrough()
艂膮czy ReadableStream z TransformStream, a nast臋pnie z WritableStream. - Backpressure (przeciwci艣nienie): Mechanizm, kt贸ry pozwala konsumentowi zasygnalizowa膰 producentowi, 偶e nie jest gotowy na otrzymanie wi臋kszej ilo艣ci danych. Zapobiega to przeci膮偶eniu konsumenta i zapewnia, 偶e dane s膮 przetwarzane w zr贸wnowa偶onym tempie.
Tworzenie ReadableStream
Mo偶esz utworzy膰 ReadableStream za pomoc膮 konstruktora ReadableStream()
. Konstruktor przyjmuje jako argument obiekt, kt贸ry mo偶e definiowa膰 kilka metod kontroluj膮cych zachowanie strumienia. Najwa偶niejsze z nich to metoda start()
, kt贸ra jest wywo艂ywana podczas tworzenia strumienia, oraz metoda pull()
, kt贸ra jest wywo艂ywana, gdy strumie艅 potrzebuje wi臋cej danych.
Oto przyk艂ad tworzenia ReadableStream, kt贸ry generuje sekwencj臋 liczb:
const readableStream = new ReadableStream({
start(controller) {
let counter = 0;
function push() {
if (counter >= 10) {
controller.close();
return;
}
controller.enqueue(counter++);
setTimeout(push, 100);
}
push();
},
});
W tym przyk艂adzie metoda start()
inicjalizuje licznik i definiuje funkcj臋 push()
, kt贸ra dodaje liczb臋 do kolejki strumienia, a nast臋pnie wywo艂uje sam膮 siebie po kr贸tkim op贸藕nieniu. Metoda controller.close()
jest wywo艂ywana, gdy licznik osi膮gnie 10, sygnalizuj膮c zako艅czenie strumienia.
Konsumowanie ReadableStream
Aby konsumowa膰 dane z ReadableStream, mo偶esz u偶y膰 ReadableStreamDefaultReader
. Czytnik (reader) dostarcza metody do odczytywania chunk贸w ze strumienia. Najwa偶niejsz膮 z nich jest metoda read()
, kt贸ra zwraca obietnic臋 (promise) rozwi膮zuj膮c膮 si臋 obiektem zawieraj膮cym chunk danych oraz flag臋 wskazuj膮c膮, czy strumie艅 zosta艂 zako艅czony.
Oto przyk艂ad konsumowania danych z ReadableStream utworzonego w poprzednim przyk艂adzie:
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();
W tym przyk艂adzie funkcja read()
odczytuje chunk ze strumienia, loguje go do konsoli, a nast臋pnie wywo艂uje sam膮 siebie, dop贸ki strumie艅 nie zostanie zako艅czony.
Tworzenie WritableStream
Mo偶esz utworzy膰 WritableStream za pomoc膮 konstruktora WritableStream()
. Konstruktor przyjmuje jako argument obiekt, kt贸ry mo偶e definiowa膰 kilka metod kontroluj膮cych zachowanie strumienia. Najwa偶niejsze z nich to metoda write()
, kt贸ra jest wywo艂ywana, gdy chunk danych jest gotowy do zapisu, metoda close()
, wywo艂ywana przy zamykaniu strumienia, oraz metoda abort()
, wywo艂ywana przy przerwaniu strumienia.
Oto przyk艂ad tworzenia WritableStream, kt贸ry loguje ka偶dy chunk danych do konsoli:
const writableStream = new WritableStream({
write(chunk) {
console.log('Writing:', chunk);
return Promise.resolve(); // Wskazanie powodzenia
},
close() {
console.log('Stream closed');
},
abort(err) {
console.error('Stream aborted:', err);
},
});
W tym przyk艂adzie metoda write()
loguje chunk do konsoli i zwraca obietnic臋, kt贸ra rozwi膮zuje si臋, gdy chunk zostanie pomy艣lnie zapisany. Metody close()
i abort()
loguj膮 komunikaty do konsoli odpowiednio, gdy strumie艅 jest zamykany lub przerywany.
Zapisywanie do WritableStream
Aby zapisa膰 dane do WritableStream, mo偶esz u偶y膰 WritableStreamDefaultWriter
. Pisarz (writer) dostarcza metody do zapisywania chunk贸w do strumienia. Najwa偶niejsz膮 z nich jest metoda write()
, kt贸ra przyjmuje chunk danych jako argument i zwraca obietnic臋, kt贸ra rozwi膮zuje si臋, gdy chunk zostanie pomy艣lnie zapisany.
Oto przyk艂ad zapisywania danych do WritableStream utworzonego w poprzednim przyk艂adzie:
const writer = writableStream.getWriter();
async function writeData() {
await writer.write('Hello, world!');
await writer.close();
}
writeData();
W tym przyk艂adzie funkcja writeData()
zapisuje ci膮g znak贸w "Hello, world!" do strumienia, a nast臋pnie zamyka strumie艅.
Tworzenie TransformStream
Mo偶esz utworzy膰 TransformStream za pomoc膮 konstruktora TransformStream()
. Konstruktor przyjmuje jako argument obiekt, kt贸ry mo偶e definiowa膰 kilka metod kontroluj膮cych zachowanie strumienia. Najwa偶niejsz膮 z nich jest metoda transform()
, kt贸ra jest wywo艂ywana, gdy chunk danych jest gotowy do transformacji, oraz metoda flush()
, kt贸ra jest wywo艂ywana przy zamykaniu strumienia.
Oto przyk艂ad tworzenia TransformStream, kt贸ry konwertuje ka偶dy chunk danych na wielkie litery:
const transformStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
flush(controller) {
// Opcjonalnie: Wykonaj ko艅cowe operacje, gdy strumie艅 jest zamykany
},
});
W tym przyk艂adzie metoda transform()
konwertuje chunk na wielkie litery i dodaje go do kolejki kontrolera. Metoda flush()
jest wywo艂ywana, gdy strumie艅 jest zamykany i mo偶e by膰 u偶yta do wykonania wszelkich ko艅cowych operacji.
U偶ywanie TransformStreams w potokach
TransformStreams s膮 najbardziej u偶yteczne, gdy s膮 艂膮czone w 艂a艅cuchy w celu tworzenia potok贸w przetwarzania danych. Mo偶esz u偶y膰 metody pipeThrough()
, aby po艂膮czy膰 ReadableStream z TransformStream, a nast臋pnie z WritableStream.
Oto przyk艂ad tworzenia potoku, kt贸ry odczytuje dane z ReadableStream, konwertuje je na wielkie litery za pomoc膮 TransformStream, a nast臋pnie zapisuje je do 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);
W tym przyk艂adzie metoda pipeThrough()
艂膮czy readableStream
z transformStream
, a nast臋pnie metoda pipeTo()
艂膮czy transformStream
z writableStream
. Dane przep艂ywaj膮 z ReadableStream, przez TransformStream (gdzie s膮 konwertowane na wielkie litery), a nast臋pnie do WritableStream (gdzie s膮 logowane do konsoli).
Backpressure (przeciwci艣nienie)
Backpressure (przeciwci艣nienie) to kluczowy mechanizm w Web Streams, kt贸ry zapobiega przyt艂oczeniu wolnego konsumenta przez szybkiego producenta. Gdy konsument nie jest w stanie nad膮偶y膰 za tempem produkcji danych, mo偶e zasygnalizowa膰 producentowi, aby zwolni艂. Osi膮ga si臋 to za pomoc膮 kontrolera strumienia oraz obiekt贸w czytnika/pisarza.
Gdy wewn臋trzna kolejka ReadableStream jest pe艂na, metoda pull()
nie zostanie wywo艂ana, dop贸ki w kolejce nie zwolni si臋 miejsce. Podobnie, metoda write()
w WritableStream mo偶e zwr贸ci膰 obietnic臋, kt贸ra rozwi膮偶e si臋 dopiero, gdy strumie艅 b臋dzie gotowy na przyj臋cie wi臋kszej ilo艣ci danych.
Poprzez w艂a艣ciw膮 obs艂ug臋 backpressure mo偶esz zapewni膰, 偶e Twoje potoki przetwarzania danych s膮 solidne i wydajne, nawet przy zmiennych szybko艣ciach przesy艂ania danych.
Przypadki u偶ycia i przyk艂ady
1. Przetwarzanie du偶ych plik贸w
Web Streams API jest idealne do przetwarzania du偶ych plik贸w bez wczytywania ich w ca艂o艣ci do pami臋ci. Mo偶esz czyta膰 plik w chunkach, przetwarza膰 ka偶dy z nich i zapisywa膰 wyniki do innego pliku lub strumienia.
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) {
// Przyk艂ad: Konwertuj ka偶d膮 lini臋 na wielkie litery
const lines = chunk.split('\n');
lines.forEach(line => controller.enqueue(line.toUpperCase() + '\n'));
}
});
await readableStream.pipeThrough(transformStream).pipeTo(writableStream);
console.log('File processing complete!');
}
// Przyk艂ad u偶ycia (wymagany Node.js)
// const fs = require('fs');
// processFile('input.txt', 'output.txt');
2. Obs艂uga 偶膮da艅 sieciowych
Mo偶esz u偶ywa膰 Web Streams API do przetwarzania danych otrzymanych z 偶膮da艅 sieciowych, takich jak odpowiedzi API czy zdarzenia wysy艂ane przez serwer (server-sent events). Pozwala to na rozpocz臋cie przetwarzania danych, jak tylko dotr膮, zamiast czeka膰 na pobranie ca艂ej odpowiedzi.
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);
// Przetw贸rz otrzymane dane
console.log('Received:', text);
}
} catch (error) {
console.error('Error reading from stream:', error);
} finally {
reader.releaseLock();
}
}
// Przyk艂ad u偶ycia
// fetchAndProcessData('https://example.com/api/data');
3. Strumienie danych w czasie rzeczywistym
Web Streams nadaj膮 si臋 r贸wnie偶 do obs艂ugi strumieni danych w czasie rzeczywistym, takich jak notowania gie艂dowe czy odczyty z czujnik贸w. Mo偶esz po艂膮czy膰 ReadableStream ze 藕r贸d艂em danych i przetwarza膰 nap艂ywaj膮ce dane na bie偶膮co.
// Przyk艂ad: Symulacja strumienia danych w czasie rzeczywistym
const readableStream = new ReadableStream({
start(controller) {
let intervalId = setInterval(() => {
const data = Math.random(); // Symulacja odczytu z czujnika
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();
// Zatrzymaj strumie艅 po 10 sekundach
setTimeout(() => {readableStream.cancel()}, 10000);
Korzy艣ci z u偶ywania Web Streams API
- Poprawiona wydajno艣膰: Przetwarzaj dane przyrostowo, zmniejszaj膮c zu偶ycie pami臋ci i poprawiaj膮c responsywno艣膰.
- Ulepszone zarz膮dzanie pami臋ci膮: Unikaj wczytywania ca艂ych zbior贸w danych do pami臋ci, co jest szczeg贸lnie przydatne w przypadku du偶ych plik贸w lub strumieni sieciowych.
- Lepsze do艣wiadczenie u偶ytkownika: Rozpocznij przetwarzanie i wy艣wietlanie danych wcze艣niej, zapewniaj膮c bardziej interaktywne i responsywne do艣wiadczenie u偶ytkownika.
- Uproszczone przetwarzanie danych: Tw贸rz modu艂owe i reu偶ywalne potoki przetwarzania danych za pomoc膮 TransformStreams.
- Wsparcie dla Backpressure: Obs艂uguj zmienne szybko艣ci przesy艂ania danych i zapobiegaj przeci膮偶eniu konsument贸w.
Kwestie do rozwa偶enia i dobre praktyki
- Obs艂uga b艂臋d贸w: Zaimplementuj solidn膮 obs艂ug臋 b艂臋d贸w, aby p艂ynnie radzi膰 sobie z b艂臋dami strumienia i zapobiega膰 nieoczekiwanemu zachowaniu aplikacji.
- Zarz膮dzanie zasobami: Prawid艂owo zwalniaj zasoby, gdy strumienie nie s膮 ju偶 potrzebne, aby unika膰 wyciek贸w pami臋ci. U偶ywaj
reader.releaseLock()
i upewnij si臋, 偶e strumienie s膮 zamykane lub przerywane w odpowiednich momentach. - Kodowanie i dekodowanie: U偶ywaj
TextEncoderStream
iTextDecoderStream
do obs艂ugi danych tekstowych, aby zapewni膰 prawid艂owe kodowanie znak贸w. - Kompatybilno艣膰 z przegl膮darkami: Sprawd藕 kompatybilno艣膰 z przegl膮darkami przed u偶yciem Web Streams API i rozwa偶 u偶ycie polyfilli dla starszych przegl膮darek.
- Testowanie: Dok艂adnie przetestuj swoje potoki przetwarzania danych, aby upewni膰 si臋, 偶e dzia艂aj膮 poprawnie w r贸偶nych warunkach.
Podsumowanie
Web Streams API dostarcza pot臋偶ny i wydajny spos贸b na obs艂ug臋 danych strumieniowych w JavaScript. Rozumiej膮c kluczowe poj臋cia i wykorzystuj膮c r贸偶ne typy strumieni, mo偶esz tworzy膰 solidne i responsywne aplikacje internetowe, kt贸re z 艂atwo艣ci膮 poradz膮 sobie z du偶ymi plikami, 偶膮daniami sieciowymi i strumieniami danych w czasie rzeczywistym. Implementacja backpressure oraz przestrzeganie dobrych praktyk w zakresie obs艂ugi b艂臋d贸w i zarz膮dzania zasobami zapewni, 偶e Twoje potoki przetwarzania danych b臋d膮 niezawodne i wydajne. W miar臋 jak aplikacje internetowe ewoluuj膮 i obs艂uguj膮 coraz bardziej z艂o偶one dane, Web Streams API stanie si臋 niezb臋dnym narz臋dziem dla deweloper贸w na ca艂ym 艣wiecie.