在React和TypeScript中取消fetch的方法指南

462 阅读2分钟

这篇文章将介绍如何在React和TypeScript应用程序中以编程方式取消一个获取请求:

Cancelling fetch with TypeScript

一个React组件

我们有一个典型的React组件,从网络API中获取一些数据并进行渲染:

export function App() {
  const [status, setStatus] = React.useState<"loading" | "loaded" | "cancelled">("loading");
  const [data, setData] = React.useState<Character | undefined>(undefined);

  React.useEffect(() => {
    getCharacter(1).then((character) => {
      setData(character);
      setStatus("loaded");
    });
  }, []);

  if (status === "loading") {
    return (
      <div>
        <div>loading ...</div>
        <button>Cancel</button>
      </div>
    );
  }
  if (status === "cancelled") {
    return <div>Cancelled</div>;
  }

  return <div>{data && <h3>{data.name}</h3>}</div>;
}

数据是在useEffect 内的getCharacter 函数中获取的,并放在名为data 的状态中。

一个叫做status 的状态变量跟踪我们在获取过程中的位置。请注意,当数据被获取时,一个 "取消"按钮正在被呈现出来:

Cancel button

当 "取消"按钮被点击时,我们想取消获取请求。

让我们来看看getCharacter 函数:

async function getCharacter(id: number) {
  const response = await fetch(`https://swapi.dev/api/people/${id}/`);
  const data = await response.json();
  assertIsCharacter(data);
  return data;
}

这是对星球大战API的一个直接请求。

这里是Character 的类型:

type Character = {
  name: string;
};

我们只对《星球大战》人物资源中的name 字段感兴趣。

assertIsCharacter 类型断言函数如下:

function assertIsCharacter(data: any): asserts data is Character {
  if (!("name" in data)) {
    throw new Error("Not character");
  }
}

这个类型断言函数允许TypeScript将data 的类型缩小到Character

使用AbortController 来取消fetch

AbortController是最近才加入到JavaScript中的,它是在最初的fetch 实现之后出现的。好消息是,它在所有现代浏览器中都被支持。

AbortController 包含一个 方法。它还包含一个 属性,可以传递给 。当 被调用时, 的请求被取消了。abort signal fetch AbortController.abort fetch

让我们在getCharacter 中的fetch 请求中使用AbortController 和其signal :

function getCharacter(id: number) {
  const controller = new AbortController();
  const signal = controller.signal;
  const promise = new Promise(async (resolve) => {
    const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
      method: "get",
      signal,
    });
    const data = await response.json();
    assertIsCharacter(data);
    resolve(data);
  });
  promise.cancel = () => controller.abort();
  return promise;
}

我们从getCharacter 中删除了async 关键字,并将现有的代码包裹在一个新的Promise 中。Promise 是用请求中的数据来解决的。我们在Promise 中添加了一个cancel 方法,它调用了AbortController.abort

包含cancel 方法的Promise 被从getCharacter 返回,这样调用代码就可以使用这个方法来取消请求。

不过我们有一个类型错误:

// 💥 - Property 'cancel' does not exist on type 'Promise<unknown>'
promise.cancel = () => controller.abort();

让我们为包含cancel 方法的Promise创建一个新类型。

interface PromiseWithCancel<T> extends Promise<T> {
  cancel: () => void;
}

然后我们可以使用一个类型断言来解决类型错误:

function getCharacter(id: number) {
  ...
  (promise as PromiseWithCancel<Character>).cancel = () => controller.abort();  return promise as PromiseWithCancel<Character>;}

在React组件中使用新的getCharacter

我们将把来自getCharacter 的承诺存储在一个叫做query 的状态变量中:

export function App() {
  const [status, setStatus] = React.useState<"loading" | "loaded" | "cancelled">("loading");
  const [data, setData] = React.useState<Character | undefined>(undefined);
  const [query, setQuery] = React.useState<PromiseWithCancel<Character> | undefined>(undefined);  React.useEffect(() => {
    const q = getCharacter(1);    setQuery(q);    q.then((character) => {      setData(character);
      setStatus("loaded");
    });
  }, []);
  ...

现在我们可以在点击取消按钮时调用承诺中的cancel 方法。

<button
  onClick={() => {    query?.cancel();    setStatus("cancelled");  }}>
  Cancel
</button>

当 "取消"按钮被点击时,我们看到 "取消"被呈现出来了:

Cancel button

如果我们看一下网络请求,我们可以看到请求被取消了:

Cancelled request 不错。😊

捕获中止错误

如果我们看一下控制台,我们会发现当请求被取消时,出现了一个错误: Cancel error

我们可以通过将请求包装在一个try catch 语句中来捕获这个错误:

const promise = new Promise(async (resolve) => {
  try {    const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
      method: "get",
      signal,
    });
    const data = await response.json();
    assertIsCharacter(data);
    resolve(data);
  } catch (ex: unknown) {    if (isAbortError(ex)) {      console.log(ex.message);    }  }});

isAbortError 类型的谓词函数如下:

function isAbortError(error: any): error is DOMException {
  if (error && error.name === "AbortError") {
    return true;
  }
  return false;
}

现在,当我们点击 "取消"按钮时,我们会得到输出到控制台的信息,而不是错误:

Cancel message

总结

AbortController 中的signal 属性可以被传递到fetch 。然后可以调用AbortController.abort 来取消请求。

取消fetch ,会产生一个错误,可以用try catch 吞掉。

完整的代码在这个gist中。