Masalah GitHub asli untuk "Membatalkan pengambilan" dibuka pada tahun 2015. Sekarang, jika saya mengurangi 2015 dari 2017 (tahun saat ini), saya mendapatkan 2. Hal ini menunjukkan bug dalam matematika, karena 2015 sebenarnya sudah "lama sekali".
Tahun 2015 adalah saat kami pertama kali mulai mempelajari pembatalan pengambilan yang sedang berlangsung, dan setelah 780 komentar GitHub, beberapa awal yang salah, dan 5 permintaan pull, kami akhirnya memiliki halaman pengambilan yang dapat dibatalkan di browser, dengan yang pertama adalah Firefox 57.
Pembaruan: Tidak, saya salah. Edge 16 hadir dengan dukungan pembatalan terlebih dahulu. Selamat kepada tim Edge!
Saya akan membahas historinya nanti, tetapi pertama-tama, API:
Manuver pengontrol + sinyal
Kenali AbortController
dan AbortSignal
:
const controller = new AbortController();
const signal = controller.signal;
Pengontrol hanya memiliki satu metode:
controller.abort();
Jika Anda melakukannya, sinyal akan diberi tahu:
signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});
API ini disediakan oleh standar DOM, dan itulah seluruh API. Library ini sengaja bersifat umum sehingga dapat digunakan oleh standar web dan library JavaScript lainnya.
Membatalkan sinyal dan pengambilan
Pengambilan dapat mengambil AbortSignal
. Misalnya, berikut 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 akan dibatalkan.
Berikut demonya – Pada saat penulisan, satu-satunya browser yang mendukungnya adalah Firefox 57. Selain itu, bersiaplah, tidak ada orang dengan keterampilan desain apa pun yang terlibat dalam pembuatan 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);
Hal 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 error jika pengguna membatalkan operasi, karena hal ini bukan "error" jika Anda berhasil melakukan apa yang diminta pengguna. Untuk menghindari hal ini, gunakan pernyataan if seperti yang di atas untuk menangani error 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 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, 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 pengambilan
bagian paralel. 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 yang sedang berlangsung.
Acara mendatang
Browser lainnya
Edge berhasil meluncurkannya lebih dulu, dan Firefox tidak mau kalah. Engineer mereka menerapkan dari test suite saat spesifikasi ditulis. Untuk browser lain, berikut tiket yang harus diikuti:
Di pekerja layanan
Saya perlu menyelesaikan spesifikasi untuk bagian 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 pembatalan jika halaman tidak lagi tertarik dengan respons.
Akibatnya, kode seperti ini akan berfungsi:
addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
Jika halaman membatalkan pengambilan, fetchEvent.request.signal
akan memberikan sinyal pembatalan, sehingga pengambilan dalam pekerja layanan juga akan 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 melacaknya. Saya akan menambahkan link ke tiket browser setelah siap diterapkan.
Histori
Ya… perlu waktu lama untuk membuat API yang relatif sederhana ini. Berikut ini alasannya:
Ketidaksesuaian API
Seperti yang dapat Anda lihat, diskusi GitHub cukup panjang.
Ada banyak nuansa dalam rangkaian pesan tersebut (dan beberapa nuansa yang tidak ada), tetapi perbedaan pendapat utamanya adalah satu
grup ingin metode abort
ada di objek yang ditampilkan oleh fetch()
, sedangkan grup lainnya
ingin pemisahan antara mendapatkan respons dan memengaruhi respons.
Persyaratan ini tidak kompatibel, sehingga satu grup tidak akan mendapatkan apa yang mereka inginkan. Jika itu
Anda, mohon maaf. Jika itu membuat Anda merasa lebih baik, saya juga berada di grup tersebut. Namun, melihat AbortSignal
sesuai dengan
persyaratan API lain, AbortSignal
tampak seperti pilihan yang tepat. Selain itu, mengizinkan promise berantai untuk
dapat dibatalkan akan menjadi sangat rumit, bahkan tidak mungkin.
Jika ingin menampilkan objek yang memberikan respons, tetapi juga dapat dibatalkan, 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 })
};
}
False starts di TC39
Ada upaya untuk membuat tindakan yang dibatalkan berbeda dari error. Hal ini mencakup status promise ketiga untuk menunjukkan "dibatalkan", dan beberapa sintaksis baru untuk menangani pembatalan dalam kode sinkron dan asinkron:
Bukan kode sungguhan — proposal telah ditarik
try { // Start spinner, then: await someAction(); } catch cancel (reason) { // Maybe do nothing? } catch (err) { // Show error message } finally { // Stop spinner }
Tindakan yang paling umum dilakukan saat tindakan dibatalkan adalah tidak melakukan apa pun. 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 perlu melakukannya.
Proposal ini mencapai tahap 1 di TC39, tetapi tidak mencapai konsensus, dan proposal ditarik.
Proposal alternatif kami, AbortController
, tidak memerlukan sintaksis baru, sehingga tidak masuk akal
untuk menentukannya dalam TC39. Semua yang kita butuhkan dari JavaScript sudah ada, jadi kita menentukan
antarmuka dalam platform web, khususnya standar DOM. Setelah kami membuat keputusan tersebut,
hal lainnya berjalan relatif cepat.
Perubahan spesifikasi yang besar
XMLHttpRequest
telah dapat dibatalkan selama bertahun-tahun, tetapi spesifikasinya cukup samar. Tidak jelas pada
titik mana aktivitas jaringan yang mendasarinya dapat dihindari, atau dihentikan, atau apa yang terjadi jika
ada kondisi perlombaan antara abort()
yang dipanggil dan pengambilan selesai.
Kami ingin melakukannya dengan benar kali ini, tetapi hal itu mengakibatkan perubahan spesifikasi besar yang memerlukan banyak peninjauan (itu adalah kesalahan saya, dan terima kasih banyak kepada Anne van Kesteren dan Domenic Denicola karena telah membantu saya mengatasinya) dan serangkaian pengujian yang memadai.
Namun, kita sudah sampai. Kami 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 API tingkat yang lebih tinggi untuk mengamati progres pengambilan.