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