ตั้งแต่ 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())
เพื่อแปลงให้
ข้อจำกัด
คำขอการสตรีมเป็นความสามารถใหม่สำหรับเว็บ จึงมีข้อจำกัดบางอย่างดังนี้
Half duplex
หากต้องการอนุญาตให้ใช้สตรีมในคำขอ คุณต้องตั้งค่าduplex
ตัวเลือกคำขอเป็น 'half'
ฟีเจอร์ HTTP ที่ไม่ค่อยมีใครรู้จัก (แม้ว่าลักษณะการทำงานมาตรฐานจะขึ้นอยู่กับว่าคุณถามใคร) คือคุณสามารถเริ่มรับการตอบกลับได้ในขณะที่ยังส่งคำขออยู่ อย่างไรก็ตาม มีคนรู้จักฟีเจอร์นี้น้อยมาก เซิร์ฟเวอร์จึงไม่ค่อยรองรับ และไม่มีเบราว์เซอร์ใดรองรับ
ในเบราว์เซอร์ การตอบกลับจะไม่พร้อมใช้งานจนกว่าจะส่งเนื้อหาคำขอทั้งหมด แม้ว่าเซิร์ฟเวอร์จะส่งการตอบกลับเร็วกว่าก็ตาม ซึ่งเป็นความจริงสําหรับการดึงข้อมูลของเบราว์เซอร์ทั้งหมด
รูปแบบเริ่มต้นนี้เรียกว่า "ฮาล์ฟดูเพล็กซ์"
อย่างไรก็ตาม การใช้งานบางอย่าง เช่น fetch
ใน Deno จะตั้งค่าเริ่มต้นเป็น "ฟูลดูเพล็กซ์" สำหรับการดึงข้อมูลแบบสตรีม ซึ่งหมายความว่าการตอบกลับอาจพร้อมใช้งานก่อนที่คำขอจะเสร็จสมบูรณ์
ดังนั้น เพื่อหลีกเลี่ยงปัญหาความเข้ากันได้นี้ ในเบราว์เซอร์ duplex: 'half'
ต้อง
ระบุในคำขอที่มีเนื้อหาของสตรีม
ในอนาคต เบราว์เซอร์อาจรองรับ duplex: 'full'
สำหรับคำขอแบบสตรีมมิงและแบบไม่สตรีมมิง
ในระหว่างนี้ วิธีที่ดีที่สุดรองลงมาในการสื่อสารแบบดูเพล็กซ์คือการดึงข้อมูลครั้งเดียวด้วยคำขอสตรีมมิง แล้วดึงข้อมูลอีกครั้งเพื่อรับการตอบกลับแบบสตรีมมิง เซิร์ฟเวอร์จะต้องมีวิธีเชื่อมโยงคำขอทั้ง 2 รายการนี้ เช่น รหัสใน URL การสาธิตจะทำงานในลักษณะนี้
การเปลี่ยนเส้นทางที่ถูกจำกัด
การเปลี่ยนเส้นทาง HTTP บางรูปแบบกำหนดให้เบราว์เซอร์ต้องส่งเนื้อหาของคำขอไปยัง URL อื่นอีกครั้ง เบราว์เซอร์จะต้องบัฟเฟอร์เนื้อหาของสตรีมเพื่อรองรับการดำเนินการนี้ ซึ่งจะทำให้การดำเนินการนี้ไม่สมเหตุสมผล ดังนั้นเบราว์เซอร์จึงไม่ทำเช่นนั้น
แต่หากคำขอมีเนื้อหาการสตรีม และการตอบกลับเป็นการเปลี่ยนเส้นทาง HTTP อื่นที่ไม่ใช่ 303 การดึงข้อมูลจะถูกปฏิเสธและไม่มีการติดตามการเปลี่ยนเส้นทาง
อนุญาตให้ใช้การเปลี่ยนเส้นทาง 303 เนื่องจากจะเปลี่ยนวิธีการเป็น GET
อย่างชัดเจนและทิ้งเนื้อหาของคำขอ
ต้องใช้ CORS และทริกเกอร์คำขอ Preflight
คำขอสตรีมมีเนื้อหา แต่ไม่มีส่วนหัว Content-Length
เนื่องจากเป็นคำขอประเภทใหม่ จึงต้องใช้ CORS และคำขอเหล่านี้จะทริกเกอร์คำขอก่อนส่งเสมอ
ไม่อนุญาตให้ใช้คำขอสตรีมมิง no-cors
ใช้กับ HTTP/1.x ไม่ได้
ระบบจะปฏิเสธการดึงข้อมูลหากการเชื่อมต่อเป็น HTTP/1.x
เนื่องจากตามกฎของ HTTP/1.1 เนื้อหาของคำขอและการตอบกลับต้องส่งส่วนหัว Content-Length
เพื่อให้ฝั่งตรงข้ามทราบปริมาณข้อมูลที่จะได้รับ หรือเปลี่ยนรูปแบบของข้อความเพื่อใช้การเข้ารหัสแบบเป็นกลุ่ม การเข้ารหัสแบบเป็นกลุ่มจะแบ่งเนื้อหาออกเป็นส่วนๆ โดยแต่ละส่วนจะมี 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