关于“中止提取”的原始 GitHub 问题是在 2015 年提出的。现在,如果我从 2017 年(当前年份)减去 2015 年,我会得到 2。这说明了数学中存在 bug,因为 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。它是故意设计为通用的,以便其他 Web 标准和 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.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 中
我需要完成服务工作器部分的规范,但计划如下:
如前所述,每个 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 帮我度过难关),以及一组不错的测试。
不过,我们现在就在这里!我们推出了用于中止异步操作的新 Web 基元,并且可以同时控制多个提取操作!接下来,我们将介绍如何在整个提取生命周期内启用优先级更改,以及如何使用更高级别的 API 来监控提取进度。