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

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