Vấn đề ban đầu trên GitHub về việc "Huỷ tìm nạp" là khai trương vào năm 2015. Bây giờ, nếu tôi lấy 2015 đi từ năm 2017 (năm hiện tại), tôi nhận được 2. Điều này thể hiện lỗi trong toán học, vì năm 2015 thực ra là "mãi mãi" trước.
Năm 2015 là thời điểm chúng tôi lần đầu tiên bắt đầu khám phá việc huỷ các lần tìm nạp đang diễn ra và sau 780 nhận xét trên GitHub, một vài lần khởi động sai và 5 yêu cầu kéo, cuối cùng chúng ta đã có trang đích tìm nạp có thể huỷ trong trình duyệt, đầu tiên là Firefox 57.
Tin cập nhật: Ôi, tôi đã nhầm. Edge 16 đã hạ cánh với tính năng hỗ trợ huỷ trước tiên! Xin chúc mừng Đội ngũ lợi hại!
Tôi sẽ tìm hiểu sâu hơn về lịch sử sau, nhưng trước tiên là API:
Bộ điều khiển + hoạt động điều khiển tín hiệu
Xin giới thiệu AbortController
và AbortSignal
:
const controller = new AbortController();
const signal = controller.signal;
Bộ điều khiển chỉ có một phương thức:
controller.abort();
Khi bạn làm như vậy, Trợ lý sẽ thông báo tín hiệu:
signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});
API này do tiêu chuẩn DOM cung cấp và đó là toàn bộ API. Bây giờ cố ý chung chung để có thể dùng cho các tiêu chuẩn web và thư viện JavaScript khác.
Huỷ bỏ các tín hiệu và tìm nạp
Quá trình tìm nạp có thể mất AbortSignal
. Ví dụ: dưới đây là cách bạn sẽ tạo thời gian chờ tìm nạp sau 5
giây:
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);
});
Khi bạn huỷ tìm nạp, thao tác này sẽ huỷ cả yêu cầu và phản hồi, do đó mọi lần đọc nội dung phản hồi
(chẳng hạn như response.text()
) cũng bị huỷ.
Đây là bản minh hoạ – Tại thời điểm viết bài, trình duyệt duy nhất hỗ trợ trình duyệt này là Firefox 57. Ngoài ra, hãy chuẩn bị sẵn sàng, không ai có bất kỳ kỹ năng thiết kế nào tham gia khi tạo bản minh hoạ.
Ngoài ra, tín hiệu có thể được cấp cho một đối tượng yêu cầu, sau đó được chuyển để tìm nạp:
const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });
fetch(request);
Phương thức này hiệu quả vì request.signal
là một AbortSignal
.
Phản ứng với quá trình tìm nạp bị huỷ
Khi bạn huỷ một hoạt động không đồng bộ, lời hứa sẽ từ chối bằng DOMException
có tên là 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);
}
});
Bạn thường không muốn hiển thị thông báo lỗi nếu người dùng huỷ thao tác, vì đó không phải là "lỗi" nếu bạn làm thành công những gì người dùng yêu cầu. Để tránh điều này, hãy sử dụng câu lệnh if như ở trên để xử lý cụ thể lỗi huỷ.
Dưới đây là ví dụ cung cấp cho người dùng một nút để tải nội dung và một nút để huỷ bỏ. Nếu phương thức tìm nạp lỗi sẽ xuất hiện, trừ phi đó là lỗi huỷ bỏ:
// 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;
});
Đây là bản minh hoạ – Tại thời điểm viết bài, trình duyệt duy nhất hỗ trợ này là Edge 16 và Firefox 57.
Một tín hiệu, nhiều lần tìm nạp
Bạn có thể dùng một tín hiệu để huỷ nhiều lần tìm nạp cùng một lúc:
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);
}
Trong ví dụ trên, cùng một tín hiệu được sử dụng cho lần tìm nạp ban đầu và cho chương song song
tìm nạp. Dưới đây là cách bạn sẽ sử dụng fetchStory
:
const controller = new AbortController();
const signal = controller.signal;
fetchStory({ signal }).then(chapters => {
console.log(chapters);
});
Trong trường hợp này, việc gọi controller.abort()
sẽ huỷ mọi lần tìm nạp đang diễn ra.
Tương lai
Các trình duyệt khác
Edge đã làm rất tốt trong việc đưa sản phẩm này đầu tiên và Firefox rất thu hút họ. Các kỹ sư của công ty được triển khai từ bộ kiểm thử trong khi thông số kỹ thuật được đang được viết. Đối với các trình duyệt khác, bạn nên làm theo các bước sau:
Trong một trình chạy dịch vụ
Tôi cần hoàn thành thông số kỹ thuật cho các bộ phận của worker bảo dưỡng, nhưng sau đây là kế hoạch:
Như tôi đã đề cập trước đó, mỗi đối tượng Request
đều có một thuộc tính signal
. Trong một trình chạy dịch vụ,
fetchEvent.request.signal
sẽ báo hiệu huỷ nếu trang không còn quan tâm đến phản hồi đó.
Do đó, mã như thế này chỉ hoạt động:
addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
Nếu trang huỷ tìm nạp, fetchEvent.request.signal
sẽ báo hiệu sẽ huỷ, do đó, quá trình tìm nạp trong
Service worker cũng huỷ.
Nếu đang tìm nạp dữ liệu nào đó không phải là event.request
, bạn cần truyền tín hiệu đến
(các) lần tìm nạp tuỳ chỉnh.
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 })
);
}
});
Làm theo thông số kỹ thuật để theo dõi điều này – Tôi sẽ thêm liên kết tới khi sẵn sàng để triển khai.
Lịch sử
Đúng vậy... mất nhiều thời gian để sử dụng API tương đối đơn giản này. Dưới đây là lý do:
Không đồng ý về API
Như bạn có thể thấy, cuộc thảo luận trên GitHub khá dài.
Có rất nhiều sắc thái trong chủ đề đó (và một số điểm thiếu sắc thái), nhưng bất đồng chính là một
nhóm muốn phương thức abort
tồn tại trên đối tượng được fetch()
trả về, trong khi phương thức khác
muốn tách biệt giữa việc nhận phản hồi và tác động đến phản hồi.
Các yêu cầu này không tương thích với nhau, vì vậy, một nhóm không có được những gì họ muốn. Nếu trường hợp đó
rất tiếc! Nếu bạn thấy thoải mái hơn, thì tôi cũng thuộc nhóm đó. Nhưng thấy AbortSignal
phù hợp với
yêu cầu của các API khác khiến đây có vẻ là lựa chọn phù hợp. Ngoài ra, việc cho phép hứa hẹn theo chuỗi
có thể huỷ bỏ được sẽ trở nên rất phức tạp, nếu không muốn nói là không thể.
Nếu muốn trả về một đối tượng cung cấp phản hồi, nhưng cũng có thể huỷ, bạn có thể tạo một trình bao bọc đơn giản:
function abortableFetch(request, opts) {
const controller = new AbortController();
const signal = controller.signal;
return {
abort: () => controller.abort(),
ready: fetch(request, { ...opts, signal })
};
}
Bắt đầu báo cáo False trong TC39
Đã có một nỗ lực để làm cho tác vụ đã huỷ khác biệt với lỗi. Điều này bao gồm lời hứa thứ ba trạng thái để biểu thị "đã hủy" và một số cú pháp mới để xử lý việc hủy trong cả đồng bộ hóa và không đồng bộ mã:
Không phải mã thực – đề xuất đã bị rút lại
try { // Start spinner, then: await someAction(); } catch cancel (reason) { // Maybe do nothing? } catch (err) { // Show error message } finally { // Stop spinner }
Trường hợp phổ biến nhất cần làm khi một tác vụ bị huỷ là không có gì. Đề xuất ở trên đã tách riêng
để huỷ bỏ lỗi, nhờ đó bạn không cần phải xử lý cụ thể lỗi huỷ. catch cancel
cho phép
bạn nghe về các hành động bị huỷ, nhưng hầu hết thời gian bạn không cần làm vậy.
Đề xuất này đã chuyển sang giai đoạn 1 trong TC39, nhưng không đạt được sự đồng thuận và đề xuất đã bị rút lại.
Đề xuất thay thế của chúng tôi, AbortController
, không yêu cầu bất kỳ cú pháp mới nào, vì vậy nó không hợp lý
để chỉ định mã đó trong TC39. Tất cả những gì chúng tôi cần từ JavaScript đã có sẵn ở đó, vì vậy chúng tôi đã xác định
trong nền tảng web, cụ thể là tiêu chuẩn DOM. Sau khi đưa ra quyết định đó,
phần còn lại được kết hợp tương đối nhanh.
Thay đổi lớn về thông số kỹ thuật
XMLHttpRequest
đã bị huỷ bỏ trong nhiều năm, nhưng thông số kỹ thuật còn khá mơ hồ. Trời không rõ ràng vào lúc
điểm nào có thể tránh hoặc chấm dứt hoạt động cơ bản trên mạng hoặc điều gì sẽ xảy ra nếu
đã có một tình huống tương tranh giữa thời điểm gọi abort()
và quá trình tìm nạp hoàn tất.
Chúng tôi muốn thực hiện ngay lần này, nhưng điều đó đã dẫn đến một sự thay đổi lớn về thông số kỹ thuật và cần rất nhiều bài đánh giá (đó là lỗi của tôi và chân thành cảm ơn Anne van Kesteren và Domenic Denicola vì đã khuyến khích tôi khám phá) và một loạt các bài kiểm tra ưng ý.
Nhưng chúng tôi đang ở đây! Chúng tôi có một phương thức gốc mới trên web để huỷ các hành động không đồng bộ và có thể tìm nạp nhiều lần được kiểm soát cùng một lúc! Về sau, chúng ta sẽ xem xét việc bật các thay đổi về mức độ ưu tiên trong suốt thời gian tìm nạp và cấp cao hơn API để quan sát tiến trình tìm nạp.