“中止提取”GitHub 原始问题为 于 2015 年开放。现在,如果我从 2017 年(今年)抽成 2015 年,会得到 2 个。这演示了 因为 2015 年其实是“永生”。
2015 年,我们首次开始探索如何取消正在执行的抓取操作,在 GitHub 收到 780 条评论之后, 我们终于在浏览器上成功启动了可中止提取操作,并发出了 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 库使用。
中止信号并提取
提取操作可以接受 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 会拒绝,并显示名为 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
属性。在 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 Kesteren 和 Domenic Denicola(多梅尼克·德尼考拉)拖我过,以及一系列不错的测试。
但我们现在到了!我们提供了用于取消异步操作的新网络基元,并且可以进行多次提取 可以同时控制!下面,我们将着眼于如何在提取的整个生命周期内启用优先级更改,以及如何实现更高级别的 用于观察提取进度的 API。