Od wersji Chromium 105 możesz rozpocząć żądanie, zanim będzie dostępna cała treść, używając interfejsu Streams API.
Możesz użyć tego do:
- Rozgrzej serwer. Innymi słowy, możesz rozpocząć żądanie, gdy użytkownik skupi się na polu tekstowym, a potem poczekać, aż użytkownik naciśnie „Wyślij”, zanim dane zostaną wysłane.
- stopniowo wysyłać dane wygenerowane na kliencie, takie jak dane audio, wideo lub dane wejściowe;
- Utwórz ponownie gniazda internetowe za pomocą protokołu HTTP/2 lub HTTP/3.
Ponieważ jest to funkcja niskiego poziomu platformy internetowej, nie ograniczaj się do moich pomysłów. Może znasz znacznie ciekawszy przypadek użycia strumieniowego przesyłania żądań.
Prezentacja
Pokazuje on, jak możesz przesyłać dane strumieniowo od użytkownika do serwera i zwrotnie do użytkownika, aby można je było przetwarzać w czasie rzeczywistym.
Nie jest to może najbardziej kreatywny przykład, ale chciałem zachować prostotę.
Jak to działa?
W poprzednim odcinku w ekscytujących przygodach strumieni pobierania
Strumienie odpowiedzi są od jakiegoś czasu dostępne we wszystkich nowoczesnych przeglądarkach. Umożliwiają one dostęp do części odpowiedzi w miarę ich otrzymywania 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 element value
to Uint8Array
bajtów.
Liczba uzyskanych tablic i ich rozmiar zależą od szybkości sieci.
Jeśli masz szybkie połączenie, otrzymasz mniej, ale większych „kawałków” danych.
Jeśli masz wolne połączenie, otrzymasz więcej mniejszych fragmentów.
Jeśli chcesz przekonwertować bajty na tekst, możesz użyć TextDecoder
lub nowszej funkcji transformowania strumienia, jeśli przeznaczone przeglądarki ją obsługują:
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
to strumień transformacji, który przechwytuje wszystkie te fragmenty Uint8Array
i konwertuje je na ciągi znaków.
Strumienie są świetne, ponieważ możesz zacząć działać na podstawie danych, gdy tylko się pojawią. Jeśli np. otrzymasz listę 100 „wyników”, możesz wyświetlić pierwszy wynik, gdy tylko go otrzymasz, zamiast czekać na wszystkie 100.
To są strumienie odpowiedzi. Chcę teraz opowiedzieć o nowej, ekscytującej funkcji, jaką są strumienie żądań.
Treść żądania przesyłania strumieniowego
Żądania mogą mieć podmioty:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Wcześniej, zanim można było rozpocząć żądanie, trzeba było mieć gotowe całe body. Teraz w Chromium 105 możesz podać własne 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 ciąg „To jest powolne żądanie”, dzieląc go na pojedyncze słowa i wstawiając między nimi 1-sekundową przerwę.
Każdy fragment treści żądania musi mieć Uint8Array
bajtów, więc używam funkcji pipeThrough(new TextEncoderStream())
, aby dokonać konwersji za mnie.
Ograniczenia
Żądania strumieniowe to nowa funkcja w internecie, dlatego obowiązują w ich przypadku pewne ograniczenia:
Półduplex?
Aby umożliwić używanie strumieni w żądaniu, opcja duplex
musi być ustawiona na 'half'
.
Mało znaną funkcją HTTP (chociaż czy jest to standardowe zachowanie, zależy od tego, kogo zapytasz) jest to, że możesz zacząć otrzymywać odpowiedź, gdy nadal wysyłasz żądanie. Jest on jednak tak mało znany, że serwery i przeglądarki nie obsługują go dobrze.
W przeglądarkach odpowiedź nigdy nie jest dostępna, dopóki treść żądania nie zostanie całkowicie wysłana, nawet jeśli serwer wyśle odpowiedź wcześniej. Dotyczy to wszystkich pobierania w przeglądarce.
Ten domyślny wzór to tzw. „półdupleks”.
Jednak niektóre implementacje, np. fetch
w Deno, domyślnie używają „pełnego dupleksu” do pobierania danych strumieniowo, co oznacza, że odpowiedź może być dostępna, zanim żądanie zostanie zakończone.
Aby obejść ten problem ze zgodnością, w przeglądarkach należy w żądaniach zawierających treść strumienia podać parametr duplex: 'half'
.
W przyszłości duplex: 'full'
może być obsługiwany w przeglądarkach w przypadku żądań strumieniowych i niestrumieniowych.
Tymczasem najbliższym odpowiednikiem komunikacji dwukierunkowej jest wykonanie jednego pobierania z prośbą o przesyłanie strumieniowe, a potem kolejnego pobierania w celu otrzymania odpowiedzi przesyłanej strumieniowo. Serwer musi mieć możliwość powiązania tych 2 żądań, np. za pomocą identyfikatora w adresie URL. W ten sposób działa demo.
Ograniczone przekierowania
Niektóre formy przekierowania HTTP wymagają, aby przeglądarka ponownie wysłała treść żądania do innego adresu URL. Aby to umożliwić, przeglądarka musiałaby buforować zawartość strumienia, co w pewnym sensie niweczy cel tej funkcji, więc nie robi tego.
Jeśli jednak żądanie zawiera treść strumieniową, a odpowiedź to przekierowanie HTTP inne 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 uruchamia wstępną kontrolę.
Żądania strumieniowego mają treść, ale nie mają nagłówka Content-Length
.
To nowy rodzaj żądania, więc wymagana jest zgodność z CORS, a takie żądania zawsze powodują sprawdzanie wstępne.
Prośby o strumieniowanie no-cors
są niedozwolone.
Nie działa w przypadku HTTP/1.x
Jeśli połączenie jest HTTP/1.x, pobieranie zostanie odrzucone.
Wynika to z faktu, że zgodnie z zasadami HTTP/1.1 nagłówki żądania i odpowiedzi muszą albo wysyłać nagłówek Content-Length
, aby druga strona wiedziała, ile danych otrzyma, albo zmienić format wiadomości, aby użyć kodowania w kawałkach. W przypadku kodowania w częściach treść jest dzielona na części, z których każda ma swoją długość.
Kodowanie w kawałkach jest dość powszechne w przypadku odpowiedzi w protokole HTTP/1.1, ale bardzo rzadko występuje w żądaniach, więc wiąże się z zbyt dużym ryzykiem niezgodności.
Potencjalne problemy
Jest to nowa funkcja, która jest obecnie rzadko używana 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ń strumieniowych i zamiast tego czekają na pełne żądanie, zanim wyświetlą cokolwiek, co trochę niweczy sens. Zamiast tego użyj serwera aplikacji obsługującego strumieniowanie, 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 serwerem CDN. Jeśli któryś z nich zdecyduje się na buforowanie żądania przed przekazaniem go do następnego serwera w łańcuchu, utracisz korzyści płynące ze strumieniowego przesyłania żądań.
Niezgodność niezależna od Ciebie
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 mieć włączony serwer proxy na swoim komputerze. Niektóre programy do ochrony w internecie robią to, aby monitorować wszystko, co dzieje się między przeglądarką a siecią. W niektórych przypadkach mogą one buforować treści żądania.
Aby temu zapobiec, możesz utworzyć „test funkcji” podobny do powyższego demonstracyjnego, w którym próbujesz przesyłać strumień danych bez zamykania strumienia. Jeśli serwer otrzyma dane, może odpowiedzieć za pomocą innego wywołania. Gdy to nastąpi, będziesz mieć pewność, że klient obsługuje strumieniowe żądania od początku do końca.
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 się dowiedzieć więcej, oto jak działa wykrywanie funkcji:
Jeśli przeglądarka nie obsługuje określonego typu body
, wywołuje metodę toString()
obiektu i wykorzystuje wynik jako treść.
Jeśli przeglądarka nie obsługuje strumieni żądań, treść żądania staje się ciągiem znaków "[object ReadableStream]"
.
Gdy 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 żądania, więc możemy zakończyć działanie.
Safari obsługuje strumienie w obiektach żądania, ale nie pozwala na ich używanie z fetch
, więc testowana jest opcja duplex
, której Safari obecnie nie obsługuje.
Używanie w przypadku strumieni do zapisu
Czasami łatwiej jest pracować z strumieniami, gdy masz WritableStream
.
Możesz to zrobić, używając strumienia „tożsamości”, który jest parą odczytu i zapisu, która pobiera wszystko, co jest przekazywane do końca z możliwością zapisu, i wysyła je do końca z możliwością odczytu.
Możesz utworzyć jedną z nich, tworząc funkcję TransformStream
bez argumentów:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Teraz wszystko, co wyślesz do strumienia z możliwością 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 pod 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 tym przykładzie do kompresji dowolnych danych za pomocą gzip służą strumy kompresji.