ตั้งแต่ Chromium 105 เป็นต้นไป คุณสามารถเริ่มคําขอได้ก่อนที่จะมีเนื้อหาทั้งหมดโดยใช้ Streams API
คุณสามารถใช้ฟีเจอร์นี้เพื่อดำเนินการต่อไปนี้
- อุ่นเครื่องเซิร์ฟเวอร์ กล่าวคือ คุณอาจเริ่มคําขอเมื่อผู้ใช้โฟกัสที่ช่องป้อนข้อความ และซ่อนส่วนหัวทั้งหมด จากนั้นรอจนกว่าผู้ใช้จะกด "ส่ง" ก่อนส่งข้อมูลที่ป้อน
- ส่งข้อมูลที่สร้างขึ้นในไคลเอ็นต์ เช่น เสียง วิดีโอ หรือข้อมูลอินพุต ทีละน้อย
- สร้างเว็บโซケットใหม่ผ่าน HTTP/2 หรือ HTTP/3
แต่เนื่องจากเป็นฟีเจอร์ระดับล่างของแพลตฟอร์มเว็บ คุณจึงไม่ต้องจำกัดความคิดไว้เพียงความคิดของฉัน คุณอาจนึกถึงกรณีการใช้งานที่น่าตื่นเต้นกว่าสตรีมมิงคำขอ
สาธิต
ตัวอย่างนี้แสดงวิธีสตรีมข้อมูลจากผู้ใช้ไปยังเซิร์ฟเวอร์ และส่งข้อมูลที่ประมวลผลได้แบบเรียลไทม์กลับ
ใช่ ตัวอย่างนี้อาจไม่ได้มีความสร้างสรรค์มากนัก แต่เราแค่ต้องการอธิบายให้เข้าใจง่าย
วิธีการมีดังนี้
ก่อนหน้านี้ในการผจญภัยอันน่าตื่นเต้นของการดึงข้อมูลสตรีม
สตรีมคำตอบพร้อมใช้งานในเบราว์เซอร์สมัยใหม่ทั้งหมดมาระยะหนึ่งแล้ว ซึ่งจะช่วยให้คุณเข้าถึงคำตอบบางส่วนได้เมื่อคำตอบดังกล่าวมาจากเซิร์ฟเวอร์
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');
value
แต่ละรายการมีความยาว Uint8Array
ไบต์
จำนวนอาร์เรย์ที่คุณได้รับและขนาดของอาร์เรย์จะขึ้นอยู่กับความเร็วของเครือข่าย
หากใช้การเชื่อมต่อที่รวดเร็ว คุณจะได้รับ "กลุ่ม" ข้อมูลขนาดใหญ่จำนวนน้อยลง
หากใช้การเชื่อมต่อที่ช้า คุณจะได้รับข้อมูลเป็นชิ้นเล็กๆ จำนวนมาก
หากต้องการแปลงไบต์เป็นข้อความ ให้ใช้ TextDecoder
หรือสตรีมการเปลี่ยนรูปแบบที่ใหม่กว่าหากเบราว์เซอร์เป้าหมายรองรับ
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
คือสตรีมการเปลี่ยนรูปแบบที่ดึงข้อมูล Uint8Array
ทั้งหมดและแปลงเป็นสตริง
สตรีมมีประโยชน์มากเนื่องจากคุณสามารถเริ่มดําเนินการกับข้อมูลได้ทันทีที่เข้ามา เช่น หากคุณได้รับรายการ "ผลลัพธ์" 100 รายการ คุณสามารถแสดงผลลัพธ์แรกทันทีที่ได้รับ แทนที่จะรอให้ระบบแสดงผลลัพธ์ทั้งหมด 100 รายการ
ต่อไปเราจะพูดถึงสตรีมคำตอบ แต่สิ่งใหม่ที่น่าตื่นเต้นที่ฉันอยากพูดถึงคือสตรีมคำขอ
เนื้อหาคำขอสตรีมมิง
คำขออาจมีเนื้อหาดังต่อไปนี้
await fetch(url, {
method: 'POST',
body: requestBody,
});
ก่อนหน้านี้คุณต้องเตรียมข้อมูลทั้งตัวให้พร้อมก่อนจึงจะเริ่มคําขอได้ แต่ตอนนี้ใน Chromium 105 คุณสามารถระบุ ReadableStream
ของข้อมูลของคุณเองได้ ดังนี้
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',
});
บรรทัดด้านบนจะส่ง "This is a slow request" ไปยังเซิร์ฟเวอร์ทีละคำ โดยหยุดพัก 1 วินาทีระหว่างแต่ละคำ
แต่ละกลุ่มของเนื้อหาคําขอต้องเป็น Uint8Array
ไบต์ ดังนั้นเราจึงใช้ pipeThrough(new TextEncoderStream())
เพื่อแปลงให้เรา
ข้อจำกัด
คำขอสตรีมมิงเป็นเครื่องมือใหม่สำหรับเว็บ จึงมีข้อจำกัดบางอย่างดังนี้
การสื่อสารแบบครึ่งอัตราส่วน
หากต้องการอนุญาตให้ใช้สตรีมในคำขอ คุณต้องตั้งค่าตัวเลือกคำขอ duplex
เป็น 'half'
ฟีเจอร์ที่ไม่ค่อยมีคนรู้จักของ HTTP (แม้ว่าลักษณะการทำงานนี้เป็นลักษณะการทำงานมาตรฐานหรือไม่นั้นขึ้นอยู่กับว่าใครเป็นคนถาม) คือคุณจะเริ่มได้รับการตอบกลับขณะที่ยังคงส่งคำขออยู่ อย่างไรก็ตาม รูปแบบนี้ไม่ค่อยมีคนรู้จัก จึงไม่ได้รับการรองรับจากเซิร์ฟเวอร์มากนัก และไม่มีเบราว์เซอร์ใดรองรับ
ในเบราว์เซอร์ การตอบกลับจะไม่พร้อมใช้งานจนกว่าระบบจะส่งเนื้อหาคําขอจนเสร็จสมบูรณ์ แม้ว่าเซิร์ฟเวอร์จะส่งการตอบกลับเร็วกว่านั้นก็ตาม ซึ่งเป็นความจริงสำหรับการดึงข้อมูลเบราว์เซอร์ทั้งหมด
รูปแบบเริ่มต้นนี้เรียกว่า "ครึ่งดูเพล็กซ์"
อย่างไรก็ตาม การใช้งานบางอย่าง เช่น fetch
ใน Deno จะตั้งค่าเริ่มต้นเป็น "Full Duplex" สำหรับการดึงข้อมูลสตรีมมิง ซึ่งหมายความว่าการตอบกลับจะพร้อมใช้งานก่อนที่คำขอจะเสร็จสมบูรณ์
ดังนั้น ในการแก้ปัญหาความเข้ากันได้นี้ ในเบราว์เซอร์ คุณต้องระบุ duplex: 'half'
ในคำขอที่มีเนื้อหาสตรีม
ในอนาคต เบราว์เซอร์อาจรองรับ duplex: 'full'
สำหรับคำขอสตรีมมิงและไม่สตรีม
ในระหว่างนี้ ทางเลือกที่ดีที่สุดรองจากการรับส่งข้อมูลแบบ 2 ทิศทางคือการดึงข้อมูล 1 ครั้งด้วยคําขอสตรีมมิง จากนั้นดึงข้อมูลอีกครั้งเพื่อรับการตอบกลับสตรีมมิง เซิร์ฟเวอร์จะต้องมีวิธีเชื่อมโยงคําขอ 2 รายการนี้ เช่น รหัสใน URL เดโมจึงทำงานด้วยวิธีนี้
การเปลี่ยนเส้นทางที่ถูกจํากัด
การเปลี่ยนเส้นทาง HTTP บางรูปแบบกำหนดให้เบราว์เซอร์ส่งเนื้อหาของคำขอไปยัง URL อื่นอีกครั้ง หากต้องการรองรับการดำเนินการนี้ เบราว์เซอร์จะต้องบัฟเฟอร์เนื้อหาของสตรีม ซึ่งทำให้เสียจุดประสงค์ไป จึงไม่ได้ดำเนินการดังกล่าว
แต่หากคำขอมีเนื้อหาสตรีมมิงและการตอบกลับเป็นการเปลี่ยนเส้นทาง HTTP อื่นที่ไม่ใช่ 303 ระบบจะปฏิเสธการดึงข้อมูลและจะไม่ทำตามการเปลี่ยนเส้นทาง
ระบบอนุญาตให้ใช้การเปลี่ยนเส้นทาง 303 เนื่องจากเปลี่ยนวิธีการเป็น GET
และทิ้งเนื้อหาคำขออย่างชัดเจน
ต้องใช้ CORS และทริกเกอร์การตรวจสอบก่อนเข้าสู่ระบบ
คำขอสตรีมมิงมีเนื้อหา แต่ไม่มีส่วนหัว Content-Length
นี่เป็นคำขอประเภทใหม่ จึงต้องใช้ CORS และคำขอเหล่านี้จะทริกเกอร์การตรวจสอบก่อนเข้าสู่ระบบเสมอ
ไม่อนุญาตให้ส่งคำขอno-cors
สตรีมมิง
ใช้ไม่ได้กับ HTTP/1.x
ระบบจะปฏิเสธการดึงข้อมูลหากการเชื่อมต่อเป็น HTTP/1.x
เนื่องจากตามกฎ HTTP/1.1 หัวของคำขอและคำตอบต้องส่งส่วนหัว Content-Length
เพื่อให้อีกฝั่งทราบปริมาณข้อมูลที่จะได้รับ หรือเปลี่ยนรูปแบบข้อความให้ใช้การเข้ารหัสแบบแบ่งกลุ่ม เมื่อใช้การเข้ารหัสแบบแบ่งกลุ่ม ระบบจะแบ่งเนื้อหาออกเป็นส่วนๆ โดยแต่ละส่วนจะมีความยาวเนื้อหาของตัวเอง
การเข้ารหัสแบบแบ่งกลุ่มเป็นที่นิยมมากสำหรับการตอบกลับ HTTP/1.1 แต่ไม่ค่อยพบในคำขอ จึงมีความเสี่ยงด้านความเข้ากันได้มากเกินไป
ก็ตามปัญหาที่อาจเกิดขึ้น
นี่เป็นฟีเจอร์ใหม่และฟีเจอร์ที่ไม่ค่อยมีคนใช้บนอินเทอร์เน็ตในปัจจุบัน ปัญหาที่ควรระวังมีดังนี้
เข้ากันไม่ได้ที่ฝั่งเซิร์ฟเวอร์
เซิร์ฟเวอร์แอปบางเครื่องไม่รองรับคำขอสตรีมมิง และรอให้ได้รับคำขอทั้งหมดก่อนที่จะให้คุณดู ซึ่งทำให้เสียเวลา แต่ให้ใช้เซิร์ฟเวอร์แอปที่รองรับการสตรีมแทน เช่น NodeJS หรือ Deno
แต่คุณยังไม่รอด โดยปกติแล้ว เซิร์ฟเวอร์แอปพลิเคชัน เช่น NodeJS จะทำงานอยู่หลังเซิร์ฟเวอร์อีกตัวหนึ่ง ซึ่งมักเรียกว่า "เซิร์ฟเวอร์ฝั่งไคลเอ็นต์" ซึ่งอาจทำงานอยู่หลัง CDN หากเซิร์ฟเวอร์ใดตัดสินใจที่จะบัฟเฟอร์คําขอก่อนที่จะส่งไปยังเซิร์ฟเวอร์ถัดไปในเชน คุณก็จะเสียสิทธิ์รับประโยชน์จากการสตรีมคําขอ
ความไม่เข้ากันได้ที่อยู่นอกเหนือการควบคุมของคุณ
เนื่องจากฟีเจอร์นี้ใช้งานได้ผ่าน HTTPS เท่านั้น คุณจึงไม่ต้องกังวลเกี่ยวกับพร็อกซีระหว่างคุณกับผู้ใช้ แต่ผู้ใช้อาจใช้พร็อกซีในเครื่องของตน ซอฟต์แวร์ป้องกันอินเทอร์เน็ตบางรายการทําเช่นนี้เพื่อให้สามารถตรวจสอบทุกอย่างที่เกิดขึ้นระหว่างเบราว์เซอร์กับเครือข่าย และอาจมีกรณีที่ซอฟต์แวร์นี้บัฟเฟอร์เนื้อหาคําขอ
หากต้องการป้องกันปัญหานี้ คุณสามารถสร้าง "การทดสอบฟีเจอร์" คล้ายกับเดโมด้านบน ซึ่งคุณจะพยายามสตรีมข้อมูลบางส่วนโดยไม่ปิดสตรีม หากเซิร์ฟเวอร์ได้รับข้อมูล ก็จะตอบกลับผ่านการดึงข้อมูลอื่นได้ เมื่อเห็นข้อความนี้ แสดงว่าไคลเอ็นต์รองรับคำขอสตรีมมิงจากต้นทางถึงปลายทาง
การตรวจหาองค์ประกอบ
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 {
// …
}
หากสงสัย ต่อไปนี้คือวิธีการทำงานของการตรวจหาสถานที่
หากเบราว์เซอร์ไม่รองรับ body
บางประเภท ระบบจะเรียกใช้ toString()
ในออบเจ็กต์และใช้ผลลัพธ์เป็นเนื้อหา
ดังนั้นหากเบราว์เซอร์ไม่รองรับสตรีมคำขอ เนื้อหาคำขอจะกลายเป็นสตริง "[object ReadableStream]"
เมื่อใช้สตริงเป็นเนื้อหา ระบบจะตั้งค่าส่วนหัว Content-Type
เป็น text/plain;charset=UTF-8
อย่างสะดวก
ดังนั้น หากมีการตั้งค่าส่วนหัวดังกล่าว เราจะทราบว่าเบราว์เซอร์ไม่รองรับสตรีมในออบเจ็กต์คำขอ และสามารถออกได้ก่อนเวลา
Safari รองรับสตรีมในออบเจ็กต์คำขอ แต่ไม่อนุญาตให้ใช้กับ fetch
จึงมีการทดสอบตัวเลือก duplex
ซึ่ง Safari ไม่รองรับในขณะนี้
การใช้กับสตรีมแบบเขียนได้
บางครั้งการทำงานกับสตรีมจะง่ายขึ้นเมื่อคุณมี WritableStream
ซึ่งทำได้โดยใช้สตรีม "ข้อมูลระบุตัวตน" ซึ่งเป็นคู่ที่อ่านได้/เขียนได้ซึ่งจะนำทุกอย่างที่ส่งไปยังปลายทางที่เขียนได้ และส่งไปยังปลายทางที่อ่านได้
คุณสร้างรายการใดรายการหนึ่งเหล่านี้ได้โดยสร้าง TransformStream
ที่ไม่มีอาร์กิวเมนต์
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
ตอนนี้ทุกอย่างที่คุณส่งไปยังสตรีมที่เขียนได้จะเป็นส่วนหนึ่งของคำขอ ซึ่งจะช่วยให้คุณคอมโพสิทสตรีมเข้าด้วยกันได้ ตัวอย่างเช่น นี่เป็นตัวอย่างที่ตลกๆ ของการดึงข้อมูลจาก URL หนึ่ง บีบอัด และส่งไปยัง 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,
});
ตัวอย่างข้างต้นใช้สตรีมการบีบอัดเพื่อบีบอัดข้อมูลแบบกำหนดเองโดยใช้ gzip