九、与 RESTful API 交互
在构建应用程序时,与 RESTful API 交互是一项非常常见的任务,它总是导致我们必须编写异步代码。因此,在本章中,首先,我们将详细介绍异步代码。
我们可以使用许多库来帮助我们与 RESTAPI 交互。在本章中,我们将介绍本机浏览器函数和用于与 RESTAPI 交互的流行开源库。我们将发现开源库相对于本机函数所具有的其他特性。我们还将研究如何在 React 类和基于函数的组件中与 restapi 交互
在本章中,我们将学习以下主题:
- 编写异步代码
- 使用 fetch
- 将 axios 与类组件一起使用
- 将 axios 与功能组件一起使用
技术要求
我们在本章中使用以下技术:
- TypeScript 游戏场:这是一个位于的网站 https://www.typescriptlang.org/play/ 这允许我们在不安装任何东西的情况下使用异步代码。
- Node.js 和
npm
:TypeScript 和 React 依赖于这些。我们可以从安装这些 https://nodejs.org/en/download/ 。如果我们已经安装了这些,请确保npm
至少是 5.2 版 - 类型脚本:可通过
npm
在终端中使用以下命令进行安装:
npm install -g typescript
-
Visual Studio 代码。我们需要一个编辑器来编写 React 和 TypeScript 代码,可以从安装 https://code.visualstudio.com/ 。我们还需要在 VisualStudio 代码中安装 TSLint(由 egamma 编写)和 Prettier(由 Estben Petersen 编写)扩展。
-
jsonplaceholder.typicode.com
:我们将使用此在线服务帮助我们学习如何与 RESTful API 交互。
All the code snippets in this chapter can be found online at https://github.com/carlrip/LearnReact17WithTypeScript/tree/master/09-RestfulAPIs.
编写异步代码
默认情况下,TypeScript 代码是同步执行的,其中每一行代码都是在彼此之后执行的。然而,TypeScript 代码也可以是异步的,这意味着事情可以独立于我们的代码发生。调用 RESTAPI 是异步代码的一个示例,因为 API 请求是在我们的 TypeScript 代码之外处理的。因此,与 RESTAPI 交互迫使我们编写异步代码。
在本节中,我们将花时间了解在使用异步代码与 RESTful API 交互之前编写异步代码时可以采用的方法。在下一节中,我们将从回调开始。
回调
回调函数是作为参数传递给异步函数的函数,在异步函数完成时调用。在下一节中,我们将介绍一个使用回调编写异步代码的示例。
回调执行
让我们看一个在 TypeScript 游戏场的异步代码中使用回调的示例。让我们输入以下代码:
let firstName: string;
setTimeout(() => {
firstName = "Fred";
console.log("firstName in callback", firstName);
}, 1000);
console.log("firstName after setTimeout", firstName);
代码调用 JavaScriptsetTimeout
函数,该函数是异步的。它将回调作为第一个参数,执行时应等待的毫秒数作为第二个参数执行回调。
我们使用一个 arrow 函数作为回调函数,将firstName
变量设置为“Fred”,并将其输出到控制台。我们也会在调用setTimeout
后立即在控制台中登录firstName
。
那么,哪个console.log
语句将首先执行?如果我们运行代码并查看控制台,我们将看到最后一行首先执行:
关键是调用setTimeout
后,执行继续到下一行代码。执行不会等待回调被调用。这会使包含回调的代码比同步代码更难读取,特别是当回调嵌套在回调中时。这被许多开发者称为回调地狱!
那么,我们如何处理异步回调代码中的错误呢?我们将在下一节中找到答案。
处理回调错误
在本节中,我们将探讨如何在使用回调代码时处理错误:
- 让我们首先在 TypeScript 中输入以下代码:
try {
setTimeout(() => {
throw new Error("Something went wrong");
}, 1000);
} catch (ex) {
console.log("An error has occurred", ex);
}
我们再次使用setTimeout
来尝试回调。这一次,我们在回调中抛出一个错误。我们希望在setTimeout
函数周围使用try / catch
捕捉回调之外的错误。
如果我们运行代码,就会发现我们没有捕捉到错误:
- 我们必须处理回调中的错误。因此,让我们将示例调整为以下内容:
interface IResult {
success: boolean;
error?: any;
}
let result: IResult = { success: true };
setTimeout(() => {
try {
throw new Error("Something went wrong");
} catch (ex) {
result.success = false;
result.error = ex;
}
}, 1000);
console.log(result);
这一次,try / catch
在回调中。我们使用一个变量result
来确定回调是否成功执行,以及是否存在任何错误。IResult
接口为我们提供了一个很好的类型安全性,结果是variable
。
如果我们运行此代码,我们将看到我们成功地处理了错误:
因此,在读取基于回调的代码的同时处理错误是一项挑战。幸运的是,有其他方法可以解决这些挑战,我们将在下一节中介绍。
承诺
promise 是一个 JavaScript 对象,表示异步操作的最终完成(或失败)及其结果值。在下一节中,我们将看一个使用基于承诺的函数的示例,然后创建我们自己的基于承诺的函数。
使用基于承诺的函数
让我们快速查看一些公开基于承诺的 API 的代码:
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => console.log(data))
.catch(json => console.log("error", json));
- 此函数是用于与 RESTful API 交互的本机 JavaScript
fetch
函数 - 该函数接收请求的 URL
- 它有一个
then
方法来处理响应体的响应和读取 - 它有一个
catch
方法来处理任何错误
代码的执行按照我们阅读的方式向下流动。我们也不必在then
方法中做任何额外的工作来处理错误。因此,这比使用基于回调的异步代码要好得多。
在下一节中,我们将创建自己的基于承诺的函数。
创建基于承诺的函数
在本节中,我们将创建一个wait
函数,以异步等待作为参数传入的毫秒数:
- 让我们在 TypeScript 中输入以下内容:
const wait = (ms: number) => {
return new Promise((resolve, reject) => {
if (ms > 1000) {
reject("Too long");
}
setTimeout(() => {
resolve("Sucessfully waited");
}, ms);
});
};
- 函数首先返回一个
Promise
对象,该对象接受需要异步执行的函数作为其构造函数参数 promise
函数接受resolve
参数,该参数是我们在函数完成执行后调用的函数- promise 函数还接受一个
reject
参数,当函数出错时,我们调用该参数 -
在内部,我们使用带有回调的
setTimeout
来进行实际的等待 -
让我们使用我们承诺的基于
wait
的功能:
wait(500)
.then(result => console.log("then >", result))
.catch(error => console.log("catch >", error));
函数只需在等待 500 毫秒后将结果或错误输出到控制台。
那么,让我们尝试一下并运行它:
如我们所见,控制台中的输出指示执行了then
方法
- 如果调用参数大于 1000 的
wait
函数,则应调用catch
方法。让我们尝试一下:
wait(1500)
.then(result => console.log("then >", result))
.catch(error => console.log("catch >", error));
如预期,执行catch
方法:
因此,承诺为我们提供了一种编写异步代码的好方法。然而,我们在本书前面已经多次使用了另一种方法。我们将在下一节中介绍此方法。
异步并等待
async
和await
是两个 JavaScript 关键字,我们可以使用它们使异步代码的读取与同步代码几乎相同:
- 让我们来看一个使用我们在上一节中创建的
wait
函数的例子,在wait
函数声明之后,将以下代码输入到 TypeScript 中:
const someWork = async () => {
try {
const result = await wait(500);
console.log(result);
} catch (ex) {
console.log(ex);
}
};
someWork();
- 我们创建了一个名为
someWork
的箭头函数,该函数用async
关键字标记为异步。 - 然后我们调用前缀为
await
关键字的wait
。这将停止执行下一行,直到wait
完成。 try / catch
将捕获任何错误。
因此,代码与以同步方式编写代码的方式非常相似。
如果我们运行这个例子,我们得到确认,try
分支中的console.log
语句等待wait
函数完成后才执行:
- 让我们将等待时间更改为
1500
毫秒:
const result = await wait(1500);
如果运行此操作,我们会看到引发并捕获错误:
所以,async
和await
使我们的代码变得美观易读。在 TypeScript 中使用这些功能的一个好处是,代码可以传输到较旧的浏览器中。例如,我们可以使用async
和await
编码,并且仍然支持 IE。
现在我们已经对编写异步代码有了很好的理解,我们将在下面几节中与 RESTful API 交互时将其付诸实践。
使用 fetch
fetch
函数是一个本机 JavaScript 函数,我们可以使用它与 RESTful API 交互。在本节中,我们将使用fetch
介绍一些常见的 RESTful API 交互,从获取数据开始。在本节中,我们将与奇妙的JSONPlaceholder
RESTAPI 交互。
使用 fetch 获取数据
在本节中,我们将使用fetch
从JSONPlaceholder
REST API 获取一些帖子,从一个基本的GET
请求开始。
基本 GET 请求
让我们打开 TypeScript 游乐场并输入以下内容:
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => console.log(data));
以下是一些要点:
fetch
函数中的第一个参数是请求的 URLfetch
是基于承诺的功能- 第一个
then
方法处理响应 - 第二个
then
方法处理主体何时被解析为 JSON
如果我们运行代码,应该会看到控制台输出的 POST 数组:
获取响应状态
通常,我们需要检查请求的状态。我们可以这样做:
fetch("https://jsonplaceholder.typicode.com/posts").then(response => {
console.log(response.status, response.ok);
});
- 响应
status
属性给出响应的 HTTP 状态代码 - 响应
ok
属性为boolean
,返回 HTTP 状态码是否在 200 范围内
如果我们运行前面的代码,我们将得到 200 和 true 输出到控制台。
让我们在帖子不存在的情况下尝试一个示例请求:
fetch("https://jsonplaceholder.typicode.com/posts/1001").then(response => {
console.log(response.status, response.ok);
});
如果我们运行前面的代码,我们将得到 404 和假输出到控制台。
处理错误
正如我们对基于承诺的函数所期望的那样,我们在catch
方法中处理错误:
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => console.log(data))
.catch(json => console.log("error", json));
但是,catch
方法不会捕获不在 200 范围内的响应。这方面的一个例子是在上一个例子中,我们在响应状态代码中得到了 404。因此,HTTP 错误状态代码可以在第一个then
方法中处理,而不是在catch
方法中处理。
那么,什么是catch
方法呢?答案是捕捉网络错误。
这就是使用fetch
获取数据的方法。在下一节中,我们将介绍过账数据。
使用 fetch 创建数据
在本节中,我们将使用fetch
使用JSONPlaceholder
REST API 创建一些数据。
基本职位要求
通过 REST API 创建数据通常涉及使用 HTTPPOST
方法处理我们要在请求体中创建的数据。
让我们打开 TypeScript 游乐场并输入以下内容:
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
body: JSON.stringify({
title: "Interesting post",
body: "This is an interesting post about ...",
userId: 1
})
})
.then(response => {
console.log(response.status);
return response.json();
})
.then(data => console.log(data));
fetch
调用与获取数据基本相同。关键区别在于第二个参数,它是一个选项对象,可以包含请求的方法和主体。还要注意,主体需要是一个string
。
如果我们运行前面的代码,我们将在控制台中获得一个 201 和一个包含生成的 post ID 的对象。
请求 HTTP 头
通常,我们需要在请求中包含 HTTP 头。我们可以在headers
属性的options
对象中指定:
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "bearer some-bearer-token"
},
body: JSON.stringify({
title: "Interesting post",
body: "This is an interesting post about ...",
userId: 1
})
})
.then(response => {
console.log(response.status);
return response.json();
})
.then(data => console.log(data));
请求头可以以这种方式用于任何 HTTP 方法,而不仅仅是 HTTPPOST
。例如,我们可以将其用于GET
请求,如下所示:
fetch("https://jsonplaceholder.typicode.com/posts/1", {
headers: {
"Content-Type": "application/json",
Authorization: "bearer some-bearer-token"
}
}).then(...);
这就是如何使用fetch
将数据发布到 RESTAPI。在下一节中,我们将研究不断变化的数据。
使用 fetch 更改数据
在本节中,我们将使用fetch
通过 REST API 更改一些数据
基本 PUT 请求
更改数据的一种常见方式是通过PUT
请求。让我们打开 TypeScript 游乐场并输入以下内容:
fetch("https://jsonplaceholder.typicode.com/posts/1", {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "Corrected post",
body: "This is corrected post about ...",
userId: 1
})
})
.then(response => {
console.log(response.status);
return response.json();
})
.then(data => console.log(data));
因此,执行 HTTPPUT
的fetch
调用的结构与POST
请求非常相似。唯一的区别是我们将 options 对象中的method
属性指定为PUT
。
如果我们运行前面的代码,我们将得到 200 和更新后的POST
对象输出到控制台。
基本补丁请求
一些 REST API 提供PATCH
请求,允许我们提交对资源某一部分的更改。让我们打开 TypeScript 游乐场并输入以下内容:
fetch("https://jsonplaceholder.typicode.com/posts/1", {
method: "PATCH",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({
title: "Corrected post"
})
})
.then(response => {
console.log(response.status);
return response.json();
})
.then(data => console.log(data));
因此,我们正在使用PATCH
HTTP 方法提交对文章标题的更改。如果我们运行前面的代码,我们将得到 200 和更新后的 post 对象输出到控制台。
这就是如何使用fetch
来PUT
和PATCH
。在下一节中,我们将删除一些数据。
使用 fetch 删除数据
通常,我们通过 REST API 上的DELETE
HTTP 方法删除数据。让我们在 TypeScript 中输入以下内容:
fetch("https://jsonplaceholder.typicode.com/posts/1", {
method: "DELETE"
}).then(response => {
console.log(response.status);
});
因此,我们请求使用DELETE
方法删除一篇帖子。
如果我们运行前面的代码,我们将得到 200 个输出到控制台。
因此,我们学习了如何使用本机fetch
函数与 RESTful API 交互。在下一节中,我们将研究如何使用一个流行的开源库,并了解它相对于fetch
的好处。
将 axios 与类组件一起使用
axios
是一款流行的开源 JavaScript HTTP 客户端。我们将构建一个小型 React 应用程序,用于创建、读取、更新和删除JSONPlaceholder
REST API 中的帖子。一路上,我们会发现axios
比fetch
有一些好处。我们下一节的第一项工作是安装axios
。
安装 axios
在安装axios
之前,我们将快速创建我们的小 React 应用程序:
- 在我们选择的文件夹中,打开 Visual Studio 代码及其终端,输入以下命令以创建新的 React 和 TypeScript 项目:
npx create-react-app crud-api --typescript
请注意,我们使用的 React 版本至少需要为版本16.7.0-alpha.0
。我们可以在package.json
文件中查看。如果package.json
中 React 的版本早于16.7.0-alpha.0
,那么我们可以使用以下命令安装此版本:
npm install react@16.7.0-alpha.0
npm install react-dom@16.7.0-alpha.0
- 创建项目后,让我们将 TSLint 作为开发依赖项添加到我们的项目中,以及一些与 React 和 Prettier 配合良好的规则:
cd crud-api
npm install tslint tslint-react tslint-config-prettier --save-dev
- 现在我们添加一个包含一些规则的
tslint.json
文件:
{
"extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
"rules": {
"ordered-imports": false,
"object-literal-sort-keys": false,
"jsx-no-lambda": false,
"no-debugger": false,
"no-console": false,
},
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts",
"coverage/lcov-report/*.js"
]
}
}
- 如果我们打开
App.tsx
,则有一个脱毛错误。那么,让我们通过在render
方法上添加public
作为修饰符来解决这个问题:
class App extends Component {
public render() {
return ( ... );
}
}
- 现在我们可以使用 NPM 安装
axios
:
npm install axios
注意,axios
中有 TypeScript 类型,所以我们不需要安装它们
- 在继续开发之前,让我们开始运行我们的应用程序:
npm start
然后应用程序将启动并在我们的浏览器中运行。在下一节中,我们将使用 axios 从 JSONPlaceholder 获取文章。
使用 axios 获取数据
在本节中,我们将在App
组件中呈现来自JSONPlaceholder
的帖子。
基本 GET 请求
我们将首先使用带有axios
的基本 GET 请求获取帖子,然后将它们呈现在无序列表中:
- 我们打开
App.tsx
并为axios
添加导入语句:
import axios from "axios";
- 我们还要为来自 JSONPlaceholder 的帖子创建一个接口:
interface IPost {
userId: number;
id?: number;
title: string;
body: string;
}
- 我们将以状态存储帖子,因此让我们为其添加一个接口:
interface IState {
posts: IPost[];
}
class App extends React.Component<{}, IState> { ... }
- 然后将 post 状态初始化为构造函数中的空数组:
class App extends React.Component<{}, IState> {
public constructor(props: {}) {
super(props);
this.state = {
posts: []
};
}
}
- 当从 RESTAPI 获取数据时,我们通常在
componentDidMount
生命周期方法中这样做。那么,让我们通过axios
来获得我们的帖子:
public componentDidMount() {
axios
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts")
.then(response => {
this.setState({ posts: response.data });
});
}
- 我们使用
axios
中的get
函数获取数据,这是一个承诺的基于fetch
的函数 - 这是一个接受响应主体类型作为参数的通用函数
-
我们将请求的 URL 作为参数传递给
get
函数 -
然后我们可以用
then
方法处理响应 - 根据泛型参数,我们通过类型化的响应对象中的
data
属性访问响应体
因此,这在两个方面比fetch
好:
- 我们可以很容易地键入响应
-
只有一步(而不是两步)才能得到响应体
-
现在我们有了处于组件状态的帖子,让我们用
render
方法呈现帖子。让我们也移除header
标签:
public render() {
return (
<div className="App">
<ul className="posts">
{this.state.posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
我们使用posts
数组的map
函数在无序列表中显示帖子。
- 我们引用了一个
posts
CSS 类,所以我们将其添加到index.css
中:
.posts {
list-style: none;
margin: 0px auto;
width: 800px;
text-align: left;
}
如果我们看一下 running 应用程序,它现在将如下所示:
因此,一个带有axios
的基本GET
请求既简单又好用。我们需要在类组件中使用componentDidMount
生命周期方法来进行 RESTAPI 调用,该调用将包含来自响应的数据。
我们如何处理错误呢?我们将在下一节介绍这一点。
处理错误
- 让我们调整请求中的 URL:
.get<IPost[]>("https://jsonplaceholder.typicode.com/postsX")
如果我们看一下正在运行的应用程序,帖子将不再呈现。
- 我们希望处理这种情况并给用户一些反馈。我们可以使用
catch
方法来实现这一点:
axios
.get<IPost[]>("https://jsonplaceholder.typicode.com/postsX")
.then( ... )
.catch(ex => {
const error =
ex.response.status === 404
? "Resource not found"
: "An unexpected error has occurred";
this.setState({ error });
});
因此,与fetch
不同,HTTP 状态错误代码可以通过catch
方法处理。catch
中的错误对象参数包含一个response
属性,该属性包含有关响应的信息,包括 HTTP 状态代码
- 我们刚刚在
catch
方法中引用了一个名为error
的状态。我们将在下一步中使用它来呈现错误消息。但是,我们首先需要将此状态添加到接口并对其进行初始化:
interface IState {
posts: IPost[];
error: string;
}
class App extends React.Component<{}, IState> {
public constructor(props: {}) {
super(props);
this.state = {
posts: [],
error: ""
};
}
}
- 然后,如果错误包含一个值,则呈现该错误:
<ul className="posts">
...
</ul>
{this.state.error && <p className="error">{this.state.error}</p>}
- 让我们添加刚才引用到
index.css
的error
CSS 类:
.error {
color: red;
}
如果我们现在查看正在运行的应用程序,我们将看到红色的“未找到资源”。
- 现在让我们将 URL 更改为有效的 URL,以便在下一节中继续了解如何包含 HTTP 头:
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts")
因此,使用axios
处理 HTTP 错误与使用fetch
不同。我们在第一个then
方法中使用fetch
进行处理,而在catch
方法中使用axios
进行处理。
请求 HTTP 头
为了在请求中包含 HTTP 头,我们需要在get
函数中添加第二个参数,该参数可以包含各种选项,包括 HTTP 头。
让我们为请求中的内容类型添加一个 HTTP 头:
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
headers: {
"Content-Type": "application/json"
}
})
因此,我们在名为headers
的属性中的对象中定义 HTTP 头。
如果我们看看 running 应用程序,它将完全相同。JSONPlaceholder REST API 不需要内容类型,但是我们与之交互的其他 REST API 可以这样做。
在下一节中,我们将研究在fetch
函数中不容易实现的功能,即指定请求超时的功能。
超时
在一定时间后超时请求可以改善我们应用程序中的用户体验:
- 让我们在请求中添加一个超时:
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
headers: {
"Content-Type": "application/json"
},
timeout: 1
})
因此,向axios
请求添加超时非常简单。我们只需向 options 对象添加一个具有适当毫秒数的timeout
属性。我们只指定了 1 毫秒,因此我们希望能够看到请求超时。
- 现在让我们在
catch
方法中处理一个超时:
.catch(ex => {
const error =
ex.code === "ECONNABORTED"
? "A timeout has occurred"
: ex.response.status === 404
? "Resource not found"
: "An unexpected error has occurred";
this.setState({ error });
});
因此,我们检查捕获的错误对象中的code
属性,以确定是否发生了超时。
如果我们查看正在运行的应用程序,我们应该得到超时已发生的确认,超时已发生显示为红色。
- 现在,让我们将超时更改为更合理的值,以便我们可以在下一节中继续了解如何允许用户取消请求:
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
...
timeout: 5000
})
取消请求
允许用户取消请求可以改善我们应用程序中的用户体验。我们将在本节中的axios
帮助下完成此操作:
- 首先,我们将从
axios
导入CancelTokenSource
类型:
import axios, { CancelTokenSource } from "axios";
- 让我们将取消令牌和加载标志添加到我们的状态:
interface IState {
posts: IPost[];
error: string;
cancelTokenSource?: CancelTokenSource;
loading: boolean;
}
- 让我们在构造函数中初始化加载状态:
this.state = {
posts: [],
error: "",
loading: true
};
我们已经将取消令牌定义为可选的,因此不需要在构造函数中初始化它。
- 接下来,我们将生成取消令牌源并将其添加到状态,就在我们发出
GET
请求之前:
public componentDidMount() {
const cancelToken = axios.CancelToken;
const cancelTokenSource = cancelToken.source();
this.setState({ cancelTokenSource });
axios
.get<IPost[]>(...)
.then(...)
.catch(...);
}
- 然后,我们可以在 GET 请求中使用令牌,如下所示:
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
cancelToken: cancelTokenSource.token,
...
})
- 我们可以用
catch
方法处理取消,如下所示。我们还将loading
状态设置为false
:
.catch(ex => {
const error = axios.isCancel(ex)
? "Request cancelled"
: ex.code === "ECONNABORTED"
? "A timeout has occurred"
: ex.response.status === 404
? "Resource not found"
: "An unexpected error has occurred";
this.setState({ error, loading: false });
});
因此,我们使用axios
中的isCancel
函数来检查请求是否已被取消。
- 当我们在
componentDidMount
方法中时,我们也将then
方法中的loading
状态设置为false
:
.then(response => {
this.setState({ posts: response.data, loading: false });
})
- 在
render
方法中,我们添加一个取消按钮,允许用户取消请求:
{this.state.loading && (
<button onClick={this.handleCancelClick}>Cancel</button>
)}
<ul className="posts">...</ul>
- 让我们实现刚才引用的 Cancel 按钮处理程序:
private handleCancelClick = () => {
if (this.state.cancelTokenSource) {
this.state.cancelTokenSource.cancel("User cancelled operation");
}
};
为了取消请求,对 cancel 令牌源调用 cancel 方法。
因此,用户现在可以通过单击“取消”按钮取消请求。
- 现在,这将很难测试,因为我们使用的 RESTAPI 非常快!因此,为了查看已取消的请求,我们在发送请求后立即通过
componentDidMount
方法取消它:
axios
.get<IPost[]>( ... )
.then(response => { ... })
.catch(ex => { ... });
cancelTokenSource.cancel("User cancelled operation");
如果我们查看正在运行的应用程序,我们会看到验证请求是否已被请求取消显示为红色。
因此,axios
通过添加取消请求的功能,可以很容易地改善我们应用程序的用户体验。
在我们继续下一节之前,我们先看看如何使用axios
来创建数据,我们先删除刚才添加的行,以便在请求发出后立即取消请求。
使用 axios 创建数据
现在让我们继续创建数据。我们将允许用户输入文章标题和正文并保存:
- 让我们首先为标题和正文创建一个新状态:
interface IState {
...
editPost: IPost;
}
- 让我们也初始化这个新状态:
public constructor(props: {}) {
super(props);
this.state = {
...,
editPost: {
body: "",
title: "",
userId: 1
}
};
}
- 我们将创建一个
input
和textarea
来捕获用户的帖子标题和正文:
<div className="App">
<div className="post-edit">
<input
type="text"
placeholder="Enter title"
value={this.state.editPost.title}
onChange={this.handleTitleChange}
/>
<textarea
placeholder="Enter body"
value={this.state.editPost.body}
onChange={this.handleBodyChange}
/>
<button onClick={this.handleSaveClick}>Save</button>
</div>
{this.state.loading && (
<button onClick={this.handleCancelClick}>Cancel</button>
)}
...
</div>
- 让我们实现刚刚引用的更改处理程序以更新状态:
private handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
editPost: { ...this.state.editPost, title: e.currentTarget.value }
});
};
private handleBodyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this.setState({
editPost: { ...this.state.editPost, body: e.currentTarget.value }
});
};
- 我们可以在
index.css
中添加一点 CSS,使这一切看起来合理:
.post-edit {
display: flex;
flex-direction: column;
width: 300px;
margin: 0px auto;
}
.post-edit input {
font-family: inherit;
width: 100%;
margin-bottom: 5px;
}
.post-edit textarea {
font-family: inherit;
width: 100%;
margin-bottom: 5px;
}
.post-edit button {
font-family: inherit;
width: 100px;
}
- 我们还可以使用
axios
开始保存点击处理程序和POST
向 REST API 发布新内容:
private handleSaveClick = () => {
axios
.post<IPost>(
"https://jsonplaceholder.typicode.com/posts",
{
body: this.state.editPost.body,
title: this.state.editPost.title,
userId: this.state.editPost.userId
},
{
headers: {
"Content-Type": "application/json"
}
}
)
};
- 我们可以使用
then
方法处理响应:
.then(response => {
this.setState({
posts: this.state.posts.concat(response.data)
});
});
因此,我们将新帖子与现有帖子连接起来,为状态创建一个新的帖子数组。
post
函数调用的结构与get
非常相似。事实上,我们可以添加错误处理、超时和取消请求的能力,就像我们对get
所做的一样。
如果我们在 running 应用程序中添加新帖子并单击 Save 按钮,我们会看到它被添加到帖子列表的底部。
接下来,我们将允许用户更新帖子。
使用 axios 更新数据
现在让我们继续更新数据。我们将允许用户单击现有帖子中的更新按钮进行更改和保存:
- 让我们首先在帖子中的每个列表项中创建一个更新按钮:
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
<button onClick={() => this.handleUpdateClick(post)}>
Update
</button>
</li>
- 现在,我们可以实现更新按钮单击处理程序,该处理程序在组件状态下设置正在编辑的帖子:
private handleUpdateClick = (post: IPost) => {
this.setState({
editPost: post
});
};
- 在我们现有的 save click 处理程序中,我们现在需要两个代码分支来实现现有的
POST
请求和PUT
请求:
private handleSaveClick = () => {
if (this.state.editPost.id) {
// TODO - make a PUT request
} else {
axios
.post<IPost>( ... )
.then( ... );
}
};
- 现在让我们实现
PUT
请求:
if (this.state.editPost.id) {
axios
.put<IPost>(
`https://jsonplaceholder.typicode.com/posts/${
this.state.editPost.id
}`,
this.state.editPost,
{
headers: {
"Content-Type": "application/json"
}
}
)
.then(() => {
this.setState({
editPost: {
body: "",
title: "",
userId: 1
},
posts: this.state.posts
.filter(post => post.id !== this.state.editPost.id)
.concat(this.state.editPost)
});
});
} else {
...
}
因此,我们过滤并连接更新后的帖子,为状态创建一个新的帖子数组。
put
函数调用的结构与get
和post
非常相似。同样,我们可以添加错误处理、超时和取消请求的功能,就像我们对get
所做的一样。
在 running 应用程序中,如果我们单击帖子中的更新按钮,更改标题和正文,然后单击保存按钮,我们会看到它从原来的位置被删除,并以新标题和正文添加到帖子列表的底部。
如果我们想PATCH
一个帖子,我们可以使用patch``axios
方法。这与put
具有相同的结构,但我们不需要传递正在更改的整个对象,只需传递需要更新的值即可。
在下一节中,我们将允许用户删除帖子。
使用 axios 删除数据
现在让我们继续删除数据。我们将允许用户单击现有帖子中的删除按钮来删除它:
- 让我们首先在帖子中的每个列表项中创建一个删除按钮:
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
<button onClick={() => this.handleUpdateClick(post)}>
Update
</button>
<button onClick={() => this.handleDeleteClick(post)}>
Delete
</button>
</li>
- 现在,我们可以创建删除按钮单击处理程序:
private handleDeleteClick = (post: IPost) => {
axios
.delete(`https://jsonplaceholder.typicode.com/posts/${post.id}`)
.then(() => {
this.setState({
posts: this.state.posts.filter(p => p.id !== post.id)
});
});
};
因此,我们使用axios``delete
方法进行 HTTPDELETE
请求,其结构与其他方法相同。
如果我们转到 running 应用程序,我们会在每篇文章中看到一个删除按钮。如果我们单击其中一个按钮,我们将看到它在短时间延迟后从列表中删除
因此,这一节关于axios
和类组件的内容到此结束。我们已经看到,axios
函数比fetch
更简洁,并且能够键入响应、超时和请求取消等功能使其成为许多开发人员的热门选择。在下一节中,我们将把刚刚实现的App
组件重构为功能组件。
将 axios 与功能组件一起使用
在本节中,我们将在函数组件中使用axios
实现 REST API 调用。我们将重构上一节中构建的App
组件:
- 首先,我们将声明一个名为
defaultPosts
的常量,它将保持稍后使用的默认状态。我们将在IPost
接口后添加此项,并将其设置为空数组:
const defaultPosts: IPost[] = [];
- 我们将删除
IState
接口,因为现在状态将被构造为单独的状态。 - 我们还将删除前面的
App
类组件。 - 接下来,让我们在
defaultPosts
常量下启动App
函数组件:
const App: React.SFC = () => {}
- 现在,我们可以为正在编辑的帖子、错误、取消令牌、加载标志和帖子创建状态:
const App: React.SFC = () => {
const [posts, setPosts]: [IPost[], (posts: IPost[]) => void] = React.useState(defaultPosts);
const [error, setError]: [string, (error: string) => void] = React.useState("");
const cancelToken = axios.CancelToken;
const [cancelTokenSource, setCancelTokenSource]: [CancelTokenSource,(cancelSourceToken: CancelTokenSource) => void] = React.useState(cancelToken.source());
const [loading, setLoading]: [boolean, (loading: boolean) => void] = React.useState(false);
const [editPost, setEditPost]: [IPost, (post: IPost) => void] = React.useState({
body: "",
title: "",
userId: 1
});
}
因此,我们使用useState
函数来定义和初始化所有这些状态。
- 我们希望在组件首次安装时调用 RESTAPI 以获取 POST。在定义状态的行之后,我们可以使用
useEffect
函数来传递一个空数组作为第二个参数:
React.useEffect(() => {
// TODO - get posts
}, []);
- 让我们调用 REST API 以获取 arrow 函数中的帖子:
React.useEffect(() => {
axios
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
cancelToken: cancelTokenSource.token,
headers: {
"Content-Type": "application/json"
},
timeout: 5000
});
}, []);
- 让我们处理响应并设置 post 状态,同时将加载状态设置为
false
:
React.useEffect(() => {
axios
.get<IPost[]>(...)
.then(response => {
setPosts(response.data); setLoading(false);
});
}, []);
- 我们还要处理任何错误,将错误状态与加载状态一起设置为
false
:
React.useEffect(() => {
axios
.get<IPost[]>(...)
.then(...)
.catch(ex => {
const err = axios.isCancel(ex)
? "Request cancelled"
: ex.code === "ECONNABORTED"
? "A timeout has occurred"
: ex.response.status === 404
? "Resource not found"
: "An unexpected error has occurred";
setError(err);
setLoading(false);
});
}, []);
- 现在我们可以继续讨论事件处理程序。它们与类组件实现非常相似,用
const
替换private
访问修饰符,用特定的状态变量和状态设置函数替换this.state
和this.setState
。我们将从“取消”按钮单击处理程序开始:
const handleCancelClick = () => {
if (cancelTokenSource) {
cancelTokenSource.cancel("User cancelled operation");
}
};
- 接下来,我们可以为标题和正文输入添加更改处理程序:
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditPost({ ...editPost, title: e.currentTarget.value });
};
const handleBodyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEditPost({ ...editPost, body: e.currentTarget.value });
};
- 下一步是保存按钮单击处理程序:
const handleSaveClick = () => {
if (editPost.id) {
axios
.put<IPost>(
`https://jsonplaceholder.typicode.com/posts/${editPost.id}`,
editPost,
{
headers: {
"Content-Type": "application/json"
}
}
)
.then(() => {
setEditPost({
body: "",
title: "",
userId: 1
});
setPosts(
posts.filter(post => post.id !== editPost.id).concat(editPost)
);
});
} else {
axios
.post<IPost>(
"https://jsonplaceholder.typicode.com/posts",
{
body: editPost.body,
title: editPost.title,
userId: editPost.userId
},
{
headers: {
"Content-Type": "application/json"
}
}
)
.then(response => {
setPosts(posts.concat(response.data));
});
}
};
- 接下来,让我们执行更新按钮:
const handleUpdateClick = (post: IPost) => {
setEditPost(post);
};
- 最后一个处理程序用于删除按钮:
const handleDeleteClick = (post: IPost) => {
axios
.delete(`https://jsonplaceholder.typicode.com/posts/${post.id}`)
.then(() => {
setPosts(posts.filter(p => p.id !== post.id));
});
};
- 我们的最终任务是实现 return 语句。同样,这与类组件
render
方法非常相似,删除了对this
的引用:
return (
<div className="App">
<div className="post-edit">
<input
type="text"
placeholder="Enter title"
value={editPost.title}
onChange={handleTitleChange}
/>
<textarea
placeholder="Enter body"
value={editPost.body}
onChange={handleBodyChange}
/>
<button onClick={handleSaveClick}>Save</button>
</div>
{loading && <button onClick={handleCancelClick}>Cancel</button>}
<ul className="posts">
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
<button onClick={() => handleUpdateClick(post)}>Update</button>
<button onClick={() => handleDeleteClick(post)}>Delete</button>
</li>
))}
</ul>
{error && <p className="error">{error}</p>}
</div>
);
就这样!我们与 RESTAPI 交互的功能组件已经完成。如果我们尝试这样做,它的行为应该和以前完全一样。
REST API 交互方面的主要区别在于,我们使用useEffect
函数进行 REST API 调用,以获取需要呈现的数据。我们在安装组件时仍然会这样做,就像在基于类的组件中一样。这只是利用组件生命周期事件的另一种方式。
总结
基于回调的异步代码可能很难读取和维护。谁花了几个小时试图找出基于回调的异步代码中错误的根本原因?或者只是花了几个小时试图理解一段基于回调的异步代码试图做什么?谢天谢地,我们现在有了编写异步代码的替代方法。
与基于回调的异步代码相比,基于承诺的函数是一个很大的改进,因为代码可读性更高,错误处理也更容易。async
和await
关键字可以说使读取异步代码比基于承诺的函数代码更容易,因为它非常接近同步等价物的样子。
现代浏览器有一个称为fetch
的功能,用于与 REST API 交互。这是一个基于承诺的功能,允许我们轻松提出请求并很好地管理响应。
axios
是fetch
的流行替代品。该 API 可以说更干净,允许我们更好地处理 HTTP 错误代码。超时和取消请求也可以使用axios
非常简单。axios
也对 TypeScript 友好,将类型烘焙到库中。玩过axios
和fetch
之后,你最喜欢哪一款
我们可以在基于类和基于函数的组件中与 RESTAPI 交互。当调用 RESTAPI 以获取要在第一个组件呈现中显示的数据时,我们需要等到组件刚装入之后。在类组件中,我们使用componentDidMount
生命周期方法来实现这一点。在 function components 中,我们使用useEffect
函数来实现这一点,传递一个空数组作为第二个参数。在这两种类型的组件中都有与 RESTAPI 交互的经验,您将在下一个 React 和 TypeScript 项目中使用哪种组件类型?
RESTAPI 并不是我们可能需要与之交互的唯一 API 类型。GraphQL 是一种流行的替代 API 服务器。在下一章中,我们将学习如何与 GraphQL 服务器交互。
问题
让我们回答以下问题,以帮助我们了解刚刚学到的知识:
- 如果在浏览器中运行以下代码,控制台中的输出将是什么?
try {
setInterval(() => {
throw new Error("Oops");
}, 1000);
} catch (ex) {
console.log("Sorry, there is a problem", ex);
}
- 假设 post
9999
不存在,如果我们在浏览器中运行以下代码,控制台中的输出会是什么?
fetch("https://jsonplaceholder.typicode.com/posts/9999")
.then(response => {
console.log("HTTP status code", response.status);
return response.json();
})
.then(data => console.log("Response body", data))
.catch (error => console.log("Error", error));
- 如果我们对
axios
做了类似的练习,那么在运行以下代码时控制台中的输出是什么?
axios
.get("https://jsonplaceholder.typicode.com/posts/9999")
.then(response => {
console.log("HTTP status code", response.status);
})
.catch(error => {
console.log("Error", error.response.status);
});
- 使用本机
fetch
而不是axios
有什么好处? - 我们如何向以下
axios
请求添加承载令牌?
axios.get("https://jsonplaceholder.typicode.com/posts/1")
- 我们正在使用以下
axios``PUT
请求更新帖子标题?
axios.put("https://jsonplaceholder.typicode.com/posts/1", {
title: "corrected title",
body: "some stuff"
});
- 身体没有改变,虽然它只是我们想要更新的标题。我们如何才能将其更改为
PATCH
请求,以使此 REST 呼叫更有效? - 我们已经实现了一个功能组件来显示帖子。它使用以下代码从 RESTAPI 获取 post?
React.useEffect(() => {
axios
.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(...)
.catch(...);
});
前面的代码有什么问题?
进一步阅读
以下链接是有关本章所述主题的详细信息的良好资源:
- 有关承诺的更多信息,请访问https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- 关于
async
和await
的其他信息见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function - 有关
fetch
功能的更多信息,请参见https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API axios
GitHub 页面位于https://github.com/axios/axios
版权属于:月萌API www.moonapi.com,转载请注明出处