发布于 2026-01-06 2 阅读
0

取消获取请求,以及一种抽象化的方法

取消获取请求,以及一种抽象化的方法

在撰写另一篇关于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();
Enter fullscreen mode Exit fullscreen mode

 好的,现在让我来详细解释一下。

首先,我们定义两个用于fetch检索数据的函数,它们还会接收一个信号参数(稍后会详细解释):

function fetchTodos(signal) {
    return fetch('/todos', { signal });
}

function fetchUsers(signal) {
    return fetch('/users', { signal });
}
Enter fullscreen mode Exit fullscreen mode

之后,我们创建一个AbortController实例,该控制器允许我们获取要传递给 fetch 的信号,并且还允许我们取消请求:

const controller = new AbortController();
Enter fullscreen mode Exit fullscreen mode

然后我们只需将控制器的信号属性传递给两个 fetch 请求即可:

fetchTodos(controller.signal);
fetchUsers(controller.signal);
Enter fullscreen mode Exit fullscreen mode

这个信号是什么

简单来说,它是一种与 DOM 请求通信的机制。不过并非直接通信,而是将信号的引用传递给 fetch 函数,然后通过控制器中止请求,控制器内部会与该信号进行交互。

如您所见,我们向两个请求传递了相同的信号,这意味着如果我们中止当前控制器,它将取消所有正在进行的请求。

最后,在执行 fetch 操作后的任何时候,我们都可以取消请求(如果请求尚未完成):

controller.abort();
Enter fullscreen mode Exit fullscreen mode

注意:当abort()调用 `reject()` 时,fetch()Promise 会被拒绝并抛出一个DOMException命名错误。AbortError

但是等等

如果中止运行后尝试fetchTodos再次运行会怎样?

// ... previous code
controller.abort();

fetchTodos(controller.signal);
Enter fullscreen mode Exit fullscreen mode

如果我们传递相同的信号,请求立即中止
。 我们需要为新请求创建一个新的控制器和信号,这样一来,为每个特定请求添加这些代码就显得有些繁琐。

让我们来看看我找到的解决方案,即返回一个自定义对象,并为每个请求生成一个信号:

我们首先需要一个类,它将封装 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

现在让我们修改原来的示例,并应用新的 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();
Enter fullscreen mode Exit fullscreen mode

好多了,对吧?

我们甚至可以把它当作一个普通的承诺来使用:

const todosReq = fetchTodos();
todosReq.then(...).catch(...);
Enter fullscreen mode Exit fullscreen mode

另一点需要注意的是,如果您想使用同一个信号控制所有请求,您仍然可以覆盖该信号。

function fetchTodos() {
    return abortableFetch('/todos', { signal: globalSignal });
}
Enter fullscreen mode Exit fullscreen mode

此信号将覆盖默认信号。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();
Enter fullscreen mode Exit fullscreen mode

编辑 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(); 
});
Enter fullscreen mode Exit fullscreen mode

但我们可以这样轻松解决这个问题:

class CustomRequest {
    then(fn) {
        return new CustomRequest(
            this.promise.then(fn),
            this.abortController,
        );
    }

    catch(fn) {
        return new CustomRequest(
            this.promise.catch(fn),
            this.abortController,
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

通过返回一个附加到新 Promise 的 CustomRequest 新实例,而不是重写该方法,我们可以避免Jakub T. Jankiewiczthis.promise报告的行为。

概括

说实话,对我来说,这又是一个有点奇怪的API。它虽然能用,但本可以做得更好。不过,我们可以围绕它做一些改进,提升一下用户体验。

总结一下,本文内容包括:

  • 了解了如何以最简单的方式取消请求,
  • 发现了一些奇怪或乏味的事情,
  • 最后,我们在此基础上构建了一些东西来帮助我们简化流程!

链接


又一篇短文,我这周末一直在写作,所以……希望你们喜欢,也觉得有用!

如果你喜欢这篇文章,请考虑通过点赞、关注我的主页或GitHub账号、或者评论来支持我!
文章来源:https://dev.to/nombrekeff/cancel-fetch-requests-and-a-way-to-abstract-it-3gib