Od wersji Chromium 105 możesz rozpocząć żądanie, zanim cała treść będzie dostępna, korzystając z interfejsu Streams API.
Możesz to wykorzystać do:
- Rozgrzej serwer. Innymi słowy, możesz rozpocząć wysyłanie żądania, gdy użytkownik skupi się na polu wprowadzania tekstu, i usunąć wszystkie nagłówki, a potem poczekać, aż użytkownik naciśnie „Wyślij”, zanim wyślesz wprowadzone przez niego dane.
- Stopniowe wysyłanie danych wygenerowanych na urządzeniu klienta, takich jak dane audio, wideo lub dane wejściowe.
- Odtwarzanie gniazd internetowych za pomocą protokołów HTTP/2 lub HTTP/3.
Jest to jednak funkcja platformy internetowej niskiego poziomu, więc nie ograniczaj się do moich pomysłów. Może znasz ciekawszy przypadek użycia przesyłania strumieniowego żądań.
W poprzednich odcinkach ekscytujących przygód strumieni pobierania
Strumienie odpowiedzi są już od jakiegoś czasu dostępne we wszystkich nowoczesnych przeglądarkach. Umożliwiają one dostęp do części odpowiedzi w miarę ich przesyłania z serwera:
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const {value, done} = await reader.read();
if (done) break;
console.log('Received', value);
}
console.log('Response fully received');
Każdy value
to Uint8Array
bajtów.
Liczba tablic i ich rozmiar zależą od szybkości sieci.
Jeśli masz szybkie połączenie, otrzymasz mniej większych „porcji” danych.
Jeśli masz wolne połączenie, otrzymasz więcej mniejszych fragmentów.
Jeśli chcesz przekonwertować bajty na tekst, możesz użyć funkcji TextDecoder
lub nowszego strumienia przekształcania, jeśli przeglądarki docelowe go obsługują:
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
to strumień przekształcający, który pobiera wszystkie fragmenty Uint8Array
i przekształca je w ciągi znaków.
Strumienie są świetne, ponieważ możesz zacząć działać na podstawie danych, gdy tylko się pojawią. Jeśli np. otrzymujesz listę 100 „wyników”, możesz wyświetlić pierwszy z nich od razu po otrzymaniu, zamiast czekać na wszystkie 100.
W każdym razie to są strumienie odpowiedzi. Nowością, o której chciałem porozmawiać, są strumienie żądań.
Treści żądań przesyłanych strumieniowo
Żądania mogą mieć treść:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Wcześniej, aby rozpocząć wysyłanie żądania, trzeba było przygotować cały tekst. Teraz w Chromium 105 możesz podać własny ReadableStream
danych:
function wait(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
const stream = new ReadableStream({
async start(controller) {
await wait(1000);
controller.enqueue('This ');
await wait(1000);
controller.enqueue('is ');
await wait(1000);
controller.enqueue('a ');
await wait(1000);
controller.enqueue('slow ');
await wait(1000);
controller.enqueue('request.');
controller.close();
},
}).pipeThrough(new TextEncoderStream());
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'text/plain'},
body: stream,
duplex: 'half',
});
Powyższe polecenie wyśle na serwer tekst „This is a slow request” (To jest powolne żądanie), po jednym słowie naraz, z sekundową przerwą między słowami.
Każdy fragment treści żądania musi mieć rozmiar Uint8Array
bajtów, więc używam funkcji pipeThrough(new TextEncoderStream())
do przeprowadzenia konwersji.
Ograniczenia
Żądania przesyłania strumieniowego to nowa funkcja internetu, dlatego wiążą się z kilkoma ograniczeniami:
Półdupleks?
Aby umożliwić używanie strumieni w żądaniu, opcja żądania duplex
musi być ustawiona na 'half'
.
Mało znaną funkcją HTTP (chociaż to, czy jest to standardowe zachowanie, zależy od tego, kogo zapytasz) jest możliwość rozpoczęcia odbierania odpowiedzi podczas wysyłania żądania. Jest on jednak tak mało znany, że nie jest dobrze obsługiwany przez serwery ani przez żadną przeglądarkę.
W przeglądarkach odpowiedź nigdy nie jest dostępna, dopóki treść żądania nie zostanie w pełni wysłana, nawet jeśli serwer wyśle odpowiedź wcześniej. Dotyczy to wszystkich pobrań w przeglądarce.
Ten domyślny wzorzec nazywa się „half duplex”.
Jednak w niektórych implementacjach, np. fetch
w Deno, w przypadku pobierania strumieniowego domyślnie używano trybu „full duplex”, co oznacza, że odpowiedź może być dostępna, zanim żądanie zostanie ukończone.
Aby obejść ten problem ze zgodnością, w przeglądarkach w przypadku żądań zawierających treść strumienia należy określić duplex: 'half'
.
W przyszłości duplex: 'full'
może być obsługiwany w przeglądarkach w przypadku żądań strumieniowych i niestrumieniowych.
W międzyczasie najlepszym rozwiązaniem zastępującym komunikację dwukierunkową jest wysłanie jednego żądania strumieniowego, a następnie drugiego żądania w celu otrzymania odpowiedzi strumieniowej. Serwer musi mieć możliwość powiązania tych 2 żądań, np. za pomocą identyfikatora w adresie URL. Tak działa wersja demonstracyjna.
Przekierowania z ograniczeniami
Niektóre formy przekierowania HTTP wymagają, aby przeglądarka ponownie wysłała treść żądania do innego adresu URL. Aby to było możliwe, przeglądarka musiałaby buforować zawartość strumienia, co mija się z celem, więc tego nie robi.
Jeśli jednak żądanie ma treść przesyłaną strumieniowo, a odpowiedź jest przekierowaniem HTTP innym niż 303, pobieranie zostanie odrzucone, a przekierowanie nie zostanie wykonane.
Przekierowania 303 są dozwolone, ponieważ wyraźnie zmieniają metodę na GET
i odrzucają treść żądania.
Wymaga CORS i wywołuje żądanie wstępne
Żądania przesyłania strumieniowego mają treść, ale nie mają nagłówka Content-Length
.
Jest to nowy rodzaj żądania, więc wymagany jest CORS, a takie żądania zawsze wywołują żądanie wstępne.
Żądania przesyłania strumieniowego no-cors
są niedozwolone.
Nie działa w przypadku protokołu HTTP/1.x
Pobieranie zostanie odrzucone, jeśli połączenie jest typu HTTP/1.x.
Wynika to z faktu, że zgodnie z zasadami HTTP/1.1 w treściach żądań i odpowiedzi musi być wysyłany nagłówek Content-Length
, aby druga strona wiedziała, ile danych otrzyma, lub format wiadomości musi zostać zmieniony na kodowanie fragmentowe. W przypadku kodowania fragmentowego treść jest dzielona na części, z których każda ma własną długość.
Kodowanie fragmentowe jest dość powszechne w przypadku odpowiedzi HTTP/1.1, ale bardzo rzadkie w przypadku żądań, więc wiąże się ze zbyt dużym ryzykiem związanym z kompatybilnością.
Potencjalne problemy
Jest to nowa funkcja, która jest obecnie rzadko wykorzystywana w internecie. Oto kilka problemów, na które warto zwrócić uwagę:
Niezgodność po stronie serwera
Niektóre serwery aplikacji nie obsługują żądań przesyłania strumieniowego i czekają na otrzymanie pełnego żądania, zanim pozwolą Ci zobaczyć jego część, co trochę mija się z celem. Zamiast tego używaj serwera aplikacji, który obsługuje przesyłanie strumieniowe, np. NodeJS lub Deno.
Ale to jeszcze nie koniec! Serwer aplikacji, np. NodeJS, zwykle znajduje się za innym serwerem, często nazywanym „serwerem front-end”, który z kolei może znajdować się za siecią CDN. Jeśli którykolwiek z nich zdecyduje się na buforowanie żądania przed przekazaniem go do następnego serwera w łańcuchu, utracisz korzyści wynikające z przesyłania strumieniowego żądań.
Niezgodność, na którą nie masz wpływu
Ta funkcja działa tylko w przypadku protokołu HTTPS, więc nie musisz się martwić o serwery proxy między Tobą a użytkownikiem, ale użytkownik może korzystać z serwera proxy na swoim urządzeniu. Niektóre programy do ochrony internetu robią to, aby móc monitorować wszystko, co dzieje się między przeglądarką a siecią. W niektórych przypadkach mogą one buforować treść żądań.
Jeśli chcesz się przed tym zabezpieczyć, możesz utworzyć „test funkcji” podobny do powyższego przykładu, w którym spróbujesz przesyłać strumieniowo dane bez zamykania strumienia. Jeśli serwer otrzyma dane, może odpowiedzieć za pomocą innego pobierania. Gdy to nastąpi, będziesz mieć pewność, że klient obsługuje żądania przesyłania strumieniowego na całej długości.
Wykrywanie cech
const supportsRequestStreams = (() => {
let duplexAccessed = false;
const hasContentType = new Request('', {
body: new ReadableStream(),
method: 'POST',
get duplex() {
duplexAccessed = true;
return 'half';
},
}).headers.has('Content-Type');
return duplexAccessed && !hasContentType;
})();
if (supportsRequestStreams) {
// …
} else {
// …
}
Jeśli chcesz dowiedzieć się więcej, oto jak działa wykrywanie funkcji:
Jeśli przeglądarka nie obsługuje określonego typu body
, wywołuje metodę toString()
na obiekcie i używa wyniku jako treści.
Jeśli więc przeglądarka nie obsługuje strumieni żądań, treść żądania staje się ciągiem znaków "[object ReadableStream]"
.
Jeśli ciąg znaków jest używany jako treść, wygodnie ustawia nagłówek Content-Type
na text/plain;charset=UTF-8
.
Jeśli ten nagłówek jest ustawiony, wiemy, że przeglądarka nie obsługuje strumieni w obiektach żądań, i możemy wcześniej zakończyć działanie.
Safari obsługuje strumienie w obiektach żądań, ale nie zezwala na ich używanie z fetch
, dlatego testowana jest opcja duplex
, która nie jest obecnie obsługiwana przez Safari.
Używanie z strumieniami zapisu
Czasami łatwiej jest pracować ze strumieniami, gdy masz WritableStream
.
Możesz to zrobić za pomocą strumienia „tożsamość”, czyli pary do odczytu i zapisu, która przyjmuje wszystko, co jest przekazywane do jej końca do zapisu, i wysyła to do końca do odczytu.
Możesz utworzyć jeden z nich, tworząc TransformStream
bez argumentów:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Teraz wszystko, co wyślesz do strumienia zapisu, będzie częścią żądania. Dzięki temu możesz łączyć strumienie. Oto przykład, w którym dane są pobierane z jednego adresu URL, kompresowane i wysyłane na inny adres URL:
// Get from url1:
const response = await fetch(url1);
const {readable, writable} = new TransformStream();
// Compress the data from url1:
response.body.pipeThrough(new CompressionStream('gzip')).pipeTo(writable);
// Post to url2:
await fetch(url2, {
method: 'POST',
body: readable,
});
W przykładzie powyżej użyto strumieni kompresji do skompresowania dowolnych danych za pomocą gzip.