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

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