無法擷取

阿奇巴德 (Jake Archibald)
Jake Archibald

「取消擷取」的原始 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.signalAbortSignal

回應取消的擷取作業

當您取消非同步作業時,promise 會拒絕名為 AbortErrorDOMException

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 KesterenDomenic Denicola 推動了我的程式碼) 和好幾組測試

但我們現在終於開始了!您可以使用新的網路原始功能來取消非同步動作,而且可以一次控制多個擷取!我們會在後續章節中,探討在擷取的整個生命週期啟用優先變更和較高層級的 API,以便觀察擷取進度