取消获取请求,以及一种抽象化的方法
在撰写另一篇关于fetch 的文章/教程时,我发现自己需要取消单个 fetch 请求。
我稍微调查了一下,了解到了AbortController(所有浏览器都支持,除了……你能猜到是谁吗?没错,就是 IE)。
这东西挺酷的,我来给你演示一下怎么用,稍后我会解释:
function fetchTodos(signal) {
return fetch('/todos', { signal });
}
function fetchUsers(signal) {
return fetch('/users', { signal });
}
const controller = new AbortController();
fetchTodos(controller.signal);
fetchUsers(controller.signal);
controller.abort();
好的,现在让我来详细解释一下。
首先,我们定义两个用于fetch检索数据的函数,它们还会接收一个信号参数(稍后会详细解释):
function fetchTodos(signal) {
return fetch('/todos', { signal });
}
function fetchUsers(signal) {
return fetch('/users', { signal });
}
之后,我们创建一个AbortController实例,该控制器允许我们获取要传递给 fetch 的信号,并且还允许我们取消请求:
const controller = new AbortController();
然后我们只需将控制器的信号属性传递给两个 fetch 请求即可:
fetchTodos(controller.signal);
fetchUsers(controller.signal);
这个信号是什么?
简单来说,它是一种与 DOM 请求通信的机制。不过并非直接通信,而是将信号的引用传递给 fetch 函数,然后通过控制器中止请求,控制器内部会与该信号进行交互。
如您所见,我们向两个请求传递了相同的信号,这意味着如果我们中止当前控制器,它将取消所有正在进行的请求。
最后,在执行 fetch 操作后的任何时候,我们都可以取消请求(如果请求尚未完成):
controller.abort();
注意:当
abort()调用 `reject()` 时,fetch()Promise 会被拒绝并抛出一个DOMException命名错误。AbortError
但是等等
如果中止运行后尝试fetchTodos再次运行会怎样?
// ... previous code
controller.abort();
fetchTodos(controller.signal);
如果我们传递相同的信号,请求将立即中止
。 我们需要为新请求创建一个新的控制器和信号,这样一来,为每个特定请求添加这些代码就显得有些繁琐。
让我们来看看我找到的解决方案,即返回一个自定义对象,并为每个请求生成一个信号:
我们首先需要一个类,它将封装 fetch promise,并可选择性地封装中止控制器:
export class CustomRequest {
constructor(requestPromise, abortController) {
if(!(requestPromise instanceof Promise)) {
throw TypeError('CustomRequest expects "promise" argument to be a Promise');
}
// Only check abort controller if passed in, otherwise ignore it
if(abortController && !(abortController instanceof AbortController)) {
throw TypeError('CustomRequest expects "abortController" argument to be an AbortController');
}
this.promise = requestPromise;
this.abortController = abortController;
}
abort() {
if (!this.abortController) return;
return this.abortController.abort();
}
then(fn) {
this.promise = this.promise.then(fn);
return this;
}
catch(fn) {
this.promise = this.promise.catch(fn);
return this;
}
}
CustomRequest行为几乎与 Promise 完全相同,但我们通过中止方法添加了一些额外的功能。
接下来,创建一个名为 `fetch` 的包装器abortableFetch,它将返回一个新的`CustomRequest`而不是常规的 `fetch` promise:
export function abortableFetch(uri, options) {
const abortController = new AbortController();
const abortSignal = abortController.signal;
const mergedOptions = {
signal: abortSignal,
method: HttpMethods.GET,
...options,
};
const promise = fetch(uri, mergedOptions);
return new CustomRequest(promise, abortController);
}
现在让我们修改原来的示例,并应用新的 fetch 函数:
function fetchTodos() {
return abortableFetch('/todos');
}
function fetchUsers() {
return abortableFetch('/users');
}
const todosReq = fetchTodos();
const usersReq = fetchUsers();
// We can now call abort on each individual requests
todosReq.abort();
usersReq.abort();
好多了,对吧?
我们甚至可以把它当作一个普通的承诺来使用:
const todosReq = fetchTodos();
todosReq.then(...).catch(...);
另一点需要注意的是,如果您想使用同一个信号控制所有请求,您仍然可以覆盖该信号。
function fetchTodos() {
return abortableFetch('/todos', { signal: globalSignal });
}
此信号将覆盖默认信号。abortableFetch
完整代码
export class CustomRequest {
constructor(requestPromise, abortController) {
if(!(requestPromise instanceof Promise)) {
throw TypeError('CustomRequest expects "promise" argument to be a Promise');
}
// Only check abort controller if passed in, otherwise ignore it
if(abortController && !(abortController instanceof AbortController)) {
throw TypeError('CustomRequest expects "abortController" argument to be an AbortController');
}
this.promise = requestPromise;
this.abortController = abortController;
}
abort() {
if (!this.abortController) return;
return this.abortController.abort();
}
then(fn) {
this.promise = this.promise.then(fn);
return this;
}
catch(fn) {
this.promise = this.promise.catch(fn);
return this;
}
}
export function abortableFetch(uri, options) {
const abortController = new AbortController();
const abortSignal = abortController.signal;
const mergedOptions = {
signal: abortSignal,
method: HttpMethods.GET,
...options,
};
const promise = fetch(uri, mergedOptions);
return new CustomRequest(promise, abortController);
}
function fetchTodos() {
return abortableFetch('/todos');
}
function fetchUsers() {
return abortableFetch('/users');
}
const todosReq = fetchTodos();
const usersReq = fetchUsers();
// We can now call abort on each individual requests
todosReq.abort();
usersReq.abort();
编辑 1
正如Jakub T. Jankiewicz在评论中指出的那样,初始实现存在问题,以下代码会失败:
const p = abortableFetch('...');
p.then(function() {
// nothing
});
p.then(function(res) {
// this will give error because first then return undefined and modify the promise
res.text();
});
但我们可以这样轻松解决这个问题:
class CustomRequest {
then(fn) {
return new CustomRequest(
this.promise.then(fn),
this.abortController,
);
}
catch(fn) {
return new CustomRequest(
this.promise.catch(fn),
this.abortController,
);
}
}
通过返回一个附加到新 Promise 的 CustomRequest 新实例,而不是重写该方法,我们可以避免Jakub T. Jankiewiczthis.promise报告的行为。
概括
说实话,对我来说,这又是一个有点奇怪的API。它虽然能用,但本可以做得更好。不过,我们可以围绕它做一些改进,提升一下用户体验。
总结一下,本文内容包括:
- 了解了如何以最简单的方式取消请求,
- 发现了一些奇怪或乏味的事情,
- 最后,我们在此基础上构建了一些东西来帮助我们简化流程!
链接
又一篇短文,我这周末一直在写作,所以……希望你们喜欢,也觉得有用!