คำขอสตรีมมิงที่มี API การดึงข้อมูล

เจค อาร์ชิบาลด์
เจค อาร์ชิบาลด์

ตั้งแต่ 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