Pengambilan yang dapat dibatalkan

Jake Archibald
Jake Archibald

Masalah GitHub asli untuk "Membatalkan pengambilan" dibuka pada tahun 2015. Sekarang, jika saya mengambil tahun 2015 dari 2017 (tahun ini), saya mendapatkan 2. Hal ini menunjukkan adanya bug dalam matematika, karena tahun 2015 sebenarnya sudah "selamanya".

Tahun 2015 adalah saat kami pertama kali mengeksplorasi pembatalan pengambilan yang sedang berlangsung, dan setelah 780 komentar GitHub, beberapa start yang salah, dan 5 permintaan pull, akhirnya kami memiliki pendaratan pengambilan yang dapat dibatalkan di browser, dengan yang pertama adalah Firefox 57.

Pembaruan: Maaf, saya salah. Edge 16 mendarat dengan dukungan pembatalan terlebih dahulu! Selamat kepada tim Edge!

Saya akan membahasnya lebih mendalam nanti, tetapi sebelumnya, API:

Pengontrol + manuver sinyal

Memperkenalkan AbortController dan AbortSignal:

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

Pengontrol hanya memiliki satu metode:

controller.abort();

Jika Anda melakukannya, fitur ini akan memberi tahu sinyal:

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

API ini disediakan oleh standar DOM, dan merupakan keseluruhan API. Kode ini disengaja generik sehingga dapat digunakan oleh standar web dan library JavaScript lainnya.

Batalkan sinyal dan pengambilan

Pengambilan dapat memerlukan waktu AbortSignal. Misalnya, berikut ini cara Anda membuat waktu tunggu pengambilan setelah 5 detik:

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

Jika Anda membatalkan pengambilan, permintaan dan respons akan dibatalkan, sehingga setiap pembacaan isi respons (seperti response.text()) juga dibatalkan.

Berikut ini demonya – Pada saat penulisan ini, satu-satunya browser yang mendukungnya adalah Firefox 57. Selain itu, persiapkan diri Anda, tidak ada orang dengan keterampilan desain apa pun yang terlibat dalam membuat demo.

Atau, sinyal dapat diberikan ke objek permintaan dan kemudian diteruskan untuk mengambil:

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

fetch(request);

Cara ini berfungsi karena request.signal adalah AbortSignal.

Bereaksi terhadap pengambilan yang dibatalkan

Saat Anda membatalkan operasi asinkron, promise akan ditolak dengan DOMException bernama 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);
    }
});

Anda sebaiknya tidak ingin menampilkan pesan error jika pengguna membatalkan operasi, karena ini bukan "error" jika Anda berhasil melakukan apa yang diminta pengguna. Untuk menghindarinya, gunakan pernyataan if seperti di atas untuk menangani error pembatalan secara khusus.

Berikut adalah contoh yang memberi pengguna tombol untuk memuat konten, dan tombol untuk membatalkan. Jika terjadi error pengambilan, error akan ditampilkan, kecuali error tersebut adalah error pembatalan:

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

Berikut demonya – Pada saat penulisan ini, satu-satunya browser yang mendukungnya adalah Edge 16 dan Firefox 57.

Satu sinyal, banyak pengambilan

Satu sinyal dapat digunakan untuk membatalkan banyak pengambilan sekaligus:

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

Pada contoh di atas, sinyal yang sama digunakan untuk pengambilan awal, dan untuk pengambilan segmen paralel. Berikut ini cara menggunakan fetchStory:

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

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

Dalam hal ini, memanggil controller.abort() akan membatalkan pengambilan mana pun yang sedang berlangsung.

Acara mendatang

Browser lainnya

Edge melakukan pekerjaan yang bagus untuk mengirimkan ini terlebih dahulu, dan Firefox sedang dalam proses. Engineer mereka menerapkan dari rangkaian pengujian saat spesifikasi sedang ditulis. Untuk browser lain, berikut tiket yang harus diikuti:

Dalam pekerja layanan

Saya perlu menyelesaikan spesifikasi komponen service worker, tetapi berikut rencananya:

