Dari Chromium 105, Anda dapat memulai permintaan sebelum seluruh isi tersedia menggunakan Streams API.
Anda dapat menggunakannya untuk:
- Panaskan server. Dengan kata lain, Anda dapat memulai permintaan setelah pengguna memfokuskan kolom input teks, dan menghapus semua header, lalu menunggu hingga pengguna menekan 'kirim' sebelum mengirim data yang dimasukkan.
- Kirim data yang dihasilkan di klien secara bertahap, seperti data audio, video, atau input.
- Buat ulang soket web melalui HTTP/2 atau HTTP/3.
Namun, karena ini adalah fitur platform web tingkat rendah, jangan dibatasi oleh ide saya. Mungkin Anda dapat memikirkan kasus penggunaan yang jauh lebih menarik untuk streaming permintaan.
Demo
Ini menunjukkan cara Anda dapat melakukan streaming data dari pengguna ke server, dan mengirim kembali data yang dapat diproses secara real time.
Ya, ini bukan contoh yang paling imajinatif, saya hanya ingin membuatnya sederhana, oke?
Lalu, bagaimana cara kerjanya?
Sebelumnya di petualangan menarik aliran pengambilan
Streaming Response telah tersedia di semua browser modern sejak beberapa waktu lalu. Fungsi ini memungkinkan Anda mengakses bagian respons saat respons tersebut diterima dari server:
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');
Setiap value
adalah Uint8Array
byte.
Jumlah array yang Anda dapatkan dan ukuran array bergantung pada kecepatan jaringan.
Jika menggunakan koneksi cepat, Anda akan mendapatkan lebih sedikit 'bagian' data yang lebih besar.
Jika koneksi internet Anda lambat, Anda akan mendapatkan lebih banyak potongan yang lebih kecil.
Jika ingin mengonversi byte menjadi teks, Anda dapat menggunakan TextDecoder
, atau aliran transformasi yang lebih baru jika browser target Anda mendukungnya:
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
adalah aliran transformasi yang mengambil semua bagian Uint8Array
tersebut dan mengonversinya menjadi string.
Streaming sangat bagus, karena Anda dapat mulai menindaklanjuti data saat data tersebut tiba. Misalnya, jika menerima daftar 100 'hasil', Anda dapat menampilkan hasil pertama segera setelah menerimanya, bukan menunggu ke-100.
Ngomong-ngomong, itu adalah aliran respons, hal baru yang menarik yang ingin saya bicarakan adalah aliran permintaan.
Isi permintaan streaming
Permintaan dapat memiliki isi:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Sebelumnya, Anda harus menyiapkan seluruh isi sebelum dapat memulai permintaan, tetapi sekarang di Chromium 105, Anda dapat memberikan ReadableStream
data Anda sendiri:
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',
});
Kode di atas akan mengirim "This is a slow request" ke server, satu kata pada satu waktu, dengan jeda satu detik di antara setiap kata.
Setiap bagian isi permintaan harus berupa Uint8Array
byte, jadi saya menggunakan pipeThrough(new TextEncoderStream())
untuk melakukan konversi.
Pembatasan
Permintaan streaming adalah kemampuan baru untuk web, sehingga memiliki beberapa batasan:
Half duplex?
Agar streaming dapat digunakan dalam permintaan, opsi permintaan duplex
harus ditetapkan ke 'half'
.
Fitur HTTP yang kurang dikenal (meskipun, apakah ini adalah perilaku standar bergantung pada siapa yang Anda tanyakan) adalah Anda dapat mulai menerima respons saat masih mengirim permintaan. Namun, karena sangat jarang diketahui, format ini tidak didukung dengan baik oleh server, dan tidak didukung oleh browser apa pun.
Di browser, respons tidak akan tersedia hingga isi permintaan dikirim sepenuhnya, meskipun server mengirim respons lebih cepat. Hal ini berlaku untuk semua pengambilan browser.
Pola default ini dikenal sebagai 'half duplex'.
Namun, beberapa implementasi, seperti fetch
di Deno, ditetapkan secara default ke 'full duplex' untuk pengambilan streaming, yang berarti respons dapat tersedia sebelum permintaan selesai.
Jadi, untuk mengatasi masalah kompatibilitas ini, di browser, duplex: 'half'
perlu ditentukan pada permintaan yang memiliki isi streaming.
Di masa mendatang, duplex: 'full'
mungkin didukung di browser untuk permintaan streaming dan non-streaming.
Sementara itu, hal terbaik berikutnya untuk komunikasi dupleks adalah melakukan satu pengambilan dengan permintaan streaming, lalu melakukan pengambilan lain untuk menerima respons streaming. Server akan memerlukan beberapa cara untuk mengaitkan kedua permintaan ini, seperti ID di URL. Begitulah cara kerja demo.
Pengalihan yang dibatasi
Beberapa bentuk pengalihan HTTP mengharuskan browser mengirim ulang isi permintaan ke URL lain. Untuk mendukung hal ini, browser harus melakukan buffering konten streaming, yang akan mengurangi kualitasnya, sehingga browser tidak melakukannya.
Sebagai gantinya, jika permintaan memiliki isi streaming, dan responsnya adalah pengalihan HTTP selain 303, pengambilan akan ditolak dan pengalihan tidak akan diikuti.
Pengalihan 303 diizinkan, karena secara eksplisit mengubah metode menjadi GET
dan menghapus isi permintaan.
Memerlukan CORS dan memicu preflight
Permintaan streaming memiliki isi, tetapi tidak memiliki header Content-Length
.
Ini adalah jenis permintaan baru, sehingga CORS diperlukan, dan permintaan ini selalu memicu preflight.
Permintaan no-cors
streaming tidak diizinkan.
Tidak berfungsi di HTTP/1.x
Pengambilan akan ditolak jika koneksinya adalah HTTP/1.x.
Hal ini karena, menurut aturan HTTP/1.1, isi permintaan dan respons harus mengirim header Content-Length
, sehingga pihak lain mengetahui jumlah data yang akan diterima, atau mengubah format pesan untuk menggunakan encoding dengan bagian. Dengan encoding yang dikelompokkan, isi dibagi menjadi beberapa bagian, masing-masing dengan panjang kontennya sendiri.
Encoding chunked cukup umum dalam hal respons HTTP/1.1, tetapi sangat jarang dalam hal permintaan, sehingga terlalu berisiko kompatibilitas.
Potensi masalah
Ini adalah fitur baru, dan fitur yang kurang digunakan di internet saat ini. Berikut beberapa masalah yang harus diperhatikan:
Ketidakcocokan di sisi server
Beberapa server aplikasi tidak mendukung permintaan streaming, dan menunggu permintaan lengkap diterima sebelum mengizinkan Anda melihatnya, yang agak membingungkan. Sebagai gantinya, gunakan server aplikasi yang mendukung streaming, seperti NodeJS atau Deno.
Namun, Anda belum sepenuhnya aman. Server aplikasi, seperti NodeJS, biasanya berada di belakang server lain, yang sering disebut "server frontend", yang pada akhirnya mungkin berada di belakang CDN. Jika salah satu dari server tersebut memutuskan untuk melakukan buffering pada permintaan sebelum memberikannya ke server berikutnya dalam rantai, Anda akan kehilangan manfaat streaming permintaan.
Inkompatibel di luar kendali Anda
Karena fitur ini hanya berfungsi melalui HTTPS, Anda tidak perlu khawatir tentang proxy antara Anda dan pengguna, tetapi pengguna mungkin menjalankan proxy di komputernya. Beberapa software perlindungan internet melakukan hal ini agar dapat memantau semua yang terjadi antara browser dan jaringan, dan mungkin ada kasus saat software ini buffering isi permintaan.
Jika ingin melindungi dari hal ini, Anda dapat membuat 'pengujian fitur' yang mirip dengan demo di atas, tempat Anda mencoba melakukan streaming beberapa data tanpa menutup streaming. Jika server menerima data, server dapat merespons melalui pengambilan yang berbeda. Setelah hal ini terjadi, Anda tahu bahwa klien mendukung permintaan streaming secara menyeluruh.
Deteksi fitur
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 {
// …
}
Jika Anda penasaran, berikut cara kerja deteksi fitur:
Jika tidak mendukung jenis body
tertentu, browser akan memanggil toString()
pada objek dan menggunakan hasilnya sebagai isi.
Jadi, jika browser tidak mendukung aliran permintaan, isi permintaan akan menjadi string "[object ReadableStream]"
.
Saat digunakan sebagai isi, string akan menetapkan header Content-Type
ke text/plain;charset=UTF-8
dengan mudah.
Jadi, jika header tersebut ditetapkan, kita tahu bahwa browser tidak mendukung streaming dalam objek permintaan, dan kita dapat keluar lebih awal.
Safari mendukung streaming dalam objek permintaan, tetapi tidak mengizinkannya digunakan dengan fetch
, sehingga opsi duplex
diuji, yang saat ini tidak didukung Safari.
Menggunakan dengan aliran yang dapat ditulis
Terkadang, lebih mudah untuk menggunakan streaming jika Anda memiliki WritableStream
.
Anda dapat melakukannya menggunakan aliran 'identitas', yang merupakan pasangan yang dapat dibaca/ditulis yang mengambil apa pun yang diteruskan ke ujung yang dapat ditulis, dan mengirimkannya ke ujung yang dapat dibaca.
Anda dapat membuat salah satu dari ini dengan membuat TransformStream
tanpa argumen apa pun:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Sekarang, apa pun yang Anda kirim ke aliran data yang dapat ditulis akan menjadi bagian dari permintaan. Hal ini memungkinkan Anda menyusun streaming secara bersamaan. Misalnya, berikut adalah contoh sederhana saat data diambil dari satu URL, dikompresi, dan dikirim ke URL lain:
// 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,
});
Contoh di atas menggunakan aliran kompresi untuk mengompresi data arbitrer menggunakan gzip.