無法擷取

Jake Archibald
Jake Archibald

「取消擷取」的原始 GitHub 問題是 自 2015 年開業至今現在,如果我在 2015 年外出 (今年) 拿到 2 號,這表示 因為 2015 年其實就是「永遠」。

2015 年是我們第一次開始嘗試停止執行中的擷取作業,而在 780 個 GitHub 註解之後 分別提出幾個 false 啟動、5 提取要求,我們終於在瀏覽器中擷取到達目的地, 第一個是 Firefox 57

最新資訊:這不對,是錯了。Edge 16 號登陸月球支援先鋒!恭喜 邊緣隊!

我會先來深入介紹歷史,但首先是 API:

控制器 + 信號操縱

認識 AbortControllerAbortSignal

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 所以可以運作。

回應已取消的擷取作業

取消非同步作業時,承諾會拒絕含有名為 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 屬性。在 Service Worker 中 如果網頁不再對回應內容感興趣,fetchEvent.request.signal 就會中止訊息。 因此,類似下方的程式碼應該可以正常運作:

addEventListener('fetch', event => {
    event.respondWith(fetch(event.request));
});

如果網頁取消擷取作業,fetchEvent.request.signal 信號就會中止,因此擷取 Service Worker 也會取消。

如果您要擷取 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 可以滿足 其實這是合適的選擇此外,透過鏈結的承諾 會變得非常複雜

如要傳回提供回應的物件,但也可以取消,則可以建立 簡易包裝函式:

function abortableFetch(request, opts) {
    const controller = new AbortController();
    const signal = controller.signal;

    return {
    abort: () => controller.abort(),
    ready: fetch(request, { ...opts, signal })
    };
}

在 TC39 中發生否定

系統無法將取消的動作與錯誤區分完成。這也包含第三個承諾 狀態來表示「已取消」,以及處理同步處理和非同步作業中取消作業的新語法 程式碼:

錯誤做法

非實際程式碼 - 提案已撤銷

    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 KesterenDomenic Denicola,以拖曳的方式將我拉過來) 以及一系列精良的考試

我們終於來到這裡了!我們擁有可以取消非同步動作的全新網頁基本功能,而多個擷取作業可以 就能同時控制!在下方的章節中,我們會說明如何在擷取的整個生命週期中啟用優先順序變更,以及更高層級的設定 用於觀察擷取進度的 API。