Pengambilan yang dapat dibatalkan

Jake Archibald
Jake Archibald

Masalah GitHub asli untuk "Membatalkan pengambilan" adalah dibuka pada tahun 2015. Sekarang, jika saya mengambil 2015 dari 2017 (tahun ini), saya mendapatkan 2. Hal ini menunjukkan {i>bug<i} dalam matematika, karena 2015 sebenarnya adalah "selamanya" yang lalu.

Tahun 2015 adalah saat kami pertama kali mulai mengeksplorasi pembatalan pengambilan yang sedang berlangsung, dan setelah 780 komentar GitHub, beberapa {i>false start<i}, dan 5 {i>pull request<i}, kita akhirnya memiliki halaman landing pengambilan yang dapat dibatalkan di browser, yang pertama adalah Firefox 57.

Pembaruan: Yah, saya salah. Edge 16 hadir dengan dukungan pembatalan terlebih dahulu. Selamat kepada Tim Edge!

Saya akan mempelajari historinya nanti, tetapi pertama-tama, API:

Pengontrol + manuver sinyal

Kenali AbortController dan AbortSignal:

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

Pengontrol hanya memiliki satu metode:

controller.abort();

Setelah Anda melakukannya, sinyal akan diberi tahu:

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

API ini disediakan oleh standar DOM, dan itulah API keseluruhan. Penting sengaja dibuat generik sehingga dapat digunakan oleh standar web dan library JavaScript lainnya.

Batalkan sinyal dan ambil

Pengambilan dapat memerlukan AbortSignal. Misalnya, berikut ini cara 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 juga akan dibatalkan, sehingga pembacaan isi respons (seperti response.text()) juga dibatalkan.

Berikut adalah demo – Pada saat penulisan ini, satu-satunya browser yang mendukungnya adalah Firefox 57. Juga, persiapkan diri Anda, tidak ada seorang pun yang terlibat dalam keterampilan desain membuat demo.

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

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

fetch(request);

Ini berfungsi karena request.signal adalah AbortSignal.

Merespons 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 tidak sering ingin menampilkan pesan {i>error<i} jika pengguna membatalkan operasi, karena hal itu "kesalahan" jika Anda berhasil melakukan apa yang diminta pengguna. Untuk menghindarinya, gunakan pernyataan if seperti penjelasan di atas untuk menangani kesalahan pembatalan secara khusus.

Berikut adalah contoh yang memberi pengguna tombol untuk memuat konten dan tombol untuk membatalkan. Jika pengambilan error, error akan ditampilkan, kecuali jika merupakan 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 adalah demo – Pada saat penulisan, satu-satunya browser yang mendukung ini 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 segmen paralel pengambilan. Berikut 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 telah bekerja dengan baik dalam meluncurkannya terlebih dahulu, dan Firefox sangat populer. Engineer mereka diimplementasikan dari rangkaian pengujian saat spesifikasinya sedang ditulis. Untuk browser lain, berikut tiket yang harus diikuti:

Di pekerja layanan

Saya perlu menyelesaikan spesifikasi untuk suku cadang pekerja layanan, tetapi berikut rencananya:

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

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

Jika halaman membatalkan pengambilan, sinyal fetchEvent.request.signal akan dibatalkan, sehingga pengambilan dalam pekerja layanan juga akan dibatalkan.

Jika Anda 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 ini. Saya akan menambahkan link ke tiket {i>browser<i} setelah siap untuk diterapkan.

Histori

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 perbedaan dalam utas tersebut (dan beberapa kurang bernuansa), tetapi perbedaan utamanya adalah satu ingin agar metode abort ada pada objek yang ditampilkan oleh fetch(), sedangkan menginginkan pemisahan antara mendapatkan respons dan mempengaruhi respons.

Persyaratan ini tidak kompatibel, sehingga satu kelompok tidak akan mendapatkan apa yang mereka inginkan. Jika itu Anda, maaf! Jika itu membuat Anda merasa lebih baik, saya juga ada di grup itu. Namun, melihat AbortSignal sesuai dengan API lain yang menjadikannya tampak seperti pilihan yang tepat. Juga, memungkinkan promise berantai untuk menjadi dapat dibatalkan akan menjadi sangat rumit, bahkan tidak mungkin.

Jika Anda ingin mengembalikan 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 start di TC39

Ada upaya untuk membuat tindakan yang dibatalkan yang berbeda dengan error. Termasuk di antaranya, status untuk menandakan "cancelled", dan beberapa sintaksis baru untuk menangani pembatalan baik dalam mode sinkron maupun asinkron kode:

Larangan

Bukan kode sebenarnya — proposal telah 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 saat tindakan dibatalkan, adalah tidak melakukan apa pun. Proposal di atas dipisahkan pembatalan dari pesan {i>error<i} sehingga Anda tidak perlu menangani kesalahan pembatalan secara khusus. catch cancel izinkan Anda mendengar tentang tindakan yang dibatalkan, tetapi sering kali Anda tidak memerlukannya.

Kebijakan 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 menetapkannya dalam TC39. Segala sesuatu yang kita butuhkan dari JavaScript sudah ada di sana, jadi kita mendefinisikan antarmuka dalam platform web, khususnya standar DOM. Setelah kami membuat keputusan itu, yang lainnya muncul dengan relatif cepat.

Perubahan spesifikasi besar

XMLHttpRequest telah dibatalkan selama bertahun-tahun, tetapi spesifikasinya tidak jelas. Tidak jelas pada pukul titik aktivitas jaringan yang mendasarinya dapat dihindari, dihentikan, atau apa yang terjadi jika ada kondisi race antara abort() yang dipanggil dan pengambilan selesai.

Kami ingin melakukannya dengan benar kali ini, tetapi itu menghasilkan perubahan spesifikasi besar yang membutuhkan banyak meninjau (itu salah saya, dan terima kasih banyak kepada Anne van Kesteren dan Domenic Denicola karena telah membawa saya melewatinya) dan serangkaian tes yang bagus.

Tapi, kita di sini sekarang! Kita memiliki primitif web baru untuk membatalkan tindakan asinkron, dan beberapa pengambilan dapat dikontrol sekaligus! Selanjutnya, kita akan melihat cara mengaktifkan perubahan prioritas selama proses pengambilan, dan konfigurasi level yang lebih tinggi API untuk mengamati progres pengambilan.