可取消的提取

Jake Archibald
Jake Archibald

“中止提取”GitHub 原始问题为 于 2015 年开放。现在,如果我从 2017 年(今年)抽成 2015 年,会得到 2 个。这演示了 因为 2015 年其实是“永生”。

2015 年,我们首次开始探索如何取消正在执行的抓取操作,在 GitHub 收到 780 条评论之后, 我们终于在浏览器上成功启动了可中止提取操作,并发出了 5 次拉取请求 第一个是 Firefox 57

最新消息:哎呀,我错了。Edge 16 已率先支持中止!恭喜 Edge 团队!

我稍后会深入探讨历史记录,但首先介绍一下 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())的操作也会被中止。

这是一个演示 – 在撰写本文时,Google 仅提供一种 支持 Firefox 57另外,做好万全准备,没有人参与过任何设计技能 了解这一点

或者,您也可以将此信号提供给请求对象,稍后将其传递给提取方法:

const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });

fetch(request);

这之所以有效,是因为 request.signal 是一个 AbortSignal

对取消的提取做出响应

取消异步操作时,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 属性。在 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 的讨论已经很长。 讨论帖中有许多细微差别(并且缺少一些细微差别),但主要的分歧之处在于 群组希望在 fetch() 返回的对象上存在 abort 方法,而另一个 希望在获得响应和影响响应之间区分开来。

这些要求是不兼容的,因此一组客户无法得到他们想要的东西。如果 你,对不起!如果这样能让你感觉好多了,我也加入了这个群组。但看到AbortSignal符合 其他 API 的要求,看起来是合适的选择。此外,允许链式 promise 会变得非常复杂,即使并非不可行。

如果想要返回一个提供响应的对象,但您也可以取消,可以创建一个 简单的封装容器:

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

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

在 TC39 中以 False 开头

尝试将已取消的操作与错误区分开来。这包括第三个 promise 状态表示“已取消”,以及一些新语法,用于在同步和异步处理中处理取消 代码:

错误做法

不是真实代码 - 提案已撤消

    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。