这篇文章将介绍如何在React和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 的状态变量跟踪我们在获取过程中的位置。请注意,当数据被获取时,一个 "取消"按钮正在被呈现出来:

当 "取消"按钮被点击时,我们想取消获取请求。
让我们来看看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>
当 "取消"按钮被点击时,我们看到 "取消"被呈现出来了:

如果我们看一下网络请求,我们可以看到请求被取消了:
不错。😊
捕获中止错误
如果我们看一下控制台,我们会发现当请求被取消时,出现了一个错误:

我们可以通过将请求包装在一个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;
}
现在,当我们点击 "取消"按钮时,我们会得到输出到控制台的信息,而不是错误:

总结
AbortController 中的signal 属性可以被传递到fetch 。然后可以调用AbortController.abort 来取消请求。
取消fetch ,会产生一个错误,可以用try catch 吞掉。
完整的代码在这个gist中。