واکشی قابل سقط

نسخه اصلی GitHub برای "Aborting a Fetch" در سال 2015 باز شد. اکنون، اگر سال 2015 را از 2017 (سال جاری) حذف کنم، 2 دریافت می کنم. این نشان دهنده یک اشکال در ریاضیات است، زیرا سال 2015 در واقع "برای همیشه" پیش بود. .

سال 2015 زمانی بود که ما برای اولین بار شروع به کاوش در مورد لغو واکشی های در حال انجام کردیم، و پس از 780 نظر GitHub، چند شروع اشتباه و 5 درخواست کشش، در نهایت واکشی قابل لغو در مرورگرها را داریم که اولین مورد فایرفاکس 57 بود.

به روز رسانی: نه، من اشتباه کردم. Edge 16 ابتدا با پشتیبانی abort فرود آمد! تبریک به تیم 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 است. این عمداً عمومی است، بنابراین می تواند توسط سایر استانداردهای وب و کتابخانه های جاوا اسکریپت استفاده شود.

سیگنال ها را لغو کنید و واکشی کنید

Fetch می تواند یک 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() ) نیز لغو می‌شود.

در اینجا یک نسخه آزمایشی وجود دارد - در زمان نوشتن، تنها مرورگری که از این پشتیبانی می‌کند فایرفاکس 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 برای اولین بار این کار را انجام داد و فایرفاکس در مسیر خود بسیار خوب است. مهندسین آنها از مجموعه آزمایشی در حالی که مشخصات نوشته می شد، پیاده سازی کردند. برای سایر مرورگرها، در اینجا بلیط هایی وجود دارد که باید دنبال کنید:

در یک کارگر خدماتی

من باید مشخصات قطعات سرویس کار را تمام کنم، اما این طرح است:

همانطور که قبلا ذکر کردم، هر شی Request دارای یک ویژگی signal است. در یک سرویس‌کار، 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 شروع می شود

تلاشی برای متمایز کردن یک اقدام لغو شده از یک خطا انجام شد. این شامل یک حالت وعده سوم برای نشان دادن "لغو شد" و برخی از نحو جدید برای کنترل لغو در هر دو کد همگام و غیر همگام بود:

نکن

کد واقعی نیست - پیشنهاد پس گرفته شد

    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 مشخص کنیم. همه چیزهایی که از جاوا اسکریپت نیاز داشتیم قبلاً وجود داشت، بنابراین ما رابط‌ها را در بستر وب، به‌ویژه استاندارد DOM تعریف کردیم. وقتی این تصمیم را گرفتیم، بقیه نسبتاً سریع جمع شدند.

تغییر مشخصات بزرگ

XMLHttpRequest سال‌ها قابل سقط بود، اما مشخصات آن بسیار مبهم بود. مشخص نبود که در چه نقاطی می‌توان از فعالیت شبکه اصلی اجتناب کرد، یا خاتمه داد، یا اگر یک شرط مسابقه بین فراخوانی abort() و تکمیل واکشی وجود داشت، چه اتفاقی می‌افتاد.

ما می‌خواستیم این بار آن را به درستی انجام دهیم، اما منجر به یک تغییر مشخصات بزرگ شد که نیاز به بازبینی زیادی داشت (این تقصیر من است، و از آن ون کسترن و دومنیک دنیکولا تشکر می‌کنم که من را به این کار کشاندند) و مجموعه مناسبی از تست ها .

اما ما الان اینجا هستیم! ما یک وب اولیه جدید برای لغو کنش‌های همگام‌سازی داریم، و چندین واکشی را می‌توان همزمان کنترل کرد! در ادامه، به فعال کردن تغییرات اولویت در طول عمر واکشی و یک API سطح بالاتر برای مشاهده پیشرفت واکشی نگاه خواهیم کرد.