「取消擷取」的原始 GitHub 問題已於 2015 年開放使用。現在,如果我離開 2017 年 (當年),我獲得了 2 個。這證明瞭數學中的一項錯誤,因為 2015 年時,事實上就是「永遠」的。
2015 年,我們開始探索取消持續擷取的項目,在 780 個 GitHub 註解、780 個錯誤開始及 5 次提取要求之後,瀏覽器終於可以取消擷取到達畫面,第一個是 Firefox 57。
更新:答錯了,Edge 16 先打好援手!恭喜邊緣團隊!
我稍後會深入探討歷史,但首先介紹的是 API:
遙控器 + 訊號操控裝置
認識《AbortController
》和《AbortSignal
》:
const controller = new AbortController();
const signal = controller.signal;
控制器只有一個方法:
controller.abort();
執行此動作後,系統會通知以下信號:
signal.addEventListener('abort', () => {
// Logs true:
console.log(signal.aborted);
});
這個 API 是由 DOM 標準提供,也就是整個 API。這種方式十分通用,可供其他網頁標準和 JavaScript 程式庫使用。
取消信號並擷取
擷取作業可能需要AbortSignal
。以下範例說明如何在 5 秒後設定擷取逾時:
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);
});
取消擷取作業時,系統會取消要求和回應,因此也會取消讀取回應主體 (例如 response.text()
) 的所有作業。
這裡示範 - 截至本文撰寫時間時,只有 Firefox 57 瀏覽器支援這項功能。此外,請大膽自說,沒有人擅長製作示範。
或者,您也可以向要求物件提供信號,然後在之後傳遞以擷取:
const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });
fetch(request);
這是因為 request.signal
是 AbortSignal
。
回應取消的擷取作業
當您取消非同步作業時,promise 會拒絕名為 AbortError
的 DOMException
:
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);
}
});
使用者取消作業時,您通常不會想要顯示錯誤訊息,因為只要成功執行使用者要求,這不是「錯誤」。如要避免這種情況,請使用上述的 if 陳述式,例如上述範例來處理取消錯誤。
以下範例提供使用者載入內容的按鈕和取消按鈕的按鈕。如果擷取錯誤,系統會顯示錯誤,「除非」是取消錯誤:
// 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;
});
如需示範內容,截至本文撰寫時間時,只有 Edge 16 和 Firefox 57 瀏覽器支援這項功能。
單一信號,大量擷取作業
單一信號可用於一次取消多項擷取作業:
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);
}
在上述範例中,相同的信號用於初始擷取和平行章節擷取。fetchStory
的使用方式如下:
const controller = new AbortController();
const signal = controller.signal;
fetchStory({ signal }).then(chapters => {
console.log(chapters);
});
在此情況下,呼叫 controller.abort()
會取消正在進行中的擷取作業。
日後規劃
其他瀏覽器
Edge 的資歷非常出色,因此開始推動了這波浪潮,而 Firefox 竟然走在軌道上。他們的工程師在編寫規格時從測試套件中實作。其他瀏覽器的票證如下:
在 Service Worker 中
我需要完成 Service Worker 零件的規格,但方案如下:
如前所述,每個 Request
物件都有一個 signal
屬性。在服務工作站中,如果網頁不再對回應感興趣,fetchEvent.request.signal
就會發出取消信號。因此,這樣的程式碼就能正常運作:
addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
如果頁面取消擷取,fetchEvent.request.signal
會表示取消擷取,因此服務工作站內的擷取也會取消。
如要擷取 event.request
以外的內容,則必須將信號傳送至自訂擷取。
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 })
);
}
});
請按照規格進行追蹤;等到準備就緒,網頁可供導入時,我就會加入瀏覽器票券的連結。
歷史
對... 這個相對簡單的 API 需要花很久的時間才能組合在一起。原因如下:
API 不同意
如您所見,GitHub 討論時間已經相當長。
該執行緒有許多細微差異 (但有點缺乏細微差異),但關鍵差異在於其中一個群組希望 fetch()
傳回的物件有 abort
方法,而另一個群組想要區隔取得回應和影響回應。
這些條件並不相容,因此各組無法達到想要的成果。如果你是使用者,很抱歉!如果感覺讓您感覺好轉,我也在那組裡。但只要看到 AbortSignal
符合其他 API 的需求,就似乎是正確的選擇。此外,允許鏈結的承諾在不可行的情況下會變得相當複雜。
如果您想傳回提供回應的物件,但也可以取消,可以建立簡單的包裝函式:
function abortableFetch(request, opts) {
const controller = new AbortController();
const signal = controller.signal;
return {
abort: () => controller.abort(),
ready: fetch(request, { ...opts, signal })
};
}
在 TC39 中以 False 開頭
我們設法將已取消的動作與錯誤重複執行。這已包含用於表示「已取消」的第三個承諾狀態,以及可同時處理同步程式碼和非同步程式碼取消作業的新語法:
程式碼不是實際的程式碼 - 提案已撤銷
try { // Start spinner, then: await someAction(); } catch cancel (reason) { // Maybe do nothing? } catch (err) { // Show error message } finally { // Stop spinner }
在動作取消時,最常採取的做法沒有任何作用,上述提案會將取消作業與錯誤分開,因此您不必特別處理取消錯誤。catch cancel
可讓您得知已取消的動作,但在多數情況下不需要。
這分成 TC39 階段的第 1 階段,但並未達成共識,而提案已撤銷。
我們的替代提案 AbortController
不需要任何新語法,因此不太適合在 TC39 中規範。我們已經在 JavaScript 中定義所有需要用到的 JavaScript,因此我們在網路平台中定義了介面,特別是 DOM 標準。我們做出決定後
其他部分就能更快合併
大型規格變更
XMLHttpRequest
多年來已放棄,但規格其實相當模糊。不清楚在哪個位置可以避免或終止基礎網路活動,或如果呼叫 abort()
和擷取完成之間發生競爭狀況,會發生什麼情況。
我們這次想盡力做到這一點,但最終導致重大規格變更需要經過許多審核 (這是我的錯誤,也是因為 Anne van Kesteren 和 Domenic Denicola 推動了我的程式碼) 和好幾組測試。
但我們現在終於開始了!您可以使用新的網路原始功能來取消非同步動作,而且可以一次控制多個擷取!我們會在後續章節中,探討在擷取的整個生命週期啟用優先變更和較高層級的 API,以便觀察擷取進度。