İptal edilebilir getirme

Jake Archibald
Jake Archibald

"Getirme iptali" için orijinal GitHub sorunu 2015'te açılmıştı. 2015'i 2017'den (bu yıl) alırsam 2 alırım. Bu, matematikteki bir hatayı gösteriyor çünkü 2015 aslında "daima" önceydi.

2015'te devam eden getirme işlemlerini iptal etmeyi ilk kez keşfetmeye başladık. 780 GitHub yorumu, birkaç yanlış başlatma ve 5 pull isteğinden sonra nihayet tarayıcılarda iptal edilebilir getirme açılışı (ilki Firefox 57 olacak şekilde) oldu.

Güncelleme: Hayır, yanıldım. Edge 16 önce iptal desteği aldı. Edge ekibini tebrik ederiz.

Geçmişe daha sonra değineceğim, ancak öncelikle API:

Kumanda + sinyal hareketi

AbortController ve AbortSignal ile tanışın:

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

Kumandanın yalnızca bir yöntemi vardır:

controller.abort();

Bunu yaptığınızda sinyal şu şekilde bildirilir:

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

Bu API, DOM standardı tarafından sağlanır ve API'nin tamamı budur. Bilinçli olarak genel olduğundan diğer web standartları ve JavaScript kitaplıkları tarafından kullanılabilir.

Sinyalleri iptal et ve getir

Getirme işlemi AbortSignal alabilir. Örneğin, 5 saniye sonra getirme zaman aşımını şu şekilde yapabilirsiniz:

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);
});

Bir getirme işlemini iptal ettiğinizde hem istek hem de yanıt iptal edilir. Dolayısıyla, yanıt gövdesinin (response.text() gibi) okunması da iptal edilir.

Bu demoyu burada bulabilirsiniz – Yazılandığı sırada bunu destekleyen tek tarayıcı Firefox 57'dir. Ayrıca, kendinizi hazır hissedin. Demo oluştururken tasarım becerisine sahip hiç kimse yer almamıştı.

Alternatif olarak, sinyal bir istek nesnesine verilebilir ve daha sonra, getirme işlemine iletilebilir:

const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });

fetch(request);

request.signal bir AbortSignal olduğundan bu işlev görür.

İptal edilen getirme işlemine tepki verme

Eş zamansız bir işlemi iptal ettiğinizde söz, AbortError adlı bir DOMException ile reddedilir:

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);
    }
});

Kullanıcı işlemi iptal ettiyse genellikle hata mesajı göstermek istemezsiniz çünkü kullanıcının istediğini başarıyla gerçekleştirdiyseniz bu bir "hata" değildir. Bunu önlemek için özellikle iptal hatalarını ele almak üzere yukarıdaki gibi bir if ifadesi kullanın.

Aşağıda, kullanıcıya içeriği yüklemek için ve iptal etmesi için düğme sağlayan bir örnek verilmiştir. Getirme hataları varsa bir iptal hatası olmadığı sürece bir hata gösterilir:

// 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;
});

Bu demoyu burada bulabilirsiniz – Yazılırken yalnızca Edge 16 ve Firefox 57 destekleniyor.

Bir sinyal, birçok getirme

Tek bir sinyal, birden fazla getirme işlemini aynı anda iptal etmek için kullanılabilir:

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);
}

Yukarıdaki örnekte ilk getirme ve paralel bölüm getirmeleri için aynı sinyal kullanılır. fetchStory ürününü şu şekilde kullanmalısınız:

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

fetchStory({ signal }).then(chapters => {
    console.log(chapters);
});

Bu durumda, controller.abort() çağrıldığında devam eden getirmeler iptal edilir.

Gelecek

Diğer tarayıcılar

Edge bu ürünü ilk olarak gönderme konusunda harika bir iş çıkardı ve Firefox da o yolda epeyce yaklaştı. Mühendisleri, spesifikasyon yazıldığı sırada test paketinden uygulamaları uyguladı. Diğer tarayıcılar için takip edilecek biletler şunlardır:

Service Worker'da

Servis çalışanı parçaları spesifikasyonunu bitirmem gerekiyor, ama planı şu şekilde:

Daha önce bahsettiğim gibi her Request nesnesi, bir signal özelliğine sahiptir. Service Worker'da sayfa artık yanıtla ilgilenmiyorsa fetchEvent.request.signal, iptal sinyalini verir. Sonuç olarak, aşağıdaki gibi bir kod çalışır:

addEventListener('fetch', event => {
    event.respondWith(fetch(event.request));
});

