İptal edilebilir getirme

Jake Archibald
Jake Archibald

"Getirme işlemini iptal etme" ile ilgili orijinal GitHub sorunu 2015'te açıldı. Şimdi 2017'den (mevcut yıl) 2015'i çıkardığımızda 2 elde ederiz. Bu, matematikte bir hata olduğunu gösterir. Çünkü 2015 aslında "çok uzun zaman önce"dir.

Devam eden getirme işlemlerini iptal etmeyi ilk kez 2015'te keşfetmeye başladık. 780 GitHub yorumu, birkaç yanlış başlangıç ve 5 çekme isteği sonrasında nihayet tarayıcılarda iptal edilebilir getirme özelliğini kullanıma sunduk. Bu özelliği ilk kullanan tarayıcı Firefox 57 oldu.

Güncelleme: Yanlıştı. Edge 16, iptal desteğiyle ilk kez kullanıma sunuldu. Edge ekibini tebrik ederiz.

Geçmişe daha sonra değineceğiz. Öncelikle API'den bahsedelim:

Denetleyici + sinyal manevrası

AbortController ve AbortSignal ile tanışın:

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

Denetleyicinin yalnızca bir yöntemi vardır:

controller.abort();

Bunu yaptığınızda sinyal aşağıdakileri bildirir:

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

Bu API, DOM standardı tarafından sağlanır ve API'nin tamamı budur. Diğer web standartları ve JavaScript kitaplıkları tarafından kullanılabilmesi için kasıtlı olarak geneldir.

Sinyalleri iptal etme ve getirme

Getirme işlemi AbortSignal sürebilir. Örneğin, 5 saniye sonra getirme zaman aşımı oluşturmak için aşağıdakileri yapmanız gerekir:

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. Bu nedenle, yanıt gövdesinin (response.text() gibi) okunması da iptal edilir.

Demoyu buradan inceleyebilirsiniz. Bu özelliği şu anda yalnızca Firefox 57 desteklemektedir. Ayrıca, demonun oluşturulmasında tasarım konusunda herhangi bir yetkinliğe sahip kimse yer almadı.

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

Bu, request.signal bir AbortSignal olduğu için işe yarar.

Kesilen getirme işlemine tepki verme

Bir asynkron 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ının istediği işlemi başarıyla gerçekleştirdiğinizde "hata" olmadığından, kullanıcı işlemi iptal ederse genellikle hata mesajı göstermek istemezsiniz. Bunu önlemek için, yukarıdaki gibi bir if-statement kullanarak kesinti hatalarını özel olarak ele alın.

Kullanıcıya içerik yükleme ve işlemi iptal etme düğmesi sunan bir örneği aşağıda bulabilirsiniz. Getirme işleminde hata oluşursa (kesme hatası hariç) 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;
});

Demoyu buradan inceleyebilirsiniz. Bu özelliği şu anda yalnızca Edge 16 ve Firefox 57 desteklemektedir.

Tek sinyal, çok sayıda 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 getirme işlemleri için aynı sinyal kullanılır. fetchStory simgesini şu şekilde kullanabilirsiniz:

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

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

Bu durumda, controller.abort() çağrısı, devam eden tüm getirme işlemlerini iptal eder.

Gelecek

Diğer tarayıcılar

Edge bu özelliği ilk kullanıma sunarak harika bir iş çıkardı ve Firefox da hemen ardından onları takip etti. Mühendisleri, spesifikasyon yazılırken test paketinden uygulama yaptı. Diğer tarayıcılar için takip edilmesi gereken destek kaydı numaraları:

Hizmet çalışanında

Hizmet çalışanı bölümlerinin spesifikasyonunu tamamlamam gerekiyor. Planımız şu şekilde:

Daha önce de belirttiğim gibi, her Request nesnesinin bir signal özelliği vardır. Bir hizmet çalışanı içinde, sayfa artık yanıtla ilgilenmiyorsa fetchEvent.request.signal iptal sinyali gönderir. Sonuç olarak, aşağıdaki gibi kodlar sorunsuz çalışır:

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

