ปัญหาเดิมใน 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 ระดับสูงขึ้นเพื่อดูความคืบหน้าในการดึงข้อมูล