Sayfa, getirme işlemini iptal ederse fetchEvent.request.signal sinyali de iptal eder. Bu nedenle, hizmet çalışanı içindeki getirme işlemi de iptal edilir.

event.request dışında bir veri getiriyorsanız sinyali özel getirmelerinize iletmeniz gerekir.

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 })
    );
    }
});

Bunu takip etmek için Spesifikasyona uyun. Uygulama için hazır olduğunda tarayıcı biletlerinin bağlantılarını ekleyeceğim.

Tarih

Evet... Bu nispeten basit API'nin bir araya gelmesi uzun zaman aldı. Bunun nedenleri aşağıda açıklanmıştır:

API uyuşmazlığı

Gördüğünüz gibi GitHub tartışması oldukça uzun. Bu iş parçacığında çok fazla nüans var (ve küçük bir ayrıntı eksikliği var) ancak temel anlaşmazlık, bir grup abort yönteminin fetch() tarafından döndürülen nesnede var olmasını, diğeri ise yanıtı alma ile yanıtı etkileme arasında ayrım yapılmasını istedi.

Bu koşullar uyumsuz olduğundan bir grup, istediğini elde edemedi. Bunu siz de bildiyseniz üzgünüm! Daha iyi hissetmene yardımcı oluyorsa ben de bu grupta yer alıyordum. Ancak AbortSignal özelliğinin diğer API'lerin gereksinimlerini karşıladığını görmek doğru seçim gibi görünüyor. Ayrıca zincirleme vaatlerin iptal edilebilir hale gelmesine izin vermek imkansız olmasa da çok karmaşık bir hale gelirdi.

Yanıt sağlayan bir nesne döndürmek isterseniz ancak işlemi iptal edebiliyorsanız basit bir sarmalayıcı oluşturabilirsiniz:

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

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

TC39'da yanlış başlatma sayısı

İptal edilen bir işlemi bir hatadan farklı kılmak için çaba harcandı. Bu, "iptal edildi"yi belirtmek için üçüncü bir vaat durumu ve hem senkronizasyon hem de eşzamansız kodda iptali işlemek için bazı yeni söz dizimi içeriyordu:

Yapılmaması gerekenler

Gerçek olmayan kod - teklif geri çekildi

    try {
      // Start spinner, then:
      await someAction();
    }
    catch cancel (reason) {
      // Maybe do nothing?
    }
    catch (err) {
      // Show error message
    }
    finally {
      // Stop spinner
    }

Bir işlem iptal edildiğinde en yaygın olarak yapılan şey hiçbir şeydir. Yukarıdaki teklif iptal hatalarını hatalardan ayırdığından iptal hatalarını özel olarak ele almanız gerekmedi. catch cancel, iptal edilen işlemler hakkında size bilgi verir, ancak çoğu zaman bunu yapmanıza gerek yoktur.

Bu, TC39'da 1. aşamaya ulaştı ancak fikir birliği sağlanamadı ve teklif geri çekildi.

Alternatif önerimiz olan AbortController, yeni bir söz dizimi gerektirmediğinden bu özelliğin TC39 içinde belirtilmesi mantıklı olmadı. JavaScript'ten ihtiyacımız olan her şey zaten vardı. Bu yüzden, web platformundaki arayüzleri, özellikle de DOM standardını tanımladık. Biz bu kararı verdikten sonra gerisi nispeten kısa sürede bir araya geliyordu.

Büyük özellik değişikliği

XMLHttpRequest yıllardır iptal edilebiliyor olsa da teknik özellikleri oldukça belirsizdi. Temel ağ etkinliğinden hangi noktalarda kaçınılabileceği veya sonlandırılabileceği ya da abort() çağrılması ile getirme işleminin tamamlanması arasında bir yarış koşulu olması halinde ne olduğu net değildi.

Bu sefer doğru ulaşmak istedik ancak bu durum çok fazla inceleme gerektiren büyük bir teknik özellik değişikliğiyle sonuçlandı (bu benim hatam oldu. Beni bu konuda sürükledikleri için Anne van Kesteren ve Domenic Denicola'ya çok teşekkür ederim) ve birçok iyi test yaptık.

Ama şimdi buradayız! Eş zamansız işlemleri iptal etmek için kullanabileceğiniz yeni bir web temel öğesi hazırladık. Aynı anda birden fazla getirme işlemi kontrol edilebilir. Bir sonraki adımda, getirme işleminin süresi boyunca öncelikli değişiklikleri etkinleştirmeye ve getirme ilerlemesini gözlemlemek için daha üst düzey bir API'yi ele alacağız.