การดึงข้อมูลที่ล้มเลิกได้

ปัญหาเดิมใน GitHub สําหรับ "การยกเลิกการดึงข้อมูล" ได้เปิดขึ้นในปี 2015 ตอนนี้หากหัก 2015 ออกจาก 2017 (ปีปัจจุบัน) ฉันจะได้ 2 นี่เป็นข้อบกพร่องทางคณิตศาสตร์ เนื่องจากปี 2015 นั้นผ่านไปนานแล้ว

ปี 2015 เป็นปีที่เราได้เริ่มสำรวจการยกเลิกการดึงข้อมูลที่กำลังดำเนินอยู่เป็นครั้งแรก และหลังจากความคิดเห็นใน GitHub 780 รายการ การเริ่มต้นที่ไม่สำเร็จ 2 ครั้ง และการดึงข้อมูล 5 รายการ ในที่สุดเราก็ได้การดึงข้อมูลแบบยกเลิกได้ในเบราว์เซอร์ โดยเบราว์เซอร์แรกที่รองรับคือ Firefox 57

ข้อมูลอัปเดต: เราคิดผิด Edge 16 รองรับการยกเลิกก่อนใคร ขอแสดงความยินดีกับทีม Edge

เราจะเจาะลึกประวัติในภายหลัง แต่ก่อนอื่นมาพูดถึง API

การควบคุม + การเปลี่ยนสัญญาณ

พบกับ AbortController และ AbortSignal

const controller = new AbortController(); const signal = controller.signal; 

ตัวควบคุมมีเพียงเมธอดเดียวเท่านั้น ดังนี้

controller.abort(); 

เมื่อดำเนินการดังกล่าว ระบบจะแจ้งสัญญาณดังนี้

