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:
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.