ปัญหาเดิมใน 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 อื่นๆ ดูเหมือนจะเป็นตัวเลือกที่เหมาะสม นอกจากนี้ การให้สัญญาแบบเชนสามารถยกเลิกได้จะมีความซับซ้อนมากจนแทบเป็นไปไม่ได้
หากต้องการแสดงผลออบเจ็กต์ที่แสดงการตอบกลับ แต่สามารถยกเลิกได้ด้วย คุณสามารถสร้างตัวห่อแบบง่ายดังนี้
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 ระดับสูงขึ้นเพื่อดูความคืบหน้าในการดึงข้อมูล