signal.addEventListener('abort', () => {     // Logs true:     console.log(signal.aborted); }); 

API นี้มาจากมาตรฐาน DOM และนี่คือ API ทั้งหมด มาตรฐานนี้มีความทั่วไปโดยเจตนาเพื่อให้มาตรฐานเว็บและไลบรารี JavaScript อื่นๆ นำไปใช้ได้

ยกเลิกสัญญาณและการดึงข้อมูล

การดึงข้อมูลอาจใช้เวลา AbortSignal ตัวอย่างเช่น วิธีตั้งค่าการหมดเวลาการดึงข้อมูลหลังจากผ่านไป 5 วินาทีมีดังนี้

const controller = new AbortController(); const signal = controller.signal;  setTimeout(() => controller.abort(), 5000);  fetch(url, { signal }).then(response => {     return response.text(); }).then(text => {     console.log(text); }); 

เมื่อคุณยกเลิกการดึงข้อมูล ระบบจะยกเลิกทั้งคําขอและการตอบกลับ ดังนั้นการอ่านเนื้อหาการตอบกลับ (เช่น response.text()) ก็จะยกเลิกด้วย

ดูการสาธิตได้ที่นี่ – ขณะเขียนบทความนี้ เบราว์เซอร์เดียวที่รองรับฟีเจอร์นี้คือ Firefox 57 และโปรดทราบว่าไม่มีใครที่มีทักษะด้านการออกแบบเข้ามาเกี่ยวข้องในการสร้างเดโมนี้

หรือจะให้สัญญาณกับออบเจ็กต์คำขอแล้วส่งไปยังการดึงข้อมูลในภายหลังก็ได้

const controller = new AbortController(); const signal = controller.signal; const request = new Request(url, { signal });  fetch(request); 

การดำเนินการนี้ได้ผลเนื่องจาก request.signal เป็น AbortSignal

การตอบสนองต่อการดึงข้อมูลที่ถูกยกเลิก

เมื่อคุณยกเลิกการดำเนินการแบบแอสซิงค์ พรมิสจะปฏิเสธด้วย DOMException ที่มีชื่อว่า AbortError ดังนี้

fetch(url, { signal }).then(response => {     return response.text(); }).then(text => {     console.log(text); }).catch(err => {     if (err.name === 'AbortError') {     console.log('Fetch aborted');     } else {     console.error('Uh oh, an error!', err);     } }); 

คุณไม่จําเป็นต้องแสดงข้อความแสดงข้อผิดพลาดบ่อยครั้งหากผู้ใช้ยกเลิกการดำเนินการ เนื่องจากนี่ไม่ใช่ "ข้อผิดพลาด" หากทําตามคําขอของผู้ใช้เรียบร้อยแล้ว หากต้องการหลีกเลี่ยงปัญหานี้ ให้ใช้คำสั่ง if เช่น คำสั่งด้านบนเพื่อจัดการข้อผิดพลาดในการยกเลิกโดยเฉพาะ

ต่อไปนี้คือตัวอย่างที่แสดงปุ่มให้ผู้ใช้โหลดเนื้อหาและปุ่มยกเลิก หากการดึงข้อมูลมีข้อผิดพลาด ระบบจะแสดงข้อผิดพลาด เว้นแต่จะเป็นข้อผิดพลาดในการยกเลิก

// This will allow us to abort the fetch. let controller;  // Abort if the user clicks: abortBtn.addEventListener('click', () => {     if (controller) controller.abort(); });  // Load the content: loadBtn.addEventListener('click', async () => {     controller = new AbortController();     const signal = controller.signal;      // Prevent another click until this fetch is done     loadBtn.disabled = true;     abortBtn.disabled = false;      try {     // Fetch the content & use the signal for aborting     const response = await fetch(contentUrl, { signal });     // Add the content to the page     output.innerHTML = await response.text();     }     catch (err) {     // Avoid showing an error message if the fetch was aborted     if (err.name !== 'AbortError') {         output.textContent = "Oh no! Fetching failed.";     }     }      // These actions happen no matter how the fetch ends     loadBtn.disabled = false;     abortBtn.disabled = true; }); 

ดูการสาธิตได้ที่นี่ – ขณะเขียนบทความนี้ เบราว์เซอร์ที่รองรับฟีเจอร์นี้มีเพียง Edge 16 และ Firefox 57

สัญญาณเดียว ดึงข้อมูลหลายครั้ง

คุณใช้สัญญาณเดียวเพื่อยกเลิกการดึงข้อมูลหลายรายการพร้อมกันได้ ดังนี้

async function fetchStory({ signal } = {}) {     const storyResponse = await fetch('/story.json', { signal });     const data = await storyResponse.json();      const chapterFetches = data.chapterUrls.map(async url => {     const response = await fetch(url, { signal });     return response.text();     });      return Promise.all(chapterFetches); } 

ในตัวอย่างข้างต้น ระบบจะใช้สัญญาณเดียวกันสำหรับการดึงข้อมูลครั้งแรกและสำหรับการดึงข้อมูลบทแบบขนาน วิธีใช้ fetchStory มีดังนี้

const controller = new AbortController(); const signal = controller.signal;  fetchStory({ signal }).then(chapters => {     console.log(chapters); }); 

ในกรณีนี้ การเรียกใช้ controller.abort() จะยกเลิกการดึงข้อมูลที่กำลังดำเนินการอยู่

อนาคต

เบราว์เซอร์อื่นๆ

Edge ทำได้ดีมากที่เปิดตัวฟีเจอร์นี้ก่อน และ Firefox ก็ตามติดมาติดๆ วิศวกรได้ติดตั้งใช้งานจากชุดทดสอบขณะที่เขียนข้อกำหนด สำหรับเบราว์เซอร์อื่นๆ โปรดดูคำขอแจ้งปัญหาต่อไปนี้

ใน Service Worker

เราจําเป็นต้องเขียนข้อกําหนดสําหรับส่วน Service Worker ให้เสร็จ แต่นี่คือแผนของเรา

ดังที่ได้กล่าวไปก่อนหน้านี้ ออบเจ็กต์ Request ทุกรายการจะมีพร็อพเพอร์ตี้ signal ภายใน Service Worker fetchEvent.request.signal จะส่งสัญญาณยกเลิกหากหน้าเว็บไม่สนใจการตอบกลับอีกต่อไป ด้วยเหตุนี้ โค้ดเช่นนี้จึงใช้งานได้

addEventListener('fetch', event => {     event.respondWith(fetch(event.request)); }); 

หากหน้าเว็บยกเลิกการดึงข้อมูล fetchEvent.request.signal ก็จะส่งสัญญาณยกเลิก ดังนั้นการดึงข้อมูลภายใน Service Worker ก็จะยกเลิกด้วย

หากดึงข้อมูลรายการอื่นที่ไม่ใช่ event.request คุณจะต้องส่งสัญญาณไปยังการดึงข้อมูลที่กำหนดเอง

addEventListener('fetch', event => {     const url = new URL(event.request.url);      if (event.request.method == 'GET' && url.pathname == '/about/') {     // Modify the URL     url.searchParams.set('from-service-worker', 'true');     // Fetch, but pass the signal through     event.respondWith(         fetch(url, { signal: event.request.signal })     );     } }); 

ทำตามข้อกําหนดเพื่อติดตามเรื่องนี้ เราจะเพิ่มลิงก์ไปยังตั๋วเบราว์เซอร์เมื่อพร้อมใช้งาน

ประวัติ

ใช่ เราใช้เวลานานมากในการสร้าง API ที่ค่อนข้างง่ายนี้ โดยมีเหตุผลดังต่อไปนี้

การโต้แย้งเกี่ยวกับ API

อย่างที่คุณเห็น การสนทนาใน GitHub ค่อนข้างยาว ประเด็นนี้มีความซับซ้อนมากในชุดข้อความ (และมีความซับซ้อนน้อยในบางประเด็น) แต่สิ่งที่ขัดแย้งกันหลักๆ คือกลุ่มหนึ่งต้องการให้มีเมธอด abort ในออบเจ็กต์ที่ fetch() แสดงผล ส่วนอีกกลุ่มต้องการแยกการรับการตอบกลับออกจากการส่งผลต่อคำตอบ

ข้อกำหนดเหล่านี้ใช้ร่วมกันไม่ได้ ดังนั้นกลุ่มหนึ่งจึงไม่ได้รับสิ่งที่ต้องการ หากเป็นเช่นนั้น ขออภัย เราอยู่ในกลุ่มนั้นด้วยเช่นกัน แต่การเห็นว่า AbortSignal เป็นไปตามข้อกำหนดของ API อื่นๆ ดูเหมือนจะเป็นตัวเลือกที่เหมาะสม นอกจากนี้ การให้สัญญาแบบเชนสามารถยกเลิกได้จะมีความซับซ้อนมากจนแทบเป็นไปไม่ได้

หากต้องการแสดงผลออบเจ็กต์ที่แสดงการตอบกลับ แต่สามารถยกเลิกได้ด้วย คุณสามารถสร้าง Wrapper ง่ายๆ ดังนี้

function abortableFetch(request, opts) {     const controller = new AbortController();     const signal = controller.signal;      return {     abort: () => controller.abort(),     ready: fetch(request, { ...opts, signal })     }; } 

False เริ่มต้นใน TC39

เราได้พยายามทำให้การดำเนินการที่ยกเลิกแตกต่างจากข้อผิดพลาด ซึ่งรวมถึงสถานะ Promise ลำดับที่ 3 เพื่อบ่งบอกว่า "ยกเลิกแล้ว" และไวยากรณ์ใหม่บางรายการเพื่อจัดการการยกเลิกทั้งในโค้ดแบบซิงค์และแบบแอ็กซิงโครนัสตดังนี้

ไม่ควรทำ

ไม่ใช่รหัสจริง มีการเพิกถอนข้อเสนอแล้ว

    try {       // Start spinner, then:       await someAction();     }     catch cancel (reason) {       // Maybe do nothing?     }     catch (err) {       // Show error message     }     finally {       // Stop spinner     }

สิ่งที่ต้องทำบ่อยที่สุดเมื่อการดำเนินการถูกยกเลิกคือไม่ต้องดำเนินการใดๆ โปรโปซาข้างต้นแยกการยกเลิกออกจากข้อผิดพลาด คุณจึงไม่ต้องจัดการข้อผิดพลาดในการยกเลิกโดยเฉพาะ catch cancel จะแจ้งให้คุณทราบเกี่ยวกับการดําเนินการที่ยกเลิก แต่โดยส่วนใหญ่แล้วคุณไม่จําเป็นต้องดำเนินการใดๆ

หัวข้อนี้ผ่านระยะที่ 1 ใน TC39 แต่ยังไม่ได้รับการยอมรับจากทุกฝ่าย และข้อเสนอถูกเพิกถอน

ข้อเสนอทางเลือกของเราคือ AbortController ไม่จำเป็นต้องใช้ไวยากรณ์ใหม่ จึงไม่มีเหตุผลที่จะระบุไว้ใน TC39 ทุกอย่างที่เราต้องการจาก JavaScript มีอยู่แล้ว เราจึงกำหนดอินเทอร์เฟซภายในแพลตฟอร์มเว็บ โดยเฉพาะมาตรฐาน DOM เมื่อตัดสินใจแล้ว สิ่งอื่นๆ ก็เกิดขึ้นอย่างรวดเร็ว

การเปลี่ยนแปลงข้อมูลจำเพาะที่สำคัญ

XMLHttpRequest ยกเลิกได้มานานแล้ว แต่ข้อมูลจำเพาะค่อนข้างคลุมเครือ เราไม่แน่ใจว่ากิจกรรมเครือข่ายที่เกี่ยวข้องจะหลีกเลี่ยงหรือสิ้นสุดเมื่อใด หรือจะเกิดอะไรขึ้นหากมีเงื่อนไขการแข่งขันระหว่างการเรียก abort() กับการดึงข้อมูลเสร็จสมบูรณ์

เราต้องการทำให้ถูกต้องในครั้งนี้ แต่นั่นส่งผลให้เกิดการเปลี่ยนแปลงข้อกำหนดจำนวนมากที่ต้องตรวจสอบอย่างละเอียด (เราขอโทษสำหรับเรื่องนี้และขอขอบคุณ Anne van Kesteren และ Domenic Denicola เป็นอย่างมากที่ช่วยเราแก้ปัญหานี้) และชุดการทดสอบที่เหมาะสม

แต่ตอนนี้เราพร้อมช่วยเหลือคุณแล้ว เรามี Web Primitive ใหม่สำหรับการหยุดการดำเนินการแบบแอซิงค์ และสามารถควบคุมการดึงข้อมูลหลายรายการพร้อมกันได้ ในอนาคต เราจะพิจารณาเปิดใช้การเปลี่ยนแปลงลําดับความสําคัญตลอดอายุการดึงข้อมูล และ API ระดับสูงขึ้นเพื่อดูความคืบหน้าในการดึงข้อมูล