คำขอสตรีมมิงที่มี 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',
});

บรรทัดด้านบนจะส่ง "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