ตั้งแต่ 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',
});
คำสั่งข้างต้นจะส่ง "นี่คือคำขอที่ช้า" ไปยังเซิร์ฟเวอร์ทีละคำ โดยหยุดระหว่างแต่ละคำชั่วคราว 1 วินาที
แต่ละส่วนของเนื้อหาคำขอต้องมีขนาด Uint8Array
ไบต์ ฉันจึงใช้ pipeThrough(new TextEncoderStream())
ในการแปลงแทน
ข้อจำกัด
คำขอสตรีมมิงเป็นวิธีการใหม่สำหรับเว็บที่มีข้อจำกัดบางประการ ดังนี้
เป็นแบบ 2 ด้านใช่ไหม
หากต้องการอนุญาตให้ใช้สตรีมในคำขอ คุณต้องตั้งค่าตัวเลือกคำขอ duplex
เป็น 'half'
ฟีเจอร์ HTTP ที่ไม่ค่อยมีคนรู้จัก (แม้ว่าฟีเจอร์นี้จะเป็นลักษณะการทำงานมาตรฐานหรือไม่ขึ้นอยู่กับผู้ที่คุณถาม) คือคุณสามารถเริ่มได้รับการตอบกลับในขณะที่ยังส่งคําขออยู่ อย่างไรก็ตาม นี่ยังไม่ค่อยจะเป็นที่รู้จัก ทำให้เซิร์ฟเวอร์ไม่มีการสนับสนุนเป็นอย่างดี และไม่มีการสนับสนุนในเบราว์เซอร์ใดเลย
ในเบราว์เซอร์จะไม่มีการตอบกลับจนกว่าจะมีการส่งเนื้อหาของคำขอโดยสมบูรณ์ แม้ว่าเซิร์ฟเวอร์จะส่งการตอบกลับเร็วกว่านั้นก็ตาม ซึ่งเป็นความจริงสำหรับการดึงข้อมูลเบราว์เซอร์ทั้งหมด
รูปแบบเริ่มต้นนี้เรียกว่า "half duplex"
อย่างไรก็ตาม การใช้งานบางอย่าง เช่น fetch
ใน Deno จะมีค่าเริ่มต้นเป็น "Full duplex" สำหรับการดึงข้อมูลสตรีมมิง ซึ่งหมายความว่าการตอบกลับจะพร้อมใช้งานก่อนที่คำขอจะเสร็จสมบูรณ์
ดังนั้น ในการแก้ปัญหาความเข้ากันได้นี้ คุณต้องระบุ duplex: 'half'
ในคำขอที่มีเนื้อหาสตรีมในเบราว์เซอร์
ในอนาคต เบราว์เซอร์อาจรองรับ duplex: 'full'
สำหรับคำขอสตรีมมิงและไม่ใช่สตรีมมิง
ในระหว่างนี้ สิ่งที่ดีที่สุดถัดไปในการสื่อสารแบบ 2 ด้านคือการดึงข้อมูล 1 ครั้งด้วยคำขอสตรีมมิง จากนั้นดึงข้อมูลอีกครั้งเพื่อรับการตอบกลับสตรีมมิง เซิร์ฟเวอร์จะต้องใช้วิธีบางอย่างในการเชื่อมโยงคำขอทั้งสองนี้ เช่น รหัสใน 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 มักจะอยู่หลังเซิร์ฟเวอร์อื่น หากมีผู้ใช้รายใดตัดสินใจบัฟเฟอร์คำขอก่อนที่จะส่งไปยังเซิร์ฟเวอร์ถัดไปในเชน คุณจะสูญเสียประโยชน์จากการสตรีมคำขอ
เข้ากันไม่ได้เมื่ออยู่นอกการควบคุม
เนื่องจากฟีเจอร์นี้จะทำงานบน 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
คุณสามารถดำเนินการได้โดยใช้สตรีม "ข้อมูลประจำตัว" ซึ่งเป็นคู่ที่อ่านได้/เขียนได้ โดยจะนำทุกสิ่งที่ส่งผ่านไปยังฝั่งที่เขียนได้และส่งไปยังฝั่งที่อ่านได้
คุณสามารถสร้าง URL เหล่านี้ได้โดยสร้าง 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