เปิดปัญหาเดิมของ GitHub เกี่ยวกับ "การยกเลิกการดึงข้อมูล" ในปี 2015 ตอนนี้ถ้าผมเอาปี 2015 ออกจากปี 2017 (ปีปัจจุบัน) ผมจะได้รับ 2 ซึ่งเป็นการแสดงถึง ข้อบกพร่องในวิชาคณิตศาสตร์ เพราะที่จริงแล้วปี 2015 นั้น "ตลอดกาล" ที่ผ่านมา
ปี 2015 คือครั้งแรกที่เราเริ่มศึกษาล้มเลิกการดึงที่ดำเนินอยู่ และหลังจากที่มีความคิดเห็น 780 รายการใน GitHub มี 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
จะล้มเลิก ส่งผลให้การดึงข้อมูลภายในโปรแกรมทำงานของบริการก็ล้มเลิกเช่นกัน
หากคุณดึงข้อมูลสิ่งอื่นที่ไม่ใช่ 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
มีความพยายามที่จะทำให้การดำเนินการที่ยกเลิกแตกต่างจากข้อผิดพลาด ซึ่งรวมถึงสถานะสัญญาครั้งที่ 3 ที่ระบุว่า "cancelled" และไวยากรณ์ใหม่ที่ใช้จัดการกับการยกเลิกทั้งในการซิงค์และโค้ดที่ไม่พร้อมกัน
ไม่ใช่โค้ดจริง ข้อเสนอถูกเพิกถอนแล้ว
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 ที่ช่วยชี้แนะฉัน) และชุดการทดสอบที่เหมาะสม
แต่เราอยู่ที่นี่แล้ว! เรามีเว็บแบบใหม่แบบเดิมๆ สำหรับล้มเลิกการดำเนินการที่ไม่พร้อมกัน คุณจึงควบคุมการดึงข้อมูลหลายรายการพร้อมกันได้ด้วย ในอนาคต เราจะดูการเปิดใช้การเปลี่ยนแปลงลำดับความสำคัญตลอดอายุของการดึงข้อมูล และใช้ API ระดับสูงขึ้นเพื่อสังเกตความคืบหน้าในการดึงข้อมูล