Sayfa getirme işlemini iptal ederse fetchEvent.request.signal iptal sinyali gönderir. Böylece hizmet çalışanındaki getirme işlemi de iptal edilir.

event.request dışında bir öğe getiriyorsanız sinyali özel getirme işlemlerinize 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 izlemek için özelliği takip edin. Uygulamaya hazır olduğunda tarayıcı biletlerine bağlantılar ekleyeceğim.

Geçmiş

Evet, bu nispeten basit API'nin oluşturulması uzun sürdü. Bunun nedenleri aşağıda açıklanmıştır:

API uyuşmazlığı

Gördüğünüz gibi, GitHub tartışması oldukça uzun. Bu ileti dizisinde birçok ayrıntı (ve bazı ayrıntı eksiklikleri) var ancak temel anlaşmazlık, bir grubun abort yönteminin fetch() tarafından döndürülen nesnede bulunmasını istemesi, diğer grubun ise yanıtı alma ile yanıtı etkileme arasında bir ayrım olmasını istemesidir.

Bu şartlar uyumlu olmadığından bir grup istediği şeyi elde edemeyecekti. Bu sizseniz özür dileriz. Sizi rahatlatacaksa ben de bu gruptaydım. Ancak AbortSignal'ün diğer API'lerin koşullarına uyduğunu görmek, doğru seçim olduğunu gösteriyor. Ayrıca, zincirlenmiş vaatlerin iptal edilebilir hale gelmesi imkansız olmasa da çok karmaşık olurdu.

Yanıt veren ancak iptal de edebilecek bir nesne döndürmek istiyorsanı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şlangıçlar

İptal edilen bir işlemin hatadan farklı olması sağlandı. Bu değişiklikler arasında "iptal edildi" durumunu belirten üçüncü bir promise durumu ve hem senkronize hem de asynkron kodda iptal işlemini gerçekleştirmek için bazı yeni söz dizimi yer aldı:

Yapılmaması gerekenler:

Gerçek kod değil: 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 yapılması gereken en yaygın şey hiçbir şey yapmamaktır. Yukarıdaki öneri, iptal işlemini hatalardan ayırdı. Böylece, iptal hatalarını özel olarak ele almanız gerekmedi. catch cancel, iptal edilen işlemler hakkında sizi bilgilendirir ancak çoğu durumda bu gerekmez.

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

Alternatif önerimiz olan AbortController için yeni söz dizimi gerekmediğinden TC39'da bu önerinin spesifikasyonunu oluşturmak anlamlı değildi. JavaScript'ten ihtiyaç duyduğumuz her şey zaten mevcuttu. Bu nedenle, web platformundaki arayüzleri, özellikle de DOM standardını tanımladık. Bu kararı verdikten sonra geri kalanı nispeten hızlı bir şekilde tamamladık.

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

XMLHttpRequest yıllardır iptal edilebilir durumda ancak spesifikasyon oldukça belirsizdi. Temel ağ etkinliğinin hangi noktalarda önlenebileceği veya sonlandırılabileceği ya da abort() çağrılması ile getirme işleminin tamamlanması arasında bir yarış koşulu varsa ne olduğu net değildi.

Bu sefer doğru olanı yapmak istedik ancak bu, çok fazla inceleme gerektiren (bu benim hatam. Beni bu süreçten geçiren Anne van Kesteren ve Domenic Denicola'ya çok teşekkürler) ve bir dizi test gerektiren büyük bir özellik değişikliğiyle sonuçlandı.

Ancak şimdi buradayız. Asenkron işlemleri iptal etmek için yeni bir web ilkelimiz var ve birden fazla getirme işlemi aynı anda kontrol edilebilir. Gelecekte, getirme işleminin ömrü boyunca öncelik değişikliklerini ve getirme işleminin ilerleme durumunu gözlemlemek için daha üst düzey bir API'yi etkinleştirmeyi planlıyoruz.