React 和 TypeScript3 学习手册(五)
基于回调的异步代码可能很难阅读和维护。谁花了几个小时来追踪回调式异步代码中错误的根本原因?或者只是花了几个小时来理解一段回调式异步代码试图做什么?幸运的是,现在我们有了编写异步代码的替代方法。基于 Promise 的函数比基于回调的异步代码有了很大的改进,因为代码更易读,错误处理也更容易。async和await关键字可以说比基于 Promise 的函数代码更容易阅读异步代码,因为它非常接近同步等效
原文:
zh.annas-archive.org/md5/9ec979022a994e15697a4059ac32f487
译者:飞龙
第九章:与 RESTful API 交互
与 RESTful API 交互是构建应用程序时我们需要做的非常常见的任务,它总是导致我们必须编写异步代码。因此,在本章的开始,我们将详细了解一般的异步代码。
有许多库可以帮助我们与 REST API 交互。在本章中,我们将看看原生浏览器函数和一个流行的开源库来与 REST API 交互。我们将发现开源库相对于原生函数的额外功能。我们还将看看如何在 React 类和基于函数的组件中与 REST API 交互。
在本章中,我们将学习以下主题:
-
编写异步代码
-
使用 fetch
-
使用 axios 与类组件
-
使用 axios 与函数组件
技术要求
在本章中,我们使用以下技术:
-
TypeScript playground:这是一个网站,位于
www.typescriptlang.org/play/
,允许我们在不安装任何东西的情况下玩耍异步代码。 -
Node.js 和
npm
:TypeScript 和 React 依赖于这些。我们可以从nodejs.org/en/download/
安装这些。如果我们已经安装了这些,请确保npm
至少是 5.2 版本。 -
TypeScript:可以通过终端中的以下命令使用
npm
安装:
npm install -g typescript
-
Visual Studio Code。我们需要一个编辑器来编写我们的 React 和 TypeScript 代码,可以从
code.visualstudio.com/
安装。我们还需要在 Visual Studio Code 中安装 TSLint (by egamma) 和 Prettier (by Estben Petersen) 扩展。 -
jsonplaceholder.typicode.com
:我们将使用这个在线服务来帮助我们学习如何与 RESTful API 交互。
本章中的所有代码片段都可以在github.com/carlrip/LearnReact17WithTypeScript/tree/master/09-RestfulAPIs.
上找到
编写异步代码
TypeScript 代码默认是同步执行的,每行代码都会依次执行。然而,TypeScript 代码也可以是异步的,这意味着事情可以独立于我们的代码发生。调用 REST API 就是异步代码的一个例子,因为 API 请求是在我们的 TypeScript 代码之外处理的。因此,与 REST API 交互会迫使我们编写异步代码。
在本节中,我们将花时间了解在编写异步代码时可以采取的方法,然后再使用它们与 RESTful API 进行交互。我们将在下一节开始时看一下回调函数。
回调函数
回调是我们将作为参数传递给异步函数的函数,在异步函数完成时调用。在下一节中,我们将通过一个使用回调的异步代码示例进行说明。
回调执行
让我们在 TypeScript 播放器中通过一个使用回调的异步代码示例来进行说明。让我们输入以下代码:
let firstName: string;
setTimeout(() => {
firstName = "Fred";
console.log("firstName in callback", firstName);
}, 1000);
console.log("firstName after setTimeout", firstName);
该代码调用了 JavaScript 的setTimeout
函数,这是一个异步函数。它以回调作为第一个参数,并以执行应等待的毫秒数作为第二个参数。
我们使用箭头函数作为回调函数,在其中将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
来尝试回调。这次,在回调函数内抛出一个错误。我们希望使用try / catch
来捕获回调外部的错误,围绕setTimeout
函数。
如果我们运行代码,我们会发现我们没有捕获错误:
- 我们必须在回调函数内处理错误。因此,让我们将我们的示例调整为以下内容:
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
接口为我们提供了对结果变量
的良好类型安全性。
如果我们运行这段代码,我们将看到我们成功处理了错误:
因此,处理错误以及读取基于回调的代码是一个挑战。幸运的是,有替代方法来处理这些挑战,我们将在接下来的部分中介绍。
承诺
promise 是一个 JavaScript 对象,它代表异步操作的最终完成(或失败)及其结果值。接下来,我们将看一个消耗基于 promise 的函数的示例,然后创建我们自己的基于 promise 的函数。
消耗基于 promise 的函数
让我们快速看一下一些暴露了基于 promise 的 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
方法中做任何额外的工作来处理错误。因此,这比使用基于回调的异步代码要好得多。
在下一节中,我们将创建我们自己的基于 promise 的函数。
创建一个基于 promise 的函数
在本节中,我们将创建一个wait
函数,以异步等待传递的毫秒数:
- 让我们在 TypeScript playground 中输入以下内容:
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
来进行实际的等待
- 让我们消费我们基于 promise 的
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
方法被执行:
因此,promise 给了我们一种很好的编写异步代码的方式。然而,在本书的早期我们已经使用了另一种方法。我们将在下一节中介绍这种方法。
异步和等待
async
和await
是两个 JavaScript 关键字,我们可以使用它们使异步代码的阅读几乎与同步代码相同:
- 让我们看一个例子,消费我们在上一节中创建的
wait
函数,将以下代码输入到 TypeScript playground 中,放在wait
函数声明之后:
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
REST API 进行交互。
使用 fetch 获取数据
在本节中,我们将使用fetch
从JSONPlaceholder
REST API 获取一些帖子,从基本的GET
请求开始。
基本的 GET 请求
让我们打开 TypeScript playground 并输入以下内容:
fetch("https://jsonplaceholder.typicode.com/posts")
.then(response => response.json())
.then(data => console.log(data));
以下是一些关键点:
-
fetch
函数中的第一个参数是请求的 URL -
fetch
是一个基于承诺的函数 -
第一个
then
方法处理响应 -
第二个
then
方法处理当响应体已解析为 JSON 时
如果我们运行代码,应该会在控制台输出一个帖子数组:
获取响应状态
我们经常需要检查请求的状态。我们可以这样做:
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 和 false 的输出。
处理错误
使用基于承诺的函数,我们在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 创建一些数据。
基本的 POST 请求
通过 REST API 创建数据通常涉及使用 HTTP POST
方法,并将要创建的数据放在请求体中。
让我们打开 TypeScript playground 并输入以下内容:
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 和包含生成的帖子 ID 的对象。
请求 HTTP 标头
我们经常需要在请求中包含 HTTP 标头。我们可以在options
对象中的headers
属性中指定这些内容:
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 方法,而不仅仅是 HTTP POST
。例如,我们可以用于GET
请求如下:
fetch("https://jsonplaceholder.typicode.com/posts/1", {
headers: {
"Content-Type": "application/json",
Authorization: "bearer some-bearer-token"
}
}).then(...);
因此,这就是如何使用fetch
向 REST API 发布数据。在下一节中,我们将看看如何更改数据。
使用 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));
因此,进行 HTTP PUT
的fetch
调用的结构与POST
请求非常相似。唯一的区别是我们在选项对象中指定method
属性为PUT
。
如果我们运行上述代码,我们将得到 200 和更新的POST
对象输出到控制台。
基本的 PATCH 请求
一些 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 和更新的帖子对象输出到控制台。
因此,这就是如何使用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 Code 和它的终端,并输入以下命令来创建一个新的 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
,会有一个 linting 错误。所以,让我们通过在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> { ... }
- 然后在构造函数中将帖子状态初始化为空数组:
class App extends React.Component<{}, IState> {
public constructor(props: {}) {
super(props);
this.state = {
posts: []
};
}
}
- 从 REST API 获取数据时,通常会在
componentDidMount
生命周期方法中进行。所以,让我们使用axios
来获取我们的帖子:
public componentDidMount() {
axios
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts")
.then(response => {
this.setState({ posts: response.data });
});
}
-
我们使用
axios
中的get
函数来获取数据,这是一个类似于fetch
的基于 Promise 的函数 -
这是一个通用函数,它接受响应主体类型作为参数
-
我们将我们请求的 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;
}
如果我们查看正在运行的应用程序,它现在将如下所示:
因此,使用axios
进行基本的GET
请求非常简单。我们需要在类组件中使用componentDidMount
生命周期方法,以便进行 REST API 调用,该调用将从响应中呈现数据。
但是我们如何处理错误呢?我们将在下一节中介绍这一点。
处理错误
- 让我们调整我们的请求中的 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>}
- 让我们现在将刚刚引用的
error
CSS 类添加到index.css
中:
.error {
color: red;
}
如果我们现在查看正在运行的应用程序,我们将看到红色的资源未找到。
- 现在让我们将 URL 更改为有效的 URL,以便我们可以继续查看如何在下一节中包含 HTTP 标头:
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts")
因此,使用axios
处理 HTTP 错误与使用fetch
不同。我们在fetch
的第一个then
方法中处理它们,而我们在axios
的catch
方法中处理它们。
请求 HTTP 标头
为了在请求中包含 HTTP 标头,我们需要向get
函数添加第二个参数,该参数可以包含各种选项,包括 HTTP 标头。
让我们在我们的请求中添加一个内容类型的 HTTP 标头:
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
headers: {
"Content-Type": "application/json"
}
})
因此,我们在一个名为headers
的属性中的对象中定义了 HTTP 标头。
如果我们查看正在运行的应用程序,它将完全相同。JSONPlaceholder REST API 不需要内容类型,但我们与之交互的其他 REST API 可能需要。
在下一节中,我们将看看在fetch
函数中很难实现的一些东西,即在请求上指定超时的能力。
超时
在一定时间后超时请求可以改善我们应用的用户体验:
- 让我们给我们的请求添加一个超时:
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
headers: {
"Content-Type": "application/json"
},
timeout: 1
})
因此,向axios
请求添加超时非常简单。我们只需在选项对象中添加一个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>
- 让我们实现刚刚引用的取消按钮处理程序:
private handleCancelClick = () => {
if (this.state.cancelTokenSource) {
this.state.cancelTokenSource.cancel("User cancelled operation");
}
};
为了取消请求,在取消令牌源上调用取消方法。
所以,用户现在可以通过点击取消按钮来取消请求。
- 现在,这将很难测试,因为我们正在使用的 REST API 非常快!因此,为了看到一个被取消的请求,让我们在
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
一样添加错误处理、超时和取消请求的能力。
如果我们在运行的应用程序中添加一个新帖子并单击“保存”按钮,我们会看到它添加到帖子列表的底部。
接下来,我们将允许用户更新帖子。
使用 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
});
};
- 在我们现有的保存点击处理程序中,我们现在需要为现有的
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
做的那样。
在运行的应用程序中,如果我们点击帖子中的“更新”按钮,更改标题和正文,然后点击“保存”按钮,我们会看到它从原来的位置移除,并以新的标题和正文添加到帖子列表的底部。
如果我们想要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
方法来发出 HTTP 的DELETE
请求,其结构与其他方法相同。
如果我们进入运行的应用程序,我们应该在每个帖子中看到一个删除按钮。如果我们点击其中一个按钮,我们会在短暂的延迟后看到它从列表中移除。
因此,这就结束了关于使用类组件的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
函数来定义和初始化所有这些状态片段。
- 当组件首次挂载时,我们希望进行 REST API 调用以获取帖子。在状态定义的行之后,我们可以使用
useEffect
函数,将空数组作为第二个参数进行这样的操作:
React.useEffect(() => {
// TODO - get posts
}, []);
- 让我们在箭头函数中调用 REST API 以获取帖子:
React.useEffect(() => {
axios
.get<IPost[]>("https://jsonplaceholder.typicode.com/posts", {
cancelToken: cancelTokenSource.token,
headers: {
"Content-Type": "application/json"
},
timeout: 5000
});
}, []);
- 让我们处理响应并设置帖子状态,同时将加载状态设置为
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));
});
};
- 我们的最后任务是实现返回语句。同样,这与类组件的
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>
);
就是这样!我们与 REST API 交互的函数组件已经完成。如果我们尝试这样做,它应该与以前的行为完全一样。
在与 REST API 交互方面的主要区别在于,我们使用useEffect
函数来进行 REST API 调用以获取需要呈现的数据。当组件已挂载时,我们仍然会这样做,就像在基于类的组件中一样。这只是一种不同的方式来利用组件的生命周期事件。
总结
基于回调的异步代码可能很难阅读和维护。谁花了几个小时来追踪回调式异步代码中错误的根本原因?或者只是花了几个小时来理解一段回调式异步代码试图做什么?幸运的是,现在我们有了编写异步代码的替代方法。
基于 Promise 的函数比基于回调的异步代码有了很大的改进,因为代码更易读,错误处理也更容易。async
和await
关键字可以说比基于 Promise 的函数代码更容易阅读异步代码,因为它非常接近同步等效代码的样子。
现代浏览器有一个名为fetch
的很好的函数,用于与 REST API 进行交互。这是一个基于 Promise 的函数,允许我们轻松地发出请求并很好地管理响应。
axios
是fetch
的一种流行替代品。该 API 可以说更清晰,并且允许我们更好地处理 HTTP 错误代码。使用axios
也可以非常简单地处理超时和取消请求。axios
也非常友好于 TypeScript,因为类型已经内置到库中。在使用过axios
和fetch
之后,你更喜欢哪一个?
我们可以在类组件和函数组件中与 REST API 进行交互。当调用 REST API 以获取数据以在第一个组件渲染中显示时,我们需要等到组件挂载后。在类组件中,我们使用componentDidMount
生命周期方法来实现这一点。在函数组件中,我们使用useEffect
函数,将空数组作为第二个参数传递。在两种类型的组件中都有与 REST API 交互的经验后,你会在下一个 React 和 TypeScript 项目中使用哪种组件类型?
REST API 并不是我们可能需要交互的唯一类型的 API。GraphQL 是一种流行的替代 API 服务器。我们将在下一章学习如何与 GraphQL 服务器交互。
问题
让我们回答以下问题,以帮助我们对刚学到的知识有更深刻的理解:
- 如果我们在浏览器中运行以下代码,控制台会输出什么?
try {
setInterval(() => {
throw new Error("Oops");
}, 1000);
} catch (ex) {
console.log("Sorry, there is a problem", ex);
}
- 假设帖子
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
请求中添加一个 Bearer 令牌?
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 调用更有效? -
我们已经实现了一个函数组件来显示一个帖子。它使用以下代码从 REST API 获取帖子?
React.useEffect(() => {
axios
.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
.then(...)
.catch(...);
});
上述代码有什么问题?
进一步阅读
以下链接是本章涵盖的主题的进一步信息的好资源:
-
有关 promises 的更多信息可以在
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
找到 -
有关
async
和await
的其他信息可以在developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
找到 -
有关
fetch
函数的更多信息可以在developer.mozilla.org/en-US/docs/Web/API/Fetch_API
找到 -
axios
的 GitHub 页面在github.com/axios/axios
上
第十章:与 GraphQL API 交互
GraphQL 是由 Facebook 维护的用于读取和写入数据的开源 Web API 语言。它允许客户端指定返回的数据,并在单个请求中请求多个数据区域。这种效率和灵活性使其成为 REST API 的一个引人注目的替代方案。GraphQL 还支持读取和写入数据。
在本章中,我们将开始尝试针对 GitHub 进行一些 GraphQL 查询,以熟悉使用GitHub GraphQL API资源管理器的语法。我们将探讨如何读取和写入 GraphQL 数据,以及如何精确指定我们希望在响应中返回的数据方式。
然后,我们将在 React 和 TypeScript 应用程序中使用 GitHub GraphQL 服务器,构建一个小应用程序,该应用程序搜索 GitHub 存储库并返回有关其的一些信息。我们将使用上一章关于axios
的知识与 GitHub GraphQL 服务器进行交互。然后我们将转而使用 Apollo,这是一个使与 GraphQL 服务器交互变得轻而易举的客户端库。
在本章中,我们将涵盖以下主题:
-
GraphQL 查询和变异语法
-
使用 axios 作为 GraphQL 客户端
-
使用 Apollo GraphQL 客户端
-
在 Apollo 中使用缓存数据
技术要求
在本章中,我们使用以下技术:
-
Node.js 和
npm
:TypeScript 和 React 依赖于这些。我们可以从nodejs.org/en/download/
安装它们。如果我们已经安装了这些,请确保npm
至少是 5.2 版本。 -
Visual Studio Code:我们需要一个编辑器来编写 React 和 TypeScript 代码,可以从
code.visualstudio.com/
安装。我们还需要在 Visual Studio Code 中安装 TSLint (by egamma) 和 Prettier (by Estben Petersen) 扩展。 -
GitHub:我们需要一个 GitHub 账户。如果我们还没有账户,可以在以下链接注册:
github.com/join
。 -
GitHub GraphQL API Explorer:我们将使用此工具来玩转 GraphQL 查询和变异的语法。该工具位于
developer.github.com/v4/explorer/
。
本章中的所有代码片段都可以在github.com/carlrip/LearnReact17WithTypeScript/tree/master/10-GraphAPIs
上找到。
GraphQL 查询和变异语法
在本节中,我们将使用 GitHub GraphQL API 资源浏览器开始熟悉与 GraphQL 服务器交互的语法,从下一节开始阅读数据。
阅读 GraphQL 数据
为了读取 GraphQL 数据,我们进行所谓的查询。在本节中,我们将首先介绍基本的 GraphQL 语法,然后讨论如何在查询结果中包含嵌套对象,以及如何通过允许传递参数来创建可重用的查询。
基本查询
在本节中,我们将使用 GitHub GraphQL API 资源浏览器来获取有关我们的 GitHub 用户帐户的信息:
- 让我们在浏览器中打开以下 URL 以打开工具:
developer.github.com/v4/explorer/
。
如果我们还没有登录 GitHub 帐户,我们将需要登录。
- 在左上角的面板中,让我们输入以下内容,然后点击执行查询按钮:
query {
viewer {
name
}
}
这是我们的第一个 GraphQL 查询。以下是一些关键点:
-
我们使用
query
关键字作为查询的前缀。这实际上是可选的。 -
viewer
是我们想要获取的对象的名称。 -
name
是我们想要返回的viewer
中的一个字段。
查询结果将显示在右侧:
我们请求的数据以 JSON 对象的形式返回。JSON 包含一个包含name
字段的viewer
对象的data
对象。name
的值应该是我们的名字,因为这是存储在我们的 GitHub 帐户中的名字。
- 在结果窗格的右侧有一个文档链接。如果我们点击这个链接,会出现一个文档资源浏览器:
如果我们点击查询链接,将显示可以查询的所有对象,包括viewer
,这是我们刚刚查询的对象。如果我们点击进入这个对象,我们将看到viewer
中可用的所有字段。
- 让我们将
avatarUrl
添加到我们的查询中,因为这是我们可以使用的另一个字段:
query {
viewer {
name
avatarUrl
}
}
因此,我们只需在name
和avatarUrl
字段之间加上一个换行符,将avatarUrl
字段添加到viewer
对象中。如果我们执行查询,我们将看到avatarUrl
添加到 JSON 结果中。这应该是我们的图像的路径。
因此,我们已经看到了 GraphQL 的灵活性,可以精确指定我们希望在响应中返回哪些字段。在下一节中,我们将进一步指定我们希望返回的嵌套对象。
返回嵌套数据
让我们在本节中进行更复杂的查询。我们将搜索 GitHub 存储库,返回有关它的信息,包括它拥有的星星数量以及最近提出的问题作为嵌套数组:
- 让我们开始输入以下查询并执行它:
query {
repository (owner:"facebook", name:"react") {
name
description
}
}
这次,我们要求repository
对象,但传递了owner
和name
存储库的两个参数。我们要求返回存储库的name
和description
。
我们看到返回了我们请求的存储库和字段:
- 现在让我们请求存储库的星星数量。为此,我们要求
stargazers
嵌套对象中的totalCount
字段:
query {
repository (owner:"facebook", name:"react") {
name
description
stargazers {
totalCount
}
}
}
如果我们执行查询,我们会看到返回的结果:
- 现在让我们给
stargazers
中的totalCount
添加一个别名:
stargazers {
stars:totalCount
}
如果我们执行查询,我们会看到星星数量返回到我们指定的别名:
{
"data": {
"repository": {
"name": "react",
"description": "A declarative, efficient, and flexible JavaScript library for building user interfaces.",
"stargazers": {
"stars": 114998
}
}
}
}
- 让我们继续请求存储库中的最后
5
个问题:
{
repository (owner:"facebook", name:"react") {
name
description
stargazers {
stars:totalCount
}
issues(last: 5) {
edges {
node {
id
title
url
publishedAt
}
}
}
}
}
我们通过将5
传递到最后一个参数来请求issues
对象。然后,我们请求包含我们感兴趣的问题字段的edges
对象中的node
对象。
那么,edges
和node
对象是什么?为什么我们不能直接请求我们想要的字段?嗯,这种结构是为了方便基于游标的分页。
如果我们执行查询,我们会得到结果中包含的最后5
个问题。
因此,GraphQL 允许我们为不同的数据部分进行单个网络请求,只返回我们需要的字段。使用 GitHub REST API 进行类似的操作可能需要多个请求,并且我们会得到比我们需要的更多的数据。在这些类型的查询中,GraphQL 比 REST 更出色。
查询参数
我们刚刚进行的查询是硬编码的,用于获取特定存储库的数据。在本节中,我们将在查询中定义变量,这些变量基本上允许将参数传递给它:
- 我们可以在
query
关键字后的括号中添加查询变量,用逗号分隔。每个参数都通过在分号后声明其类型来定义其名称。这类似于在 TypeScript 函数中使用类型注释定义参数。变量名需要以$
为前缀。类型后面的!
表示这是必需的。因此,在我们的情况下,为了执行查询,这两个变量都是必需的。然后可以在查询中引用这些变量,在我们的例子中,这是我们请求存储库对象的地方:
query ($org: String!, $repo: String!) {
repository (owner:$org, name:$repo) {
...
}
}
- 在执行查询之前,我们需要指定变量值。我们在左下角的查询变量窗格中以 JSON 对象的形式进行此操作:
{
"org": "facebook",
"repo": "react"
}
- 如果我们执行查询,我们将得到我们请求的存储库的结果:
我们现在已经开始习惯从 GraphQL 服务器中读取数据。但是我们如何创建新的数据项或更新数据呢?我们将在下一节中找到答案。
编写 GraphQL 数据
现在让我们把注意力转向写入 GraphQL 服务器。我们可以通过所谓的 mutations 来实现这一点。在本节中,我们将创建一个mutation
来向存储库添加 GitHub 星标:
- 为了收藏一个存储库,我们需要存储库的
id
。因此,让我们将这个添加到我们一直在工作的查询中:
query ($org: String!, $repo: String!) {
repository (owner:$org, name:$repo) {
id
...
}
}
- 让我们复制结果中返回的
id
。React 存储库的id
如下所示:
MDEwOlJlcG9zaXRvcnkxMDI3MDI1MA==
- 现在我们可以写我们的第一个
mutation
:
mutation ($repoId: ID!) {
addStar(input: { starrableId: $repoId }) {
starrable {
stargazers {
totalCount
}
}
}
}
以下是关于这个mutation
的一些关键点:
-
我们用
mutation
关键字作为前缀来定义一个 mutation。 -
我们将要传递给
mutation
的参数放在mutation
关键字后面的括号中。在我们的例子中,我们为要收藏的存储库id
设置了一个参数。 -
addStar
是我们正在调用的mutation
函数,它有一个名为input
的参数,我们需要传递给它。 -
input
实际上是一个对象,其中包含一个名为starrableId
的字段,我们需要包含它。其值是我们要收藏的存储库id
,因此我们将其设置为我们的存储库id
变量$repoId
。 -
在
mutation
参数之后,我们可以指定我们希望在响应中返回什么。在我们的例子中,我们希望返回存储库上的星星数量。
- 我们可以在查询变量窗格中指定存储库
id
的参数值:
{
"repoId": "MDEwOlJlcG9zaXRvcnkxMDI3MDI1MA=="
}
- 如果我们执行
mutation
,星星将被添加到存储库中,并且新的总星星数量将被返回:
现在我们对 GraphQL 查询和变异都有了很好的掌握。在下一节中,我们将开始从 React 和 TypeScript 应用程序与 GraphQL 服务器进行交互。
使用 axios 作为 GraphQL 客户端
与 GraphQL 服务器的交互是通过 HTTP 完成的。我们在第九章中学到,与 Restful API 交互,axios
是一个很好的 HTTP 客户端。因此,在本章中,我们将介绍如何使用axios
与 GraphQL 服务器进行交互。
为了帮助我们学习,我们将创建一个 React 和 TypeScript 应用程序来返回有关我们 GitHub 帐户的信息。因此,我们的第一个任务是获取一个令牌,以便我们可以访问查询 GitHub GraphQL 服务器并搭建一个 React 和 TypeScript 应用程序。
生成 GitHub 个人访问令牌
GitHub GraphQL 服务器需要一个令牌才能与其进行交互。所以,让我们去生成一个个人访问令牌:
-
让我们登录到我们的 GitHub 帐户,并通过打开头像下的菜单并选择设置来进入我们的设置页面。
-
在左侧菜单中,我们需要选择开发者设置选项。这将带我们到开发者设置页面。
-
然后我们可以在左侧菜单中选择个人访问令牌选项。
-
然后我们将看到一个生成新令牌的按钮,我们可以点击它来生成我们的令牌。点击按钮后,我们可能会被提示输入密码。
-
在生成令牌之前,我们被要求指定范围。让我们输入一个令牌描述,选中 repo 和 user,然后点击生成令牌按钮。
-
然后生成的令牌将显示在页面上供我们复制并在我们的 React 应用程序中使用。
既然我们有了我们的令牌,让我们在下一节中搭建一个 React 和 TypeScript 应用程序。
创建我们的应用程序
我们将按照通常的步骤来搭建一个 React 和 TypeScript 应用程序:
- 让我们在我们选择的文件夹中打开 Visual Studio Code 并打开终端。让我们输入以下命令来创建一个新的 React 和 TypeScript 项目:
npx create-react-app repo-search --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 repo-search
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
,会出现一个 linting 错误。所以,让我们通过在render
方法上添加public
作为修饰符来解决这个问题:
class App extends Component {
public render() {
return ( ... );
}
}
- 现在我们可以使用
npm
安装axios
:
npm install axios
- 在继续开发之前,让我们先启动我们的应用程序:
npm start
- 在我们使用
axios
进行第一个 GraphQL 查询之前,让我们在src
目录中创建一个名为Header.tsx
的新文件,其中包含以下import
:
import React from "react";
import axios from "axios";
这个组件最终将包含我们从 GitHub 获取的姓名和头像。
- 暂时让我们的
Header
组件返回空值:
export const Header: React.SFC = () => {
return null;
}
- 现在让我们回到
App.tsx
,并导入我们刚刚创建的Header
组件:
import { Header } from "./Header";
- 现在我们可以调整
App.tsx
中的 JSX,包括我们的Header
组件:
<div className="App">
<header className="App-header">
<Header />
</header>
</div>
- 作为本节的最后一个任务,让我们在
App.css
中更改App-Header
的 CSS 类,以便标题不那么高:
.App-header {
background-color: #282c34;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 16px;
color: white;
}
查询 GraphQL 服务器
现在我们已经有了我们的 React 和 TypeScript 项目,让我们使用axios
进行 GraphQL 查询:
- 在
Header.tsx
中,我们将首先为 GraphQL 查询响应和其中的 viewer 数据创建两个接口:
interface IViewer {
name: string;
avatarUrl: string;
}
interface IQueryResult {
data: {
viewer: IViewer;
};
}
- 让我们在
Header
组件中创建一些状态变量用于viewer
:
const [viewer, setViewer]: [
IViewer,
(viewer: IViewer) => void
] = React.useState({name: "", avatarUrl: ""});
- 现在是时候进行 GraphQL 查询了。我们将在组件刚刚挂载时进行这个操作。我们可以使用
useEffect
函数来实现这一点:
React.useEffect(() => {
// TODO - make a GraphQL query
}, []);
我们将一个空数组作为第二个参数传递,这样查询只会在组件挂载时执行,而不是在每次渲染时执行。
- 然后让我们使用
axios
进行 GraphQL 查询:
React.useEffect(() => {
axios
.post<IQueryResult>(
"https://api.github.com/graphql",
{
query: `query {
viewer {
name
avatarUrl
}
}`
}
)
}, []);
请注意,尽管我们正在读取数据,但我们正在进行 HTTP POST
。GraphQL 要求我们使用 HTTP POST
,因为查询的细节在请求体中。
我们还在使用之前使用的接口IQueryResult
来处理响应数据。
- 如前所述,我们需要在 HTTP 授权标头中传递我们的令牌。所以,让我们这样做:
axios
.post<IQueryResult>(
"https://api.github.com/graphql",
{
query: `query {
viewer {
name
avatarUrl
}
}`
},
{
headers: {
Authorization: "bearer our-bearer-token"
}
}
)
显然,我们需要用我们之前从 GitHub 获取的真实令牌来替换。
- 我们还没有处理响应,所以让我们设置
viewer
状态变量:
axios
.post<IQueryResult>(
...
)
.then(response => {
setViewer(response.data.data.viewer);
});
- 现在我们已经从 GraphQL 查询中获取了数据,让我们渲染我们的头像和姓名以及我们的应用程序标题:
return (
<div>
<img src={viewer.avatarUrl} className="avatar" />
<div className="viewer">{viewer.name}</div>
<h1>GitHub Search</h1>
</div>
);
- 让我们将刚刚引用的头像 CSS 类添加到
App.css
中:
.avatar {
width: 60px;
border-radius: 50%;
}
如果我们查看正在运行的应用程序,应该在应用程序标题中看到我们的头像和姓名:
因此,我们刚刚使用了一个 HTTP 库与 GraphQL 服务器进行交互。所有 GraphQL 请求都是使用 HTTP POST 方法进行的,即使是用于读取数据的请求也是如此。所有 GraphQL 请求也都是发送到同一个端点。我们想要从中获取数据的资源不在 URL 中,而是在请求体中。因此,虽然我们可以使用 HTTP 库,比如axios
,来查询 GraphQL 服务器,但感觉有点奇怪。
在下一节中,我们将看一下一个 GraphQL 客户端,它将帮助我们以更自然的方式查询 GraphQL 服务器。
使用 Apollo GraphQL 客户端
Apollo 客户端是一个用于与 GraphQL 服务器交互的客户端库。它比使用通用 HTTP 库如axios
有许多优点,比如能够在我们的 JSX 中以声明方式读写数据,并且开箱即用地启用缓存。
在本节中,我们将重构上一节中使用axios
构建的内容,以使用 Apollo,并且稍微扩展我们的应用程序以包括 GitHub 仓库搜索。
安装 Apollo 客户端
我们的第一项工作是将 Apollo 安装到我们的项目中。
- 要将 Apollo 客户端添加到我们的项目中,让我们通过
npm
安装以下包:
npm install apollo-boost react-apollo graphql
-
apollo-boost
包含了我们设置 Apollo 客户端所需的一切 -
react-apollo
包含了我们将用来与 GraphQL 服务器交互的 React 组件 -
graphql
是一个核心包,我们将用它来解析 GraphQL 查询
- 我们还将为
graphql
安装一些 TypeScript 类型:
npm install @types/graphql --save-dev
- 我们需要确保 TypeScript 在编译我们的代码时包含
es2015
和esNext
库。因此,让我们在tsconfig.json
中添加以下lib
字段:
{
"compilerOptions": {
"target": "es5",
"lib": ["es2015", "dom", "esnext"],
...
},
...
}
现在我们已经准备好开始使用 Apollo 与 GitHub GraphQL 服务器进行交互了。
从 axios 迁移到 Apollo
现在我们已经安装了所有 Apollo 的部分,让我们将我们的axios
代码迁移到 Apollo。
添加 Apollo 提供程序
我们将从App.tsx
开始,在那里我们将定义我们的 Apollo 客户端并提供给App
组件层次结构下的所有组件:
- 在
App.tsx
中,让我们导入apollo-boost
,以及从react-apollo
导入ApolloProvider
组件:
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "react-apollo";
- 在
App
类组件的上方,让我们创建我们的ApolloClient
:
const client = new ApolloClient({
uri: "https://api.github.com/graphql",
headers: {
authorization: `Bearer our-bearer-token`
}
});
显然,我们需要用我们之前从 GitHub 获取的真实令牌来替换它。
- 最后一步是使用
ApolloProvider
组件将我们创建的ApolloClient
提供给应用中的所有其他组件。我们通过将ApolloProvider
作为根组件,并将ApolloClient
对象传递给它来实现这一点:
public render() {
return (
<ApolloProvider client={client}>
<div className="App">
<header className="App-header">
<Header />
</header>
</div>
</ApolloProvider>
);
}
现在ApolloClient
已经设置好了,我们可以开始与 GraphQL 服务器进行交互了。
使用查询组件查询 GraphQL
我们现在要使用Query
组件来获取我们的 GitHub 姓名和头像,替换axios
代码:
- 让我们首先删除
axios
导入语句,而不是有以下导入:
import gql from "graphql-tag";
import { Query } from "react-apollo";
- 我们的
IViewer
接口将保持不变,但我们需要稍微调整我们的IQueryResult
接口:
interface IQueryResult {
viewer: IViewer;
}
- 我们接下来要定义我们的 GraphQL 查询:
const GET_VIEWER = gql`
{
viewer {
name
avatarUrl
}
}
`;
所以,我们将查询设置为GET_VIEWER
变量,并在模板文字中定义了我们的查询。然而,在模板文字之前的gql
函数有点奇怪。模板文字不应该在括号中吗?实际上,这被称为标记模板文字,其中来自核心 GraphQL 库的gql
函数解析其旁边的模板文字。我们最终得到了一个 Apollo 可以使用和执行的GET-VIEWER
中的查询。
- 我们现在要开始定义我们的查询。我们可以直接在 JSX 中使用
react-apollo
中的Query
组件定义我们的查询。然而,为了增加一些类型安全性,我们将创建一个名为GetViewerQuery
的新组件,该组件继承自Query
并将结果类型定义为泛型参数:
class GetViewerQuery extends Query<IQueryResult> {}
-
我们不再需要任何状态,所以我们可以删除
viewer
和setViewer
变量。 -
我们还可以删除使用
useEffect
函数调用axios
查询的部分,因为我们现在要在 JSX 中进行查询。 -
所以,让我们使用我们的
GetViewerQuery
组件来调用我们的查询:
return (
<GetViewerQuery query={GET_VIEWER}>
{({ data }) => {
if (!data || !data.viewer) {
return null;
}
return (
<div>
<img src={data.viewer.avatarUrl} className="avatar" />
<div className="viewer">{data.viewer.name}</div>
<h1>GitHub Search</h1>
</div>
);
}}
</GetViewerQuery>
);
-
我们将我们之前创建的查询作为
query
属性传递给GetViewerQuery
组件。 -
查询结果在
GetViewerQuery
的 children 函数中返回。 -
children 函数参数包含一个包含
data
属性中数据的对象。我们将这些数据解构到一个data
变量中。 -
如果没有任何数据,我们会提前退出并返回
null
。 -
如果我们有数据,然后返回我们的头像和姓名的 JSX,引用
data
属性。
如果我们查看我们正在运行的应用程序,它应该与axios
版本完全相同。如果显示错误,我们可能需要再次npm start
应用程序。
- 我们可以从 children 函数参数中获取其他信息。一个有用的信息是数据是否正在加载。让我们使用这个来显示一个加载消息:
return (
<GetViewerQuery query={GET_VIEWER}>
{({ data, loading }) => {
if (loading) {
return <div className="viewer">Loading ...</div>;
}
...
}}
</GetViewerQuery>
);
- 我们可以从 children 函数参数中获取的另一个有用的信息是有关发生的错误的信息。让我们使用这个来显示错误消息,如果有的话:
return (
<GetViewerQuery query={GET_VIEWER}>
{({ data, loading, error }) => {
if (error) {
return <div className="viewer">{error.toString()}</div>;
}
...
}}
</GetViewerQuery>
);
这个 Apollo 实现真的很优雅。Query
组件如何在组件生命周期的正确时刻进行网络请求,并允许我们向其余的组件树提供数据,真是聪明。
在下一节中,我们将继续使用 Apollo 来增强我们的应用程序。
添加一个仓库搜索组件
在这一部分,我们将添加一个组件来搜索 GitHub 仓库并返回一些关于它的信息:
- 让我们首先创建一个名为
RepoSearch.tsx
的新文件,其中包含以下导入:
import * as React from "react";
import gql from "graphql-tag";
import { ApolloClient } from "apollo-boost";
- 我们将以
ApolloClient
作为 prop 传入。因此,让我们为此添加一个接口:
interface IProps {
client: ApolloClient<any>;
}
- 接下来,我们将搭建我们的组件:
const RepoSearch: React.SFC<IProps> = props => {
return null;
}
export default RepoSearch;
- 现在让我们在
App.tsx
中引用这个,首先导入它:
import RepoSearch from "./RepoSearch";
- 现在我们可以将其添加到应用程序标题下,传入
ApolloClient
:
<ApolloProvider client={client}>
<div className="App">
<header className="App-header">
<Header />
</header>
<RepoSearch client={client} />
</div>
</ApolloProvider>
我们的仓库search
组件现在已经很好地设置好了。在下一节中,我们可以实现一个搜索表单。
实现搜索表单
让我们实现一个搜索表单,允许用户提供组织名称和仓库名称:
- 回到
RepoSearch.tsx
,让我们开始定义搜索字段的状态,从接口开始:
interface ISearch {
orgName: string;
repoName: string;
}
- 现在我们可以创建一个变量来保存我们的
search
状态,以及一个设置它的函数:
const RepoSearch: React.SFC<IProps> = props => {
const [search, setSearch]: [
ISearch,
(search: ISearch) => void
] = React.useState({
orgName: "",
repoName: ""
});
return null;
}
- 让我们在 JSX 中定义
search
表单:
return (
<div className="repo-search">
<form onSubmit={handleSearch}>
<label>Organization</label>
<input
type="text"
onChange={handleOrgNameChange}
value={search.orgName}
/>
<label>Repository</label>
<input
type="text"
onChange={handleRepoNameChange}
value={search.repoName}
/>
<button type="submit">Search</button>
</form>
</div>
);
我们引用了一些尚未实现的部分。因此,我们将逐一实现这些。
- 让我们添加在
App.css
中引用的repo-search
类。我们还将为标签和输入以及搜索按钮添加样式:
.repo-search {
margin: 30px auto;
width: 300px;
font-family: Arial;
font-size: 16px;
text-align: left;
}
.repo-search label {
display: block;
margin-bottom: 3px;
font-size: 14px;
}
.repo-search input {
display: block;
margin-bottom: 10px;
font-size: 16px;
color: #676666;
width: 100%;
}
.repo-search button {
display: block;
margin-bottom: 20px;
font-size: 16px;
}
- 接下来,让我们实现简单更新
search
状态的输入更改处理程序:
const handleOrgNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch({ ...search, orgName: e.currentTarget.value });
};
const handleRepoNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch({ ...search, repoName: e.currentTarget.value });
};
- 我们需要实现的最后一部分是
search
处理程序:
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// TODO - make GraphQL query
};
我们在事件参数上调用preventDefault
来阻止发生完整的后退。
所以,搜索表单已经开始了。我们将在下一节中实现 GraphQL 查询。
实现搜索查询
我们现在到了需要进行 GraphQL 查询来实际搜索的地步:
- 让我们首先为我们期望从查询中获取的仓库数据创建一个接口:
interface IRepo {
id: string;
name: string;
description: string;
viewerHasStarred: boolean;
stargazers: {
totalCount: number;
};
issues: {
edges: [
{
node: {
id: string;
title: string;
url: string;
};
}
];
};
}
这是我们在之前的部分中从 GitHub GraphQL Explorer 中得到的结构。
- 我们将需要为这个状态设置一个默认值。所以,让我们定义这个:
const defaultRepo: IRepo = {
id: "",
name: "",
description: "",
viewerHasStarred: false,
stargazers: {
totalCount: 0
},
issues: {
edges: [
{
node: {
id: "",
title: "",
url: ""
}
}
]
}
};
- 我们还可以为整个查询结果定义一个接口:
interface IQueryResult {
repository: IRepo;
}
- 现在我们可以使用标记模板字面量来创建查询本身:
const GET_REPO = gql`
query GetRepo($orgName: String!, $repoName: String!) {
repository(owner: $orgName, name: $repoName) {
id
name
description
viewerHasStarred
stargazers {
totalCount
}
issues(last: 5) {
edges {
node {
id
title
url
publishedAt
}
}
}
}
}
`;
这是我们在之前的部分中在 GitHub GraphQL Explorer 中进行的查询。与以前的查询不同,这个查询有一些参数,我们需要在稍后执行查询时包含这些参数。
- 我们需要将从查询中获取的数据存储在状态中。所以,让我们创建一个名为
repo
的状态变量,以及一个设置它的函数:
const [repo, setRepo]: [
IRepo,
(repo: IRepo) => void
] = React.useState(defaultRepo);
- 我们还将在状态中存储
search
的任何问题:
const [searchError, setSearchError]: [
string,
(searchError: string) => void
] = React.useState("");
- 让我们更新
handleSearch
箭头函数,在进行search
之前清除任何搜索错误状态:
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSearchError("");
};
- 让我们继续使用作为属性传递的
ApolloClient
来进行查询:
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSearchError("");
props.client
.query<IQueryResult>({
query: GET_REPO
});
};
- 这里还有更多的工作要做。首先,我们需要从我们在
search
状态中拥有的值中传递query
参数,用于组织名称和仓库名称:
.query<IQueryResult>({
query: GET_REPO,
variables: { orgName: search.orgName, repoName: search.repoName }
})
- 现在是时候在
then
方法中处理响应并将repo
状态设置为响应中的数据了:
props.client
.query<IQueryResult>( ... )
.then(response => {
setRepo(response.data.repository);
});
- 我们还将在
catch
方法中处理任何错误,并更新searchError
状态:
props.client
.query<IQueryResult>(...)
.then(...)
.catch(error => {
setSearchError(error.message);
});
如果我们在运行的应用中尝试进行search
,查询将会正常进行,但我们还没有显示结果。让我们在下一部分中做这件事。
渲染搜索结果
让我们渲染从仓库查询中获取的数据:
- 如果我们有搜索结果,让我们在
search
表单下渲染仓库名称及其星数以及描述:
return (
<div className="repo-search">
<form ...>
...
</form>
{repo.id && (
<div className="repo-item">
<h4>
{repo.name}
{repo.stargazers ? ` ${repo.stargazers.totalCount}
stars` : ""}
</h4>
<p>{repo.description}</p>
</div>
)}
</div>
);
- 我们还将渲染最后的
5
个仓库问题:
...
<p>{repo.description}</p>
<div>
Last 5 issues:
{repo.issues && repo.issues.edges ? (
<ul>
{repo.issues.edges.map(item => (
<li key={item.node.id}>{item.node.title}</li>
))}
</ul>
) : null}
</div>
- 如果出现问题,让我们渲染在状态中捕获的错误消息:
{repo.id && (
...
)}
{searchError && <div>{searchError}</div>}
- 让我们在
App.css
中为搜索结果中的仓库标题添加一些 CSS:
.repo-search h4 {
text-align: center;
}
如果我们搜索一个仓库,现在应该看到有关仓库的信息被渲染出来:
我们现在已经可以使用 Apollo 舒适地查询 GraphQL 服务器了。在下一部分,我们将处理变异。
使用 Apollo 实现变异
让我们允许用户在我们的应用中为 GitHub 仓库加星。这将涉及通过 Apollo 发送一个mutation
:
- 首先,让我们从
react-apollo
中导入Mutation
组件:
import { Mutation } from "react-apollo";
- 现在让我们创建
mutation
。这是我们之前在 GitHub GraphQL Explorer 中执行的相同查询:
const STAR_REPO = gql`
mutation($repoId: ID!) {
addStar(input: { starrableId: $repoId }) {
starrable {
stargazers {
totalCount
}
}
}
}
`;
- 在 JSX 中,在我们渲染描述的地方,让我们放置
Mutation
组件:
<p>{repo.description}</p>
<div>
{!repo.viewerHasStarred && (
<Mutation
mutation={STAR_REPO}
variables={{ repoId: repo.id }}
>
{() => (
// render Star button that invokes the mutation when
clicked
)}
</Mutation>
)}
</div> <div>
Last 5 issues:
...
</div>
-
只有在
viewer
还没有给存储库添加星标时,我们才渲染mutation
-
Mutation
组件接受我们刚刚定义的 mutation 以及变量,这在我们的情况下是存储库的id
Mutation
组件有一个 children 函数,它给了我们访问addStar
函数的权限。因此,让我们渲染一个 Star!按钮,当点击时调用addStar
:
<Mutation
...
>
{(addStar) => (
<div>
<button onClick={() => addStar()}>
Star!
</button>
</div>
)}
</Mutation>
)}
Mutation
组件还告诉我们mutation
正在执行,通过 children 函数的第二个参数中的loading
属性。让我们使用这个来禁用按钮,并通知用户星标正在被添加:
<Mutation
...
>
{(addStar, { loading }) => (
<div>
<button disabled={loading} onClick={() => addStar()}>
{loading ? "Adding ..." : "Star!"}
</button>
</div>
)}
</Mutation>
Mutation
组件还告诉我们是否有错误。因此,让我们使用这个并在发生错误时渲染错误:
<Mutation
...
>
{(addStar, { loading, error }) => (
<div>
<button ...>
...
</button>
{error && <div>{error.toString()}</div>}
</div>
)}
</Mutation>
如果我们尝试给存储库添加星标,星标应该会成功添加。我们可以去 GitHub 存储库的github.com验证这一点。
现在我们已经实现了查询和mutation
,我们真正掌握了 Apollo。不过,有一件事情有点奇怪,也许我们已经注意到了。在我们给存储库添加星标后,应用程序中星标的数量没有更新。即使我们再次搜索存储库,星标的数量仍然是我们开始之前的数量。但是,如果我们刷新浏览器并再次搜索存储库,我们会得到正确的星标数量。那么,这是怎么回事呢?我们将在下一节中找出答案。
在 Apollo 中使用缓存数据
我们在上一节结束时留下了一个谜。为什么我们在开始搜索后没有得到存储库search
的最新星标数量?答案是 Apollo 在初始search
后缓存了存储库数据。当执行相同的查询时,它会从缓存中获取结果,而不是从 GraphQL 服务器获取数据。
让我们再次确认一下:
- 让我们打开应用程序并在网络选项卡上打开开发者工具,并清除之前的请求:
- 让我们进行一次搜索。我们会看到向 GitHub GraphQL 服务器发出了几个请求:
- 在开发者工具中,网络选项卡,让我们清除请求,然后在我们的应用程序中再次点击搜索按钮。我们会看到没有网络请求被发出,但数据被渲染出来。所以,数据一定是来自本地缓存。
所以,我们使用apollo-boost
配置的ApolloClient
会自动将查询缓存到内存中。在下一节中,我们将学习如何清除缓存,以便我们的应用程序在仓库被加星后显示正确的星星数量。
使用refetchQueries
清除缓存
在mutation
发生后,我们需要一种清除缓存查询结果的方法。一种方法是在Mutation
组件上使用refetchQueries
属性:
- 让我们试一试。
refetchQueries
属性接受一个包含应该从缓存中移除的具有相应变量值的查询对象数组:
<Mutation
mutation={STAR_REPO}
variables={{ repoId: repo.id }}
refetchQueries={[
{
query: GET_REPO,
variables: {
orgName: search.orgName,
repoName: search.repoName
}
}
]}
>
...
</Mutation>
- 如果我们现在给一个仓库加星标,星星的数量不会立即更新。然而,如果按下搜索按钮,星星就会更新。
所以,缓存已经清除,但是体验仍然不理想。理想情况下,我们希望在点击“Star!”按钮后立即更新星星的数量。
如果我们仔细思考刚才做的事情,我们正在试图绕过缓存。然而,缓存的存在是为了帮助我们的应用程序表现良好。
所以,这种方法并不理想。用户体验仍然不理想,我们刚刚使我们的应用程序性能下降了。一定有更好的方法!我们将在下一节中探索另一种方法。
在 Mutation 后更新缓存
让我们再次仔细思考一下问题:
-
我们在缓存中有关于仓库的一些信息,包括它拥有的星星数量。
-
当我们给仓库加星标时,我们希望看到星星的数量增加了一个。
-
如果我们可以在缓存中将星星的数量增加一个,那会怎么样?这应该能解决问题。
所以,让我们尝试一下,在mutation
完成后更新缓存:
-
首先,让我们移除上一节中实现的
refetchQueries
属性。 -
Mutation
组件上有一个update
属性,我们可以利用它来更新缓存。所以,让我们开始实现这个功能:
<Mutation
mutation={STAR_REPO}
update={cache => {
// Get the cached data
// update the cached data
// update our state
}}
>
...
</Mutation>
- 所以,我们需要实现一个箭头函数,更新可用作参数的缓存:
<Mutation
...
update={cache => {
const data: { repository: IRepo } | null = cache.readQuery({
query: GET_REPO,
variables: {
orgName: search.orgName,
repoName: search.repoName
}
});
if (data === null) {
return;
}
}}
>
...
</Mutation>
所以,缓存有一个readQuery
函数,我们可以使用它来获取缓存的数据。如果在缓存中找不到数据,那么我们可以退出函数而不做其他事情。
- 因此,现在我们从缓存中获取了数据,我们可以增加星星的数量。为此,我们创建一个新对象,并将缓存存储库的属性扩展到其中,并用增加的星星数量和查看者已经为存储库加星的事实覆盖它:
update={cache => {
...
if (data === null) {
return;
}
const newData = {
...data.repository, viewerHasStarred: true,
stargazers: {
...data.repository.stargazers,
totalCount: data.repository.stargazers.totalCount + 1
}
};
}}
- 然后,我们可以使用其
writeQuery
函数更新缓存。我们传入带有变量值的查询和要存储在缓存中的新数据:
update={cache => {
...
const newData = {
...
};
cache.writeQuery({
query: GET_REPO,
variables: {
orgName: search.orgName,
repoName: search.repoName
},
data: { repository: newData }
});
}}
- 还有一件事要做,那就是更新
repo
状态,以便星星的数量立即在屏幕上更新:
update={cache => {
...
cache.writeQuery(...);
setRepo(newData);
}}
就是这样。如果我们再次尝试在应用程序中为存储库加星,我们应该会看到星星的数量立即增加。
缓存是 Apollo 提供的伟大功能之一。Mutation
组件上的update
属性为我们提供了一种精确更新缓存的方式。Mutation
组件上的refetchQueries
属性是一种更粗暴且效率低下的强制更新缓存的方式。
总结
GraphQL 比 REST 更出色,因为它允许我们以更少的努力有效地获取所需的数据。GitHub GraphQL Explorer 是一个很好的工具,可以让我们熟悉语法。我们可以向 GraphQL 服务器发出两种主要类型的请求:
-
我们可以执行
query
来读取数据 -
我们可以执行
mutation
来写入数据
查询允许我们指定响应中需要的对象和字段。我们可以使用别名来重命名它们。我们可以通过定义变量来参数化查询。我们可以给变量类型,并在末尾使用!
来指定每个变量是否是必需的。本章中我们没有涵盖的查询功能还有条件包含字段和强大的分页功能。总之,这是一种非常强大的查询语言!
变异与查询有一些相同的特性,比如能够向它们传递参数。我们可以控制响应中包含的数据,这真是太棒了。
GraphQL 通过 HTTP 运行,使用 HTTP POST
请求到单个 URL。HTTP 正文包含查询或mutation
信息。我们可以使用 HTTP 客户端与 GraphQL 服务器交互,但使用专门与 GraphQL 服务器交互的 Apollo 等库可能会更有效率。
React Apollo 是一组与核心 Apollo 库配合使用的 React 组件。它为我们提供了很好的Query
和Mutation
React 组件,用于在我们的 JSX 中包含查询和变更,使我们的代码更易于阅读。在我们使用这些组件之前,我们需要设置我们的ApolloClient
对象,包括 GraphQL 服务器的 URL 和任何凭据。我们还需要在我们的组件树的顶部包含一个ApolloProvider
组件,高于所有需要 GraphQL 数据的组件。
当我们使用apollo-boost
搭建项目时,缓存默认开启。Mutation
组件给了我们update
和refetchQueries
属性来管理缓存更新。
总的来说,GraphQL 是与后端交互的一种非常高效的方式,它与 React 和 TypeScript 应用程序非常配合。
因此,到目前为止,我们在这本书中学到了许多关于 React 和 TypeScript 的不同方面。一个我们尚未涉及的重要主题是如何对我们构建的应用进行健壮的测试。我们将在下一章中介绍这个主题。
问题
让我们尝试一些问题,来测试我们刚刚学到的知识:
-
在 GitHub GraphQL Explorer 中,创建一个查询,返回 React 项目中最后五个未解决的问题。在响应中返回问题标题和 URL。
-
增强最后一个查询,并使返回的问题数量成为一个参数,并将其默认设置为五。
-
在 GitHub GraphQL Explorer 中创建一个
mutation
来取消对一个已标星的存储库的标星。mutation
应该以一个必需的存储库id
作为参数。 -
GraphQL 查询的哪一部分放在 HTTP 请求中?
-
GraphQL
mutation
的哪一部分放在 HTTP 请求中? -
如何使
react-apollo
的Query
组件的响应类型安全? -
使用
react-boost
搭建项目时,默认情况下是否开启缓存? -
我们可以在
Mutation
组件上使用哪个属性来更新本地缓存?
进一步阅读
以下链接是关于 GraphQL、React 和 Apollo 的进一步信息的好资源:
-
GraphQL 文档位于
graphql.org/learn/
-
Apollo 文档位于
www.apollographql.com/docs/
-
Apollo 文档中关于 React 部分的链接是
www.apollographql.com/docs/react/
更多推荐
所有评论(0)