「Aborting a fetch」 的原始 GitHub 問題是在 2015 年提出。現在,如果我將 2017 年 (目前年份) 減去 2015 年,得到的結果是 2。這顯示了數學中的錯誤,因為 2015 年實際上是「永遠」之前。
我們在 2015 年首次開始研究如何中止目前的擷取作業,在收到 780 則 GitHub 留言、幾次誤判和 5 個提取要求後,我們終於在瀏覽器中實現可中止的擷取功能,第一個支援的瀏覽器是 Firefox 57。
更新:我錯了。Edge 16 推出了中止支援功能!恭喜 Edge 團隊!
我稍後會深入探討歷史記錄,但首先,請看一下 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 程式庫使用。
中止信號和擷取
Fetch 可以使用 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
。
回應中斷的擷取作業
當您中斷非同步作業時,承諾會拒絕使用名為 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 也緊追在後。他們的工程師在規格書撰寫期間,從測試套件實作。如果是其他瀏覽器,請參閱以下問題單:
在服務工作者中
我需要完成服務工作站部分的規格,但以下是計畫:
如前所述,每個 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 討論內容相當長。該主題討論中有很多細微差異 (以及一些缺乏細微差異的情況),但主要的爭議點在於,一組人希望 abort
方法存在於 fetch()
傳回的物件上,而另一組人則希望取得回應和影響回應之間保持分離。
這些規定不相容,因此其中一個群組無法取得所需內容。如果是這種情況,很抱歉!如果你覺得這樣比較好,我也是這個群組的成員。不過,由於 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 元素都已存在,因此我們在網路平台中定義了介面,特別是 DOM 標準。做出這個決定後,其他事情就比較順利地解決了。
大量規格變更
XMLHttpRequest
多年以來一直可中斷,但規格相當模糊。我們不清楚在哪個時機可以避免或終止基礎網路活動,或是在 abort()
呼叫和擷取完成之間發生競爭條件時會發生什麼情況。
我們這次想確保一切正確無誤,但這導致需要大量審查的重大規格變更 (這是我的錯,在此要向 Anne van Kesteren 和 Domenic Denicola 致上萬分感謝,感謝你們幫我解決這個問題),以及一組不錯的測試。
但我們現在就在這裡!我們推出了新的網頁原始碼,可用於中斷非同步動作,而且可以一次控制多個擷取作業!接下來,我們將探討如何在擷取期間啟用優先順序變更,以及更高層級的 API 來觀察擷取進度。