Seperti yang saya sebutkan sebelumnya, setiap objek Request memiliki properti signal. Dalam pekerja layanan, fetchEvent.request.signal akan memberi sinyal pembatalan jika halaman tidak lagi tertarik dengan respons. Akibatnya, kode seperti ini berfungsi:

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

Jika halaman membatalkan pengambilan, fetchEvent.request.signal akan menandakan bahwa pengambilan dalam pekerja layanan juga dibatalkan.

Jika mengambil sesuatu selain event.request, Anda harus meneruskan sinyal ke pengambilan kustom.

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

Ikuti spesifikasi untuk melacak hal ini – saya akan menambahkan link ke tiket browser setelah siap untuk diterapkan.

Sejarah

Ya... butuh waktu lama untuk menyatukan API yang relatif sederhana ini. Berikut ini alasannya:

Ketidaksepakatan API

Seperti yang Anda lihat, diskusi GitHub cukup panjang. Ada banyak nuansa dalam thread tersebut (dan beberapa kurang nuansa), tetapi ketidaksepakatan utamanya adalah satu kelompok menginginkan metode abort ada pada objek yang ditampilkan oleh fetch(), sedangkan kelompok yang lain menginginkan pemisahan antara mendapatkan respons dan memengaruhi respons.

Persyaratan ini tidak kompatibel, jadi satu kelompok tidak akan mendapatkan apa yang mereka inginkan. Jika itu Anda, mohon maaf! Jika Anda merasa lebih baik, saya juga berada dalam kelompok itu. Namun, melihat AbortSignal sesuai dengan persyaratan API lain membuatnya tampak seperti pilihan yang tepat. Selain itu, mengizinkan janji berantai untuk dibatalkan akan menjadi sangat rumit, bahkan mustahil.

Jika ingin menampilkan objek yang memberikan respons, tetapi juga dapat membatalkan, Anda dapat membuat wrapper sederhana:

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

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

Salah dimulai di TC39

Terjadi upaya untuk membuat tindakan yang dibatalkan berbeda dari error. Hal ini mencakup status promise ketiga untuk menandakan "cancelled", dan beberapa sintaksis baru untuk menangani pembatalan dalam kode sinkronisasi dan asinkron:

Larangan

Bukan kode asli — proposal dibatalkan

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

Hal paling umum yang harus dilakukan ketika suatu tindakan dibatalkan, adalah tidak ada. Proposal di atas memisahkan pembatalan dari error sehingga Anda tidak perlu menangani error pembatalan secara khusus. catch cancel memungkinkan Anda mengetahui tindakan yang dibatalkan, tetapi biasanya Anda tidak memerlukannya.

Proses ini mencapai tahap 1 di TC39, tetapi konsensus tidak tercapai, dan proposal dibatalkan.

Proposal alternatif kami, AbortController, tidak memerlukan sintaksis baru, sehingga tidak masuk akal untuk menentukannya dalam TC39. Semua yang kami butuhkan dari JavaScript sudah ada di sana, jadi kami menentukan antarmuka dalam platform web, khususnya standar DOM. Setelah kita membuat keputusan itu, lainnya datang cukup cepat.

Perubahan spesifikasi yang besar

XMLHttpRequest telah dibatalkan selama bertahun-tahun, tetapi spesifikasinya cukup tidak jelas. Tidak jelas kapan aktivitas jaringan dasar dapat dihindari, atau dihentikan, atau apa yang terjadi jika ada kondisi race antara abort() dipanggil dan penyelesaian pengambilan.

Kami ingin melakukannya dengan benar kali ini, tetapi hal itu mengakibatkan perubahan spesifikasi besar yang perlu banyak peninjauan (itu salah saya, dan terima kasih banyak kepada Anne van Kesteren dan Domenic Denicola karena telah membantu saya melewatinya) dan serangkaian pengujian yang memadai.

Tapi kita sudah di sini! Kami memiliki primitif web baru untuk membatalkan tindakan asinkron, dan beberapa pengambilan dapat dikontrol sekaligus. Selanjutnya, kita akan melihat pengaktifan perubahan prioritas selama proses pengambilan, dan API dengan tingkat yang lebih tinggi untuk mengamati progres pengambilan.