استرجاع قابل للإلغاء

كانت مشكلة GitHub الأصلية بشأن "إلغاء عملية الجلب" في عام 2015. الآن، إذا استبعدت 2015 من 2017 (السنة الحالية)، أحصل على 2. يوضح هذا خطأ في الرياضيات، لأن عام 2015 كان في الواقع "للأبد" قبل ذلك.

كان عام 2015 عندما بدأنا في استكشاف إلغاء عمليات الجلب المستمرة، وبعد 780 تعليقًا في GitHub، عدد من عمليات البدء الخاطئة و5 طلبات سحب، لدينا أخيرًا عملية جلب قابلة للإلغاء في المتصفحات، حيث كان أول إصدار هو Firefox 57.

تعديل: لا يهمّني، لم أكن مخطئًا. تم توفير Edge 16 مع دعم الإلغاء أولاً! تهانينا على فريق Edge!

سأتعمق في السجل لاحقًا، ولكن أولاً، واجهة برمجة التطبيقات:

إدارة وحدة التحكّم والإشارة

التعرّف على AbortController وAbortSignal:

const controller = new AbortController();
const signal = controller.signal;

وحدة التحكُّم لديها طريقة واحدة فقط:

controller.abort();

وعند إجراء ذلك، سيتم إشعار الإشارة:

signal.addEventListener('abort', () => {
    // Logs true:
    console.log(signal.aborted);
});

يتم توفير واجهة برمجة التطبيقات هذه من خلال معيار DOM، وهي تمثّل واجهة برمجة التطبيقات بالكامل. من المهم عامة عمدًا بحيث يمكن استخدامها بواسطة معايير الويب ومكتبات 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;
});

إليك عرض توضيحي – في وقت كتابة هذه المقالة، كانت المتصفحات الوحيدة التي على إصدار 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. داخل مشغّل الخدمات، ستشير السمة 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 })
    );
    }
});

اتبع المواصفات لتتبع ذلك – سأضيف روابط إلى تذاكر المتصفّح عندما تصبح جاهزة للتنفيذ.

التاريخ

نعم... استغرق دمج واجهة برمجة التطبيقات البسيطة نسبيًا وقتًا طويلاً. وفي ما يلي السبب في ذلك:

عدم اتفاق مع واجهة برمجة التطبيقات

كما ترى، مناقشة GitHub طويلة جدًا. هناك الكثير من الفروق الدقيقة في هذا الموضوع (وبعض الفروق الدقيقة)، لكن الخلاف الرئيسي واحد وجود الطريقة abort في الكائن الذي تم عرضه من خلال fetch()، في حين أن الإجراء الآخر الفصل بين الحصول على الاستجابة والتأثير في الاستجابة.

هذه المتطلبات غير متوافقة، لذلك لن تحصل مجموعة واحدة على ما يريدون. إذا كان أنت، عذرًا! إذا شعرت بحالة أفضل، فأنا كنت أيضًا في تلك المجموعة. لكن رؤية AbortSignal يتناسب مع لواجهات برمجة التطبيقات الأخرى يجعل الأمر يبدو وكأنه الخيار الصحيح. كما أن السماح بالوعود المتسلسلة عملية يمكن إجهاضها تصبح معقدة للغاية، إن لم تكن مستحيلة.

إذا كنت تريد إرجاع كائن يوفر استجابة، ولكن يمكنه أيضًا إلغاء العملية، فيمكنك إنشاء برنامج تضمين بسيط:

function abortableFetch(request, opts) {
    const controller = new AbortController();
    const signal = controller.signal;

    return {
    abort: () => controller.abort(),
    ready: fetch(request, { ...opts, signal })
    };
}

يبدأ الخطأ في 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. كل ما نحتاج إليه من JavaScript كان متوفرًا من قبل، لذلك حددنا ضمن منصة الويب، وتحديدًا معيار DOM. بمجرد أن نتخذ هذا القرار، تم تجميع باقي العناصر بسرعة نسبيًا.

تغيير كبير في المواصفات

تم استبعاد XMLHttpRequest منذ سنوات، ولكن مواصفاته كانت غامضة للغاية. لم يكن واضحًا في النقاط التي يمكن تجنبها أو إنهاء نشاط الشبكة الأساسي أو ما حدث إذا كانت هناك حالة سباق بين استدعاء الدالة abort() واكتمال عملية الجلب.

أردنا الحصول على ذلك بشكل صحيح هذه المرة، ولكن أدى ذلك إلى تغيير كبير في المواصفات احتاج إلى الكثير من المراجعة (هذا خطأي، وبفضل جزيل الشكر إلى آن فان كيسترين) Domenic Denicola لسحبني من خلالها) ومجموعة جيدة من الاختبارات.

لكننا هنا الآن! لدينا قاعدة أساسية جديدة على الويب لإلغاء الإجراءات غير المتزامنة، ويمكن أن تؤدي عمليات الجلب المتعددة التحكم في البيانات دفعة واحدة! في وقت لاحق، سنلقي نظرة على تفعيل التغييرات ذات الأولوية طوال مدة الجلب، وتفعيل مستوى أعلى. واجهة برمجة التطبيقات لتتبّع تقدُّم عملية الجلب.