React16 高级教程(十一)
原文:Pro React 16
二十三、使用 RESTful Web 服务
在这一章中,我通过创建一个 web 服务并使用它来管理应用的数据来解决示例应用缺乏永久数据存储的问题。应用将向 web 服务发送 HTTP 请求,以检索数据并提交更改。本章一开始,我将向您展示如何在组件中直接使用 web 服务,然后演示如何将 web 服务用于数据存储。在第二十四章中,我解释了如何使用 GraphQL,这是处理 web 服务的另一种方法。表 23-1 将这一章放在上下文中。
表 23-1
将消费 Web 服务放在上下文中
|问题
|
回答
| | --- | --- | | 这是什么? | Web 服务充当应用的数据存储库,允许使用 HTTP 请求读取、存储、修改和删除数据。 | | 为什么有用? | Web 服务非常适合浏览器中可用的特性,并且避免了处理本地存储问题。 | | 如何使用? | Web 服务的实现方式不尽相同,但一般的方法是发送 HTTP 请求,其中请求方法标识要执行的操作,请求 URL 标识要操作的数据。 | | 有什么陷阱或限制吗? | web 服务实现的不一致性意味着每个 web 服务可能需要一组稍微不同的请求。在组件中使用 web 服务时必须小心,以确保不会在每次更新时都发送请求。 | | 还有其他选择吗? | 现代 web 浏览器支持本地存储选项,这对于某些项目来说是一个很好的选择。然而,主要的缺点是每个客户端都有自己的数据,这就错过了单一中央存储库的一些优势。 |
表 23-2 总结了本章内容。
表 23-2
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 从 web 服务获取数据 | 创建一个发出 HTTP 请求的数据源,并使用调用setState方法的回调将数据反馈给应用。 | 1–11 |
| 执行附加数据操作 | 扩展数据源以发送 HTTP 方法和 URL 的不同组合来指示所需的操作。通过响应组件事件来触发请求 | 12–15 |
| 处理请求错误 | 使用try / catch块捕捉错误并将其传递给组件,以便向用户显示警告。 | 16–19 |
| 使用带有数据存储的 web 服务 | 使用中间件拦截数据存储操作,并将所需的请求发送到 web 服务。请求完成后,将操作转发到数据存储,以便可以更新它。 | 20–24 |
为本章做准备
在这一章中,我继续使用第二十二章中的productapp项目,该项目在之后的章节中进行了修改。需要做一些准备工作来将附加的包安装到项目中,并创建应用将依赖的 web 服务。
小费
你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。
将包添加到项目中
运行productapp文件夹中清单 23-1 中所示的命令,将所需的包添加到项目中。
npm install json-server@0.14.0 --save-dev
npm install npm-run-all@4.1.3 --save-dev
npm install axios@0.18.0
Listing 23-1Installing Additional Packages to the Project
为了快速参考,表 23-3 中描述了清单 23-1 中命令的包。
表 23-3
添加到项目中的包
|名字
|
描述
|
| --- | --- |
| json-server | 这个包提供了一个 web 服务,应用将查询该服务中的数据。该命令与save-dev命令一起安装,因为它是开发所必需的,不是应用的一部分。 |
| npm-run-all | 这个包允许多个命令并行运行,以便 web 服务和开发服务器可以同时启动。该命令与save-dev命令一起安装,因为它是开发所必需的,不是应用的一部分。 |
| axios | 应用将使用这个包向 web 服务发出 HTTP 请求。 |
准备 Web 服务
为了给json-server包提供要处理的数据,在productapp文件夹中添加一个名为restData.js的文件,并添加清单 23-2 中所示的代码。
module.exports = function () {
var data = {
products: [
{ id: 1, name: "Kayak", category: "Watersports", price: 275 },
{ id: 2, name: "Lifejacket", category: "Watersports", price: 48.95 },
{ id: 3, name: "Soccer Ball", category: "Soccer", price: 19.50 },
{ id: 4, name: "Corner Flags", category: "Soccer", price: 34.95 },
{ id: 5, name: "Stadium", category: "Soccer", price: 79500 },
{ id: 6, name: "Thinking Cap", category: "Chess", price: 16 },
{ id: 7, name: "Unsteady Chair", category: "Chess", price: 29.95 },
{ id: 8, name: "Human Chess Board", category: "Chess", price: 75 },
{ id: 9, name: "Bling Bling King", category: "Chess", price: 1200 }
],
suppliers: [
{ id: 1, name: "Surf Dudes", city: "San Jose", products: [1, 2] },
{ id: 2, name: "Goal Oriented", city: "Seattle", products: [3, 4, 5] },
{ id: 3, name: "Bored Games", city: "New York", products: [6, 7, 8, 9] },
]
}
return data
}
Listing 23-2The Contents of the restData.js File in the productapp Folder
json-server包可以处理 JSON 或 JavaScript 文件。如果使用 JSON 文件,它的内容将被修改,以反映客户机发出的更改请求。相反,我选择了 JavaScript 选项,它允许以编程方式生成数据,并意味着重新启动该过程将返回到原始数据。这不是您在实际项目中会做的事情,但是对于这个例子来说是有用的,因为它使得返回到一个已知的状态变得容易,同时仍然允许应用访问持久数据。
为了配置json-server包,使其响应以/api开头的 URL 请求,在productapp文件夹中创建一个名为api.routes.json的文件,其内容如清单 23-3 所示。
{ "/api/*": "/$1" }
Listing 23-3The Contents of the api.routes.json File in the productapp Folder
要配置开发工具,使 web 服务与开发 web 服务器同时启动,对productapp文件夹中的package.json文件进行清单 23-4 所示的更改。
...
"scripts": {
"start": "npm-run-all --parallel reactstart json",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"reactstart": "react-scripts start",
"json": "json-server --p 3500 -r api.routes.json restData.js"
},
...
Listing 23-4Configuring Tools in the package.json File in the productapp Folder
对package.json文件的scripts部分的更改使用了npm-run-all包,这样 HTTP 开发服务器和json-server由npm start启动。
添加组件和路线
我将演示如何孤立地使用 web 服务,然后向您展示如何使用数据存储中的数据。应用中的现有组件已经连接到数据存储,因此为了展示如何使用未连接的组件,我在src文件夹中创建了一个名为IsolatedTable.js的文件,并用它来创建清单 23-5 中所示的组件。
import React, { Component } from "react";
export class IsolatedTable extends Component {
render() {
return <table className="table table-sm table-striped table-bordered">
<thead>
<tr><th colSpan="5"
className="bg-info text-white text-center h4 p-2">
(Isolated) Products
</th></tr>
<tr>
<th>ID</th><th>Name</th><th>Category</th>
<th className="text-right">Price</th>
<th></th>
</tr>
</thead>
<tbody>
<tr><td colSpan="5" className="text-center p-2">No Data</td></tr>
</tbody>
</table>
}
}
Listing 23-5The Contents of the IsolatedTable.js File in the src Folder
该组件呈现一个空表作为该时刻的占位符。为了将组件合并到应用中,我更新了Selector组件中的路由配置,添加了一个新的Route和一个相应的导航链接,如清单 23-6 所示。
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect }
from "react-router-dom";
import { ToggleLink } from "./routing/ToggleLink";
import { RoutedDisplay } from "./routing/RoutedDisplay";
import { IsolatedTable } from "./IsolatedTable";
export class Selector extends Component {
render() {
const routes = React.Children.map(this.props.children, child => ({
component: child,
name: child.props.name,
url: `/${child.props.name.toLowerCase()}`,
datatype: child.props.datatype
}));
return <Router getUserConfirmation={ this.customGetUserConfirmation }>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/isolated">Isolated Data</ToggleLink>
{ routes.map(r => <ToggleLink key={ r.url } to={ r.url }>
{ r.name }
</ToggleLink>)}
</div>
<div className="col">
<Switch>
<Route path="/isolated" component={ IsolatedTable } />
{ routes.map(r =>
<Route key={ r.url }
path={ `/:datatype(${r.datatype})/:mode?/:id?`}
component={ RoutedDisplay(r.datatype)} />
)}
<Redirect to={ routes[0].url } />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 23-6Adding a Route in the Selector.js File in the src Folder
运行 Web 服务和示例应用
使用命令提示符,运行productapp文件夹中清单 23-7 所示的命令来启动开发工具和 web 服务。
npm start
Listing 23-7Starting the Development Tools
一旦项目的初始准备完成,一个新的浏览器窗口将打开并显示 URL http://localhost:3000,如图 23-1 所示。
图 23-1
运行示例应用
打开新的浏览器窗口并导航至http://localhost:3500/api/products/2。服务器将以下列数据响应,这些数据也显示在图 23-2 中:
图 23-2
测试 web 服务
...
{ "id": 2, "name": "Lifejacket", "category": "Watersports", "price": 48.95 }
...
我为本章选择的配置意味着有两个 HTTP 服务器在运行。React development server 在端口 3000 上侦听请求,并提供启动应用的 HTML 文档,以及将应用呈现给用户所需的 JavaScript 和 CSS 文件。RESTful web 服务在端口 3500 上监听请求,并以数据响应。这些数据以 JSON 格式表示,这意味着 JavaScript 应用很容易处理这些数据,但是不应该直接呈现给大多数用户。
理解 RESTful Web 服务
交付和存储应用数据的最常见方法是应用称为 REST 的表示性状态转移模式来创建数据 web 服务。REST 没有详细的规范,这导致很多不同的方法都打着 RESTful 的旗号。然而,在 web 应用开发中有一些有用的统一思想。
RESTful web 服务的核心前提是包含 HTTP 的特性,以便请求方法——也称为动词——指定服务器要执行的操作,请求 URL 指定操作将应用到的一个或多个数据对象。
例如,在示例应用中,下面是一个可能指向特定产品的 URL:
http://localhost:3500/api/products/2
URL 的第一段—api—通常表示请求的是数据。下一个段—products—用于指示将要操作的对象集合,并允许单个服务器提供多个服务,每个服务都有自己的数据。最后一个片段——2——在products集合中选择一个单独的对象。在这个例子中,id属性的值唯一地标识了一个对象,并将在 URL 中使用,在这个例子中,指定了Lifejacket对象。
用于发出请求的 HTTP 动词或方法告诉 RESTful 服务器应该对指定的对象执行什么操作。在上一节中测试 RESTful 服务器时,浏览器发送了一个 HTTP GET 请求,服务器将其解释为检索指定对象并将其发送给客户机的指令。
表 23-4 显示了 HTTP 方法和 URL 的最常见组合,并解释了当发送到 RESTful 服务器时它们各自的作用。
表 23-4
RESTful Web 服务中常见的 HTTP 动词及其作用
|动词
|
统一资源定位器
|
描述
|
| --- | --- | --- |
| GET | /api/products | 这种组合检索products集合中的所有对象。 |
| GET | /api/products/2 | 这个组合从products集合中检索出id为2的对象。 |
| POST | /api/products | 该组合用于向products集合添加一个新对象。请求体包含新对象的 JSON 表示。 |
| PUT | /api/products/2 | 该组合用于替换products集合中id为 2 的对象。请求体包含替换对象的 JSON 表示。 |
| PATCH | /api/products/2 | 该组合用于更新products集合中对象属性的子集,该集合的id为 2。请求体包含要更新的属性和新值的 JSON 表示。 |
| DELETE | /api/products/2 | 该组合用于从products集合中删除id为 2 的产品。 |
web 服务的实现方式有相当大的差异,这是由创建它们所使用的框架和开发团队的偏好的差异造成的。确认 web 服务如何使用动词以及在 URL 和请求正文中需要什么来执行操作是很重要的。
常见的变化包括不接受任何包含id值的请求主体的 web 服务(以确保它们是由服务器的数据存储唯一生成的)和不支持所有动词的 web 服务(通常忽略PATCH请求,只接受使用PUT动词的更新)。
小费
您可能已经注意到,编辑器组件不允许用户为id属性提供值。这是因为我在本章中创建的 web 服务会自动生成id值以确保唯一性。
选择 HTTP 请求库
在本章中,我使用 Axios 库向 web 服务发送 HTTP 请求,因为它易于使用,可以自动处理常见的数据类型,并且不需要复杂的代码来处理像 CORS 这样的特性(参见侧栏“生成跨源请求”)。Axios 广泛应用于 web 应用开发,尽管它不是专门针对 React 的。
Axios 并不是向 web 服务发送 HTTP 请求的唯一方式。最基本的选择是使用XMLlHttpRequest对象,该对象提供了使用 JavaScript 进行请求的原始 API(尽管名称中有 XML,但它能够处理一系列数据类型)。XMLHttpRequest对象使用起来有些笨拙,但是有广泛的浏览器支持,你可以在 https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest 获得进一步的细节。(Axios 使用XMLHttpRequest来发出 HTTP 请求,但简化了它们的创建和处理方式。)
Fetch API 是现代浏览器提供的最新 API,旨在取代XMLHttpRequest,在 https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 有描述。主流浏览器的最新版本支持 Fetch API,但旧版本的浏览器不支持,这对某些应用来说可能是个问题。
如果你正在使用 GraphQL,那么你应该考虑使用 Apollo 客户端,如第二十五章所述。
消费 Web 服务
在接下来的小节中,我将介绍消费 web 服务所需的步骤,首先是请求应用将向用户显示的初始数据,然后添加对存储和更新对象的支持。
创建数据源组件
将使用 Axios 消费 web 服务的代码与使用它的组件分开是一个好主意,这样可以更容易地在应用的其他地方测试和使用它。我创建了src/webservice文件夹,并添加了一个名为RestDataSource.js的文件,代码如清单 23-8 所示。
import Axios from "axios";
export class RestDataSource {
constructor(base_url) {
this.BASE_URL = base_url;
}
GetData(callback) {
this.SendRequest("get", this.BASE_URL, callback);
}
SendRequest(method, url, callback) {
Axios.request({
method: method,
url: url
}).then(response => callback(response.data));
}
}
Listing 23-8The Contents of the RestDataSource.js File in the src/webservice Folder
Listing 23-8The Contents of the RestDataSource.js File in the src/webservice Folder
RestDataSource类定义了一个接收 web 服务的基本 URL 的构造函数,并定义了一个调用SendRequest的GetData方法。
我从axios包中导入了 HTTP 功能,并将其命名为Axios。SendRequest方法使用 Axios 通过request方法发送 HTTP 请求,其中请求的细节使用具有method和url属性的配置对象指定。
Axios 提供了发送不同类型 HTTP 请求的方法——例如,get、post和put方法,但是使用清单中的方法可以更容易地应用影响所有请求类型的特性,当我在本章后面添加错误处理时,您会看到这一点。
使用 JavaScript 发出的 HTTP 请求是异步的。request方法返回一个代表请求最终结果的Promise对象(关于如何使用Promise对象的详细信息,见第四章)。在清单 23-8 中,我使用then方法为 Axios 提供一个回调函数,以便在请求完成时使用。回调函数被传递一个对象,该对象使用表 23-5 中描述的属性描述响应。
表 23-5
Axios 响应属性
|名字
|
描述
|
| --- | --- |
| status | 该属性返回响应的状态代码,如 200 或 404。 |
| statusText | 此属性返回伴随状态代码的说明性文本,如 OK 或 Not Found。 |
| headers | 此属性返回一个对象,该对象的属性表示响应标头。 |
| data | 该属性从响应中返回有效负载。 |
| config | 此属性返回一个对象,该对象包含用于发出请求的配置选项。 |
| request | 该属性返回用于发出请求的底层XMLHttpRequest对象,如果您需要直接访问浏览器提供的 API,这会很有用。 |
Axios 自动将 JSON 数据格式转换成 JavaScript 对象,并通过 response data属性呈现出来。正如第四章所解释的,使用承诺的代码可以通过使用async和await关键字来简化,如清单 23-9 所示。
import Axios from "axios";
export class RestDataSource {
constructor(base_url) {
this.BASE_URL = base_url;
}
GetData(callback) {
this.SendRequest("get", this.BASE_URL, callback);
}
async SendRequest(method, url, callback) {
let response = await Axios.request({
method: method,
url: url
});
callback(response.data);
}
}
Listing 23-9Using async and await in the RestDataSource.js File in the src/webservice Folder
我可以通过组合GetData方法中的语句来进一步简化代码,如清单 23-10 所示。
import Axios from "axios";
export class RestDataSource {
constructor(base_url) {
this.BASE_URL = base_url;
}
GetData(callback) {
this.SendRequest("get", this.BASE_URL, callback);
}
async SendRequest(method, url, callback) {
callback((await Axios.request({
method: method,
url: url
})).data);
}
}
Listing 23-10Combining Statements in the RestDataSource.js File in the src/webservice Folder
这种方法更简洁,但重要的是要确保将括号放在正确的位置,这样,await关键字被应用到由SendRequest方法返回的对象,而data属性从它产生的对象中读取。如果不遵循这种模式,您很容易就会出现这样的情况:发送了 HTTP 请求,但是响应被忽略。
获取组件中的数据
下一步是将数据放入组件,以便可以向用户显示。在清单 23-11 中,我已经更新了IsolatedTable组件,以便它创建一个数据源并使用它从 web 服务请求数据。
注意
组件名称中的术语 isolated 表示该组件不与任何其他组件共享数据,而是直接处理 web 服务。在“使用带有数据存储的 Web 服务”一节中,我向您展示了一种替代方法,其中组件通过数据存储共享数据。
import React, { Component } from "react";
import { RestDataSource } from "./webservice/RestDataSource";
export class IsolatedTable extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.dataSource = new RestDataSource("http://localhost:3500/api/products")
}
render() {
return <table className="table table-sm table-striped table-bordered">
<thead>
<tr><th colSpan="5"
className="bg-info text-white text-center h4 p-2">
(Isolated) Products
</th></tr>
<tr>
<th>ID</th><th>Name</th><th>Category</th>
<th className="text-right">Price</th>
<th></th>
</tr>
</thead>
<tbody>
{
this.state.products.map(p => <tr key={ p.id }>
<td>{ p.id }</td><td>{ p.name }</td><td>{p.category}</td>
<td className="text-right">
${ Number(p.price).toFixed(2)}
</td><td/>
</tr>)
}
</tbody>
</table>
}
componentDidMount() {
this.dataSource.GetData(data => this.setState({products: data}));
}
}
Listing 23-11Getting Data in the IsolatedTable.js File in the src Folder
数据是在componentDidMount方法中请求的,这确保了在组件呈现其内容之前不会发送 HTTP 请求。提供给GetData方法的回调函数更新组件的状态数据,这将触发更新并确保数据呈现给用户。
避免无关的数据请求
不要在render方法中请求数据。正如我在第十三章中解释的,组件的render方法可能会被经常调用,而在render方法中启动任务可能会生成大量不必要的 HTTP 请求,并增加 React 在处理响应数据时必须执行的更新次数。
即使在使用componentDidMount方法时,当从可能被卸载和重新安装的组件发出请求时也应该小心,例如示例中的IsolatedTable组件,它将由路由系统为/isolated URL 安装,当用户导航到另一个位置时卸载。每次安装组件时,它都会从 web 服务请求新的数据,这可能不是应用所需要的。为了避免不必要的数据请求,可以将数据提升到一个不会被卸载的组件,存储在一个上下文中(如第十四章中所述),或者合并到一个数据存储中,如“使用数据存储消费 Web 服务”一节中所述。
结果是当点击隔离数据按钮时,从 web 服务中获取数据并显示给用户,如图 23-3 所示。
图 23-3
从 web 服务获取数据
保存、更新和删除数据
为了实现保存、更新和删除数据所需的操作,我将清单 23-12 中所示的方法添加到数据源类中,使用 Axios 通过不同的 HTTP 方法向 web 服务发送请求。
import Axios from "axios";
export class RestDataSource {
constructor(base_url) {
this.BASE_URL = base_url;
}
GetData(callback) {
this.SendRequest("get", this.BASE_URL, callback);
}
async GetOne(id, callback) {
this.SendRequest("get", `${this.BASE_URL}/${id}`, callback);
}
async Store(data, callback) {
this.SendRequest("post", this.BASE_URL, callback, data)
}
async Update(data, callback) {
this.SendRequest("put", `${this.BASE_URL}/${data.id}`, callback, data);
}
async Delete(data, callback) {
this.SendRequest("delete", `${this.BASE_URL}/${data.id}`, callback, data);
}
async SendRequest(method, url, callback, data) {
callback((await Axios.request({
method: method,
url: url,
data: data
})).data);
}
}
Listing 23-12Adding Methods in the RestDataSource.js File in the src/webservice Folder
传递给Axios.request方法的请求配置对象使用一个data属性来指定请求的有效负载,这允许应用提供 JavaScript 对象并让 Axios 自动序列化它们。
当您实现数据源方法时,您会发现需要进行一些调整,以适应 web 服务实现的各种方式。例如,示例 web 服务将自动为 POST 请求中接收的对象分配一个唯一的id属性值,并在响应中包含完整的对象。清单 23-12 中的Store方法使用data属性从 HTTP 响应中获取完整的对象,并使用它来调用回调,这确保应用接收 web 服务存储的对象。并非所有的 web 服务都以这种方式运行——有些可能要求应用包含唯一的标识符,或者只在响应中返回标识符,而不是发送完整的对象。
修改对象时,会发送一个 PUT 请求,其中包含标识要修改的对象的 URL,如下所示:
...
this.SendRequest("put", `${this.BASE_URL}/${data.id}`, callback, data);
...
web 服务返回完整的更新对象,该对象用于调用回调函数。同样,并不是所有的 web 服务都返回完整的对象,但这是一种常见的方法,因为它确保 web 服务应用的任何附加转换都反映在客户机中。
添加对创建、编辑和删除数据的应用支持
为了提供创建和编辑数据的支持,我在src文件夹中添加了一个名为IsolatedEditor.js的文件,并用它来定义清单 23-13 中所示的组件。
import React, { Component } from "react";
import { RestDataSource } from "./webservice/RestDataSource";
import { ProductEditor } from "./ProductEditor";
export class IsolatedEditor extends Component {
constructor(props) {
super(props);
this.state = {
dataItem: {}
};
this.dataSource = this.props.dataSource
|| new RestDataSource("http://localhost:3500/api/products");
}
save = (data) => {
const callback = () => this.props.history.push("/isolated");
if (data.id === "") {
this.dataSource.Store(data, callback);
} else {
this.dataSource.Update(data, callback);
}
}
cancel = () => this.props.history.push("/isolated");
render() {
return <ProductEditor key={ this.state.dataItem.id }
product={ this.state.dataItem } saveCallback={ this.save }
cancelCallback={ this.cancel } />
}
componentDidMount() {
if (this.props.match.params.mode === "edit") {
this.dataSource.GetOne(this.props.match.params.id,
data => this.setState({ dataItem: data}));
}
}
}
Listing 23-13The Contents of the IsolatedEditor.js File in the src Folder
React 使得以新的方式使用现有组件变得容易,IsolatedEditor组件使用现有的ProductEditor及其 props 为其提供来自 web 服务数据源的数据和回调。当用户选择了一个对象进行编辑时,当前路由的细节被用来使用GetOne方法请求单个对象的细节,并且使用Store或Update方法将更改发送回 web 服务。在清单 23-14 中,我添加了对IsolatedTable组件的支持,通过导航到新的 URL 来创建和编辑对象。我还添加了一个删除按钮,其事件处理程序调用数据源的Delete方法,该方法向 web 服务发送删除请求。
import React, { Component } from "react";
import { RestDataSource } from "./webservice/RestDataSource";
import { Link } from "react-router-dom";
export class IsolatedTable extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.dataSource = new RestDataSource("http://localhost:3500/api/products")
}
deleteProduct(product) {
this.dataSource.Delete(product,
() => this.setState({products: this.state.products.filter(p =>
p.id !== product.id)}));
}
render() {
return <table className="table table-sm table-striped table-bordered">
<thead>
<tr><th colSpan="5"
className="bg-info text-white text-center h4 p-2">
(Isolated) Products
</th></tr>
<tr>
<th>ID</th><th>Name</th><th>Category</th>
<th className="text-right">Price</th>
<th></th>
</tr>
</thead>
<tbody>
{
this.state.products.map(p => <tr key={ p.id }>
<td>{ p.id }</td><td>{ p.name }</td><td>{p.category}</td>
<td className="text-right">
${ Number(p.price).toFixed(2)}
</td>
<td>
<Link className="btn btn-sm btn-warning mx-2"
to={`/isolated/edit/${p.id}`}>
Edit
</Link>
<button className="btn btn-sm btn-danger mx-2"
onClick={ () => this.deleteProduct(p)}>
Delete
</button>
</td>
</tr>)
}
</tbody>
<tfoot>
<tr className="text-center">
<td colSpan="5">
<Link to="/isolated/create"
className="btn btn-info">Create</Link>
</td>
</tr>
</tfoot>
</table>
}
componentDidMount() {
this.dataSource.GetData(data => this.setState({products: data}));
}
}
Listing 23-14Adding Data Operations in the IsolatedTable.js File in the src Folder
最后一步是更新Selector组件中的路由配置,以便/isolated/edit和/isolated/createURL 选择IsolatedEditor组件。我还为/isolated URL 设置了完全匹配的路由,以确保IsolatedTable组件的Route不匹配其他 URL,如清单 23-15 所示。
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect }
from "react-router-dom";
import { ToggleLink } from "./routing/ToggleLink";
import { RoutedDisplay } from "./routing/RoutedDisplay";
import { IsolatedTable } from "./IsolatedTable";
import { IsolatedEditor } from "./IsolatedEditor";
export class Selector extends Component {
render() {
const routes = React.Children.map(this.props.children, child => ({
component: child,
name: child.props.name,
url: `/${child.props.name.toLowerCase()}`,
datatype: child.props.datatype
}));
return <Router getUserConfirmation={ this.customGetUserConfirmation }>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/isolated">Isolated Data</ToggleLink>
{ routes.map(r => <ToggleLink key={ r.url } to={ r.url }>
{ r.name }
</ToggleLink>)}
</div>
<div className="col">
<Switch>
<Route path="/isolated" component={ IsolatedTable }
exact={ true } />
<Route path="/isolated/:mode/:id?"
component={ IsolatedEditor } />
{ routes.map(r =>
<Route key={ r.url }
path={ `/:datatype(${r.datatype})/:mode?/:id?`}
component={ RoutedDisplay(r.datatype)} />
)}
<Redirect to={ routes[0].url } />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 23-15Adding a Route in the Selector.js File in the src Folder
IsolatedTable组件显示创建、编辑、删除按钮,如图 23-4 所示。Create 和 Edit 按钮向用户显示编辑器组件,然后该组件通过发送 POST 或 PUT 请求,用用户所做的更改来更新 web 服务。删除按钮通过向 web 服务发送删除请求来删除与其相关联的对象。
图 23-4
消费 web 服务
注意
应用所做的更改存储在 web 服务中,这意味着您可以重新加载浏览器,并且更改仍然可见。本章开头的json-server包的配置意味着重新启动开发工具将重置 web 服务提供的数据。参见第八章中的 SportsStore 应用,了解使用json-server获取真正持久数据的示例,这些数据在工具重启时不会重置。
处理错误
应用假设所有的 HTTP 请求都会成功,这是一种不切实际的乐观方法。HTTP 请求失败的原因有很多,比如连接问题或服务器故障。我在第十四章中描述的错误边界不能处理异步操作中出现的问题,比如 HTTP 请求,所以需要一种不同的方法。在清单 23-16 中,我已经更改了数据源,这样它就可以接收一个在出现问题时调用的函数,并在请求失败时使用try / catch关键字来调用该函数。
import Axios from "axios";
export class RestDataSource {
constructor(base_url, errorCallback) {
this.BASE_URL = base_url;
this.handleError = errorCallback;
}
GetData(callback) {
this.SendRequest("get", this.BASE_URL, callback);
}
async GetOne(id, callback) {
this.SendRequest("get", `${this.BASE_URL}/${id}`, callback);
}
async Store(data, callback) {
this.SendRequest("post", this.BASE_URL, callback, data)
}
async Update(data, callback) {
this.SendRequest("put", `${this.BASE_URL}/${data.id}`, callback, data);
}
async Delete(data, callback) {
this.SendRequest("delete", `${this.BASE_URL}/${data.id}`, callback, data);
}
async SendRequest(method, url, callback, data) {
try {
callback((await Axios.request({
method: method,
url: url,
data: data
})).data);
} catch(err) {
this.handleError("Operation Failed: Network Error");
}
}
}
Listing 23-16Handling Errors in the RestDataSource.js File in the src/webservice Folder
通过SendRequest方法合并所有请求的好处是,我可以使用一个单独的try / catch块来处理所有请求类型的错误。catch块处理由请求引起的错误,并调用作为构造函数参数接收的回调函数。
向用户显示错误消息
当出现问题时,Axios 包会显示详细的错误,包括响应的状态代码和 web 服务提供的任何描述性文本。然而,对于大多数应用来说,将这些信息呈现给用户是没有意义的,因为用户不知道发生了什么,也不知道如何修复。相反,我建议向用户显示一条一般的错误消息,并在服务器上记录问题的详细信息,以便可以识别常见问题。
为了接收错误并将它们显示给用户,我在src/webservice文件夹中添加了一个名为RequestError.js的文件,并用它来定义清单 23-17 中所示的组件。
import React, { Component } from "react";
import { Link } from "react-router-dom";
export class RequestError extends Component {
render() {
return <div>
<h5 className="bg-danger text-center text-white m-2 p-3">
{ this.props.match.params.message }
</h5>
<div className="text-center">
<Link to="/" className="btn btn-secondary">OK</Link>
</div>
</div>
}
}
Listing 23-17The Contents of the RequestError.js File in the src/webservice Folder
该组件显示从 URL 参数获得的消息。清单 23-18 向Selector组件添加了一个新的Route,它将为/error URL 显示这个组件。
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch, Redirect }
from "react-router-dom";
import { ToggleLink } from "./routing/ToggleLink";
import { RoutedDisplay } from "./routing/RoutedDisplay";
import { IsolatedTable } from "./IsolatedTable";
import { IsolatedEditor } from "./IsolatedEditor";
import { RequestError } from "./webservice/RequestError";
export class Selector extends Component {
render() {
const routes = React.Children.map(this.props.children, child => ({
component: child,
name: child.props.name,
url: `/${child.props.name.toLowerCase()}`,
datatype: child.props.datatype
}));
return <Router getUserConfirmation={ this.customGetUserConfirmation }>
<div className="container-fluid">
<div className="row">
<div className="col-2">
<ToggleLink to="/isolated">Isolated Data</ToggleLink>
{ routes.map(r => <ToggleLink key={ r.url } to={ r.url }>
{ r.name }
</ToggleLink>)}
</div>
<div className="col">
<Switch>
<Route path="/isolated" component={ IsolatedTable }
exact={ true } />
<Route path="/isolated/:mode/:id?"
component={ IsolatedEditor } />
<Route path="/error/:message"
component={ RequestError } />
{ routes.map(r =>
<Route key={ r.url }
path={ `/:datatype(${r.datatype})/:mode?/:id?`}
component={ RoutedDisplay(r.datatype)} />
)}
<Redirect to={ routes[0].url } />
</Switch>
</div>
</div>
</div>
</Router>
}
}
Listing 23-18Adding a Route in the Selector.js File in the src Folder
清单 23-19 为数据源提供了一个回调函数,当问题出现时,它会导航到/error URL,并添加一个按钮,通过请求一个总是会产生 404-Not Found 错误的 URL 来创建一个错误。
import React, { Component } from "react";
import { RestDataSource } from "./webservice/RestDataSource";
import { Link } from "react-router-dom";
export class IsolatedTable extends Component {
constructor(props) {
super(props);
this.state = {
products: []
}
this.dataSource = new RestDataSource("http://localhost:3500/api/products",
(err) => this.props.history.push(`/error/${err}`));
}
deleteProduct(product) {
this.dataSource.Delete(product,
() => this.setState({products: this.state.products.filter(p =>
p.id !== product.id)}));
}
render() {
return <table className="table table-sm table-striped table-bordered">
<thead>
<tr><th colSpan="5"
className="bg-info text-white text-center h4 p-2">
(Isolated) Products
</th></tr>
<tr>
<th>ID</th><th>Name</th><th>Category</th>
<th className="text-right">Price</th>
<th></th>
</tr>
</thead>
<tbody>
{
this.state.products.map(p => <tr key={ p.id }>
<td>{ p.id }</td><td>{ p.name }</td><td>{p.category}</td>
<td className="text-right">
${ Number(p.price).toFixed(2)}
</td>
<td>
<Link className="btn btn-sm btn-warning mx-2"
to={`/isolated/edit/${p.id}`}>
Edit
</Link>
<button className="btn btn-sm btn-danger mx-2"
onClick={ () => this.deleteProduct(p)}>
Delete
</button>
</td>
</tr>)
}
</tbody>
<tfoot>
<tr className="text-center">
<td colSpan="5">
<Link to="/isolated/create"
className="btn btn-info">Create</Link>
<button className="btn btn-danger mx-2"
onClick={ () => this.dataSource.GetOne("err")}>
Error
</button>
</td>
</tr>
</tfoot>
</table>
}
componentDidMount() {
this.dataSource.GetData(data => this.setState({products: data}));
}
}
Listing 23-19Handling Errors in the IsolatedTable.js File in the src Folder
点击由IsolatedTable呈现的错误按钮将发送一个请求,该请求从 web 服务接收一个错误响应,这触发导航到显示错误消息的 URL,如图 23-5 所示。
图 23-5
显示错误消息
提出跨来源请求
默认情况下,浏览器会强制执行一个安全策略,只允许 JavaScript 代码在包含异步 HTTP 请求的文档的同一来源内发出这些请求。该政策旨在降低跨站点脚本(CSS)攻击的风险,这种攻击会诱使浏览器执行恶意代码,详见 http://en.wikipedia.org/wiki/Cross-site_scripting 。对于 web 应用开发人员来说,同源策略在使用 web 服务时可能是一个问题,因为它们通常位于包含应用 JavaScript 代码的源之外。如果两个 URL 具有相同的协议、主机和端口,则它们被认为是来源相同,否则它们具有不同的来源。我在本章中为 RESTful web 服务使用的 URL 与主应用使用的 URL 有不同的来源,因为它们使用不同的 TCP 端口。
跨源资源共享(CORS)协议用于向不同的源发送请求。使用 CORS,浏览器在异步 HTTP 请求中包含标头,向服务器提供 JavaScript 代码的来源。来自服务器的响应包括告诉浏览器它是否愿意接受请求的头。CORS 的详细情况不在本书讨论范围之内,但在 https://en.wikipedia.org/wiki/Cross-origin_resource_sharing 有题目介绍,在 www.w3.org/TR/cors 有 CORS 规格。
CORS 是在这一章中自动发生的事情。提供 RESTful web 服务的json-server包支持 CORS,并将接受来自任何来源的请求,而我用来发出 HTTP 请求的 Axios 包自动应用 CORS。当您为自己的项目选择软件时,您必须选择一个允许通过单一来源处理所有请求的平台,或者配置 CORS 以便服务器接受应用的数据请求。
使用带有数据存储的 Web 服务
我在上一节中定义的组件是相互隔离的,只能通过 URL 路由系统进行协调。这种方法的优点是简单,但是当用户在应用中导航时,它会导致重复地从 web 服务请求相同的数据,并且每个组件在安装时都会发送它的 HTTP 请求。如果应用使用数据存储,那么数据可以在组件之间共享。
创建新的中间件
商店已经有了接收对象和更新数据的动作,所以我要采用的方法是创建新的 Redux 中间件,它将拦截现有的动作,并向 web 服务发送相应的 HTTP 请求。我在src/webservice文件夹中添加了一个名为RestMiddleware.js的文件,内容如清单 23-20 所示。
import { STORE, UPDATE, DELETE} from "../store/modelActionTypes";
import { RestDataSource } from "./RestDataSource";
import { PRODUCTS, SUPPLIERS } from "../store/dataTypes";
export const GET_DATA = "rest_get_data";
export const getData = (dataType) => {
return {
type: GET_DATA,
dataType: dataType
}
}
export const createRestMiddleware = (productsURL, suppliersURL) => {
const dataSources = {
[PRODUCTS]: new RestDataSource(productsURL, () => {}),
[SUPPLIERS]: new RestDataSource(suppliersURL, () => {})
}
return ({dispatch, getState}) => next => action => {
switch (action.type) {
case GET_DATA:
if (getState().modelData[action.dataType].length === 0) {
dataSources[action.dataType].GetData((data) =>
data.forEach(item => next({ type: STORE,
dataType: action.dataType, payload: item})));
}
break;
case STORE:
action.payload.id = null;
dataSources[action.dataType].Store(action.payload, data =>
next({ ...action, payload: data }))
break;
case UPDATE:
dataSources[action.dataType].Update(action.payload, data =>
next({ ...action, payload: data }))
break;
case DELETE:
dataSources[action.dataType].Delete({id: action.payload },
() => next(action));
break;
default:
next(action);
}
}
}
Listing 23-20The Contents of the RestMiddleware.js File in the src/webservice Folder
需要一个新动作,即从 web 服务请求数据。以前不需要这样做,因为数据存储已经用数据自动初始化了。动作类型为GET_DATA,清单 23-20 定义了一个getData动作创建者。
createRestMiddleware函数接受产品和供应商数据的数据源,并返回处理新的GET_DATA动作和现有的STORE、UPDATE和DELETE动作的中间件,方法是向 web 服务发送一个请求,然后在收到结果时使用数据存储的现有特性分派附加动作。
将中间件添加到数据存储中
在清单 23-21 中,我已经将新的中间件添加到数据存储中。如第二十章所述,中间件组件是按照它们被添加到商店的顺序来应用的。
import { createStore, combineReducers, applyMiddleware, compose } from "redux";
import modelReducer from "./modelReducer";
import stateReducer from "./stateReducer";
import { customReducerEnhancer } from "./customReducerEnhancer";
import { multiActions } from "./multiActionMiddleware";
import { asyncEnhancer } from "./asyncEnhancer";
import { createRestMiddleware } from "../webservice/RestMiddleware";
const enhancedReducer = customReducerEnhancer(
combineReducers(
{
modelData: modelReducer,
stateData: stateReducer
})
);
const restMiddleware = createRestMiddleware(
"http://localhost:3500/api/products",
"http://localhost:3500/api/suppliers");
export default createStore(enhancedReducer,
compose(applyMiddleware(multiActions),
applyMiddleware(restMiddleware),
asyncEnhancer(2000)));
export { saveProduct, saveSupplier, deleteProduct, deleteSupplier }
from "./modelActionCreators";
Listing 23-21Applying Middleware in the index.js File in the src/store Folder
在考虑应用中现有组件如何使用数据存储时,顺序非常重要。在第二十章中创建的multiActions中间件允许一系列动作被分派,这必须放在第一位;否则,新的中间件将不能正确地处理动作。
完成应用更改
为了按需自动请求数据,我在src文件夹中添加了一个名为DataGetter.js的文件,并用它来定义清单 23-22 中所示的高阶组件。
import React, { Component } from "react";
import { PRODUCTS, SUPPLIERS } from "./store/dataTypes";
export const DataGetter = (dataType, WrappedComponent) => {
return class extends Component {
render() {
return <WrappedComponent { ...this.props } />
}
componentDidMount() {
this.props.getData(PRODUCTS);
if (dataType === SUPPLIERS) {
this.props.getData(SUPPLIERS);
}
}
}
}
Listing 23-22The Contents of the DataGetter.js File in the src Folder
组件在安装后请求数据,并知道供应商数据必须由产品数据补充,以便向用户正确显示数据,从而可以显示产品名称。在清单 23-23 中,我在TableConnector组件中添加了对新的 HOC 的支持,这确保了应用启动时请求应用所需的数据。
import { connect } from "react-redux";
//import { startEditingProduct, startEditingSupplier } from "./stateActions";
import { deleteProduct, deleteSupplier } from "./modelActionCreators";
import { PRODUCTS, SUPPLIERS } from "./dataTypes";
import { withRouter } from "react-router-dom";
import { getData } from "../webservice/RestMiddleware";
import { DataGetter } from "../DataGetter";
export const TableConnector = (dataType, presentationComponent) => {
const mapStateToProps = (storeData, ownProps) => {
if (dataType === PRODUCTS) {
return { products: storeData.modelData[PRODUCTS] };
} else {
return {
suppliers: storeData.modelData[SUPPLIERS].map(supp => ({
...supp,
products: supp.products.map(id =>
storeData.modelData[PRODUCTS]
.find(p => p.id === Number(id)) || id)
.map(val => val.name || val)
}))
}
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
getData: (type) => dispatch(getData(type)),
deleteCallback: dataType === PRODUCTS
? (...args) => dispatch(deleteProduct(...args))
: (...args) => dispatch(deleteSupplier(...args))
}
}
const mergeProps = (dataProps, functionProps, ownProps) => {
let routedDispatchers = {
editCallback: (target) => {
ownProps.history.push(`/${dataType}/edit/${target.id}`);
},
deleteCallback: functionProps.deleteCallback,
getData: functionProps.getData
}
return Object.assign({}, dataProps, routedDispatchers, ownProps);
}
return withRouter(connect(mapStateToProps,
mapDispatchToProps, mergeProps)(DataGetter(dataType,
presentationComponent)));
}
Listing 23-23Dispatching Actions in the TableConnector.js File in the src/store Folder
最后一个变化是删除用于播种数据存储的静态内容,如清单 23-24 所示。
import { PRODUCTS, SUPPLIERS } from "./dataTypes";
export const initialData = {
modelData: {
[PRODUCTS]: [],
[SUPPLIERS]: []
},
stateData: {
editing: false,
selectedId: -1,
selectedType: PRODUCTS
}
}
Listing 23-24Removing the Static Data in the initialData.js File in the src/store Folder
结果是最初的产品和供应商数据是从 web 服务中获得的,任何变化都会触发 web 服务的更新,如图 23-6 所示。
图 23-6
将 web 服务与数据存储一起使用
摘要
在本章中,我介绍了一个 web 服务,并使用它来获取用户显示的数据、存储新数据、进行更改以及删除数据。在本章中,我使用了 Axios 库,但是还有许多其他可用的选项,在 React 应用中使用 web 服务是一个相对简单的过程。在下一章中,我将介绍 GraphQL,它是 REST 的一个更灵活的 web 服务替代方案。
二十四、了解 GraphQL
GraphQL 是一个用于创建和消费 API 的端到端系统,它提供了一种比使用传统 RESTful web 服务更灵活的替代方案,例如在第二十三章中创建的服务。在这一章中,我将解释 GraphQL 服务是如何定义的以及查询是如何执行的。在第二十五章中,我展示了 React 应用使用 GraphQL API 的不同方式。表 24-1 将 GraphQL 放在上下文中。
表 24-1
将 GraphQL 放在上下文中
|问题
|
回答
| | --- | --- | | 这是什么? | GraphQL 是一种生成 API 的查询语言。 | | 为什么有用? | GraphQL 为客户机提供了对数据的灵活访问,确保客户机只接收它需要的数据,并允许在不需要服务器端更改的情况下制定新的查询。 | | 如何使用? | 在服务器端,使用解析器函数定义和实现模式。客户端使用 GraphQL 语言发送查询和请求更改。 | | 有什么陷阱或限制吗? | GraphQL 很复杂,编写一个有用的模式可能需要技巧。 | | 还有其他选择吗? | 客户端可以使用 RESTful web 服务,如第二十三章所述。 |
注意
我描述了对 React 开发最有用的 GraphQL 特性。有关 GraphQL 的完整描述,请参见 https://facebook.github.io/graphql/June2018 的 GraphQL 规范。
表 24-2 总结了本章内容。
表 24-2
章节总结
|问题
|
解决办法
|
列表
| | --- | --- | --- | | 定义 GraphQL 服务 | 描述将被支持的查询和变异,并实现提供它们的解析器 | 3, 4, 8–10, 20–21 | | 查询 GraphQL 服务 | 指定查询名称和结果中需要的字段 | 7, 11, 27, 28 | | 过滤结果 | 指定查询参数 | 12–19 | | 使用 GraphQL 服务进行更改 | 为更新指定突变和字段 | 22–24 | | 参数化查询 | 使用查询变量 | 25, 26 | | 从多个查询中请求同一组字段 | 使用查询片段 | Twenty-nine |
为本章做准备
在本章中,我继续使用第二十三章中的示例应用。为了准备本章,打开一个命令提示符,导航到productapp文件夹,运行清单 24-1 中所示的命令,将包添加到项目中。
小费
你可以从 https://github.com/Apress/pro-react-16 下载本章以及本书其他章节的示例项目。
npm install --save-dev graphql@14.0.2
npm install --save-dev express@4.16.4
npm install --save-dev express-graphql@0.7.1
npm install --save-dev graphql-import@0.7.1
npm install --save-dev cors@2.8.5
Listing 24-1Adding Packages
为了快速参考,表 24-3 中描述了清单 24-1 中命令的包。
表 24-3
添加到项目中的包
|名字
|
描述
|
| --- | --- |
| graphql | 这个包包含 GraphQL 的参考实现。 |
| express | 这个包提供了一个可扩展的 HTTP 服务器,并将成为本章中使用的 GraphQL 服务器的基础。 |
| express-graphql | 这个包通过express包在 HTTP 上提供 GraphQL 服务。 |
| graphql-import | 这个包允许在多个文件中定义 GraphQL 模式,并且导入模式比直接读取文件更容易。 |
| cors | 此软件包为 Express HTTP 服务器启用跨源资源共享(CORS)。 |
一旦软件包安装完毕,使用命令提示符运行清单 24-2 中的命令来启动开发工具。第二十三章中定义的 RESTful web 服务也被启动,并且仍然被应用使用。
npm start
Listing 24-2Starting the Development Tools
一旦项目的初始准备完成,一个新的浏览器窗口将打开并显示 URL http://localhost:3000,如图 24-1 所示。
图 24-1
运行示例应用
了解 GraphQL
RESTful web 服务很容易上手,但是随着客户端需求的发展和使用该服务的客户端应用数量的增加,它们可能会变得不灵活。
无法做出适合一个应用的更改,因为它们会在另一个应用中引起问题,工作会积压,因此无法在客户端应用发布日期前及时做出更改,并且基础架构开发团队要努力平衡对功能的竞争需求。当您依赖第三方 web 服务时,您可能没有任何途径来请求更改,因为您是所有要求新功能的几十或几百个开发团队中的一员。
结果是应用和它所依赖的 web 服务之间的不匹配。客户端经常需要向 web 服务发出多个请求来获取它们需要的数据;这然后被合并成一个有用的格式。客户端必须理解从不同 REST 请求返回的对象是如何相互关联的,并且经常必须请求数据,然后因为只需要数据字段的子集而丢弃这些数据。
REST web 服务的根本问题是它们提供的数据和提供数据的方式是固定的,随着客户端应用需求的变化,这就成了一个问题。GraphQL 通过允许客户端更多地控制请求什么数据以及如何表达数据来解决这个问题,结果是客户端应用可以添加以新的方式使用数据的功能,而只需要较少的服务器端更改。
了解 GraphQL 的缺点
GraphQL 并不适合所有情况。GraphQL 很复杂,不像 REST 那样被广泛理解,这使得很难找到有经验的开发人员和健壮且经过良好测试的工具和库。GraphQL 可以将原本由客户端应用执行的工作转移到服务器上,这会增加数据中心的成本,并且需要支持 GraphQL 的后端服务器的许可证。
将 GraphQL 作为一个选项来考虑是很重要的,特别是如果您的应用在部署后可能需要持续的开发,或者您打算开发或支持多个客户端应用。但是我的建议是不要急于使用 GraphQL,直到你确定 REST web 服务不会给你所需要的灵活性。
创建 GraphQL 服务器
我将创建一个自定义的 GraphQL 服务器,它提供与第二十三章中的 web 服务相同的数据。创建 GraphQL 服务的过程并不是所有项目都需要的,尤其是在使用第三方 API 的时候,但是了解服务器上发生的事情有助于深入了解 GraphQL 是如何工作的。在接下来的小节中,我将介绍描述客户机能够发出的请求类型的过程,并编写处理这些请求所需的代码。
选择替代的 GraphQL 服务器
我使用 GraphQL 参考实现为本章创建了一个简单的 GraphQL 服务器。它很容易演示 GraphQL 是如何工作的,但没有为处理真实数据做任何准备。
对于小而简单的项目,用 Lowdb ( https://github.com/typicode/lowdb )或 MongoDB ( https://www.mongodb.com )之类的包添加持久数据支持可能比较合适。
对于更复杂的项目,阿波罗服务器( https://github.com/apollographql/apollo-server )是最常见的选择。有开源的和付费的计划可用,有大量的数据集成选项可用,比如使用 GraphQL 作为现有 REST web 服务的前端。
创建模式
GraphQL 描述了可以使用模式执行的请求,该模式是用 GraphQL 模式语言编写的。我创建了src/graphql文件夹,并在其中添加了一个名为schema.graphql的文件,其内容如清单 24-3 所示。
type product {
id: ID!,
name: String!,
category: String!
price: Float!
}
type supplier {
id: ID!,
name: String!,
city: String!,
products: [ID]
}
type Query {
products: [product],
suppliers: [supplier]
}
Listing 24-3The Contents of the schema.graphql File in the src/graphql Folder
清单 24-3 中定义的模式定义了两个定制类型:product和supplier。这些类型将被用作 GraphQL 服务器支持的查询结果。每种结果类型都由一组字段定义,每个字段的类型如下:
...
category: String!
...
该字段的名称为category,类型为String。GraphQL 提供了一组内置类型,如表 24-4 所述。字段类型后的感叹号(!字符)表示该字段的值是必填的。字段也可以返回值数组,如下所示:
表 24-4
内置的 GraphQL 类型
|名字
|
描述
|
| --- | --- |
| ID | 此类型表示唯一的标识符。 |
| String | 此类型表示一个字符串。 |
| Int | 此类型表示一个有符号整数 |
| Float | 此类型表示浮点值 |
| Boolean | 这种类型代表一个true或false值。 |
...
products: [ID]
...
方括号表示supplier类型的products字段将是一个ID值的数组。
小费
目前不要太担心 GraphQL 类型的系统。当您看到服务器的不同部分如何组合在一起并被客户端使用时,这将变得更有意义。
除了内置类型,GraphQL 还支持Query类型,用于定义服务器将支持的查询。在清单 24-3 的模式中定义了两个查询。
...
type Query {
products: [product],
suppliers: [supplier]
}
...
第一条语句定义了一个名为products的查询,该查询将返回一组product对象。第二条语句定义了一个名为suppliers的查询,该查询将返回一组supplier对象。
创建解析器
下一步是编写实现清单 24-3 中定义的products和suppliers查询的函数。我在src/graphql文件夹中添加了一个名为resolvers.js的文件,代码如清单 24-4 所示。
var data = require("../../restData")();
module.exports = {
products: () => data.products,
suppliers: () => data.suppliers
}
Listing 24-4The Contents of the resolvers.js File in the src/graphql Folder
每个解析器都是一个函数,其名称对应于一个查询,并以模式声明的格式返回数据。products和suppliers解析器使用的数据使用从restData.js文件加载的数据。
注意
GraphQL 服务器将由 Node.js 运行,而 node . js 在本文撰写时并不支持 JavaScript 模块,这意味着不能使用import和export关键字。相反,require函数用于声明对文件的依赖,module.exports用于使代码或数据在 JavaScript 文件之外可用。
创建服务器
最后一步是创建处理模式和解析器的代码,并创建 GraphQL 服务器。我在productapp文件夹中添加了一个名为graphqlServer.js的文件,并添加了清单 24-5 中所示的代码。
var { buildSchema } = require("graphql");
var { importSchema } = require("graphql-import");
var express = require("express");
var graphqlHTTP = require("express-graphql")
var cors = require("cors")
var schema = importSchema("./src/graphql/schema.graphql");
var resolvers = require("./src/graphql/resolvers");
var app = express();
app.use(cors());
app.use("/graphql", graphqlHTTP({
schema: buildSchema(schema),
rootValue: resolvers,
graphiql: true,
}));
app.listen(3600, () => console.log("GraphQL Server Running on Port 3600"));
Listing 24-5The Contents of the graphqlServer.js File in the productapp Folder
graphql包提供了buildSchema函数,它获取一个模式字符串并准备使用。模式文件的内容使用graphql-import包导入,并传递给buildSchema函数。express-graphql包将 GraphQL 支持集成到流行的express服务器中,我已经将它配置为在端口 3600 上监听。
要启动 GraphQL 服务器,打开一个新的命令提示符,导航到productapp文件夹,并运行清单 24-6 中所示的命令。(当模式或解析器发生变化时,GraphQL 服务器不会自动重新加载,对于本章中的一些示例,它必须重启,这就是为什么我没有将它集成到npm start命令中,就像我对 RESTful web 服务所做的那样。)
node graphqlServer.js
Listing 24-6Starting the GraphQL Server
GraphQL 服务器包括对 GraphQL(读作 graphical )的支持,这是一个基于浏览器的 graph QL 工具。为了确保 GraphQL 服务器正在工作,打开一个新的浏览器选项卡并导航到http://localhost:3600/graphql,这将显示图 24-2 中的工具。
图 24-2
图形浏览器
进行 GraphQL 查询
GraphQL 工具使得在将 graph QL 集成到示例应用之前执行查询变得很容易。例如,要查询所有的供应商对象,在 GraphiQL 窗口的左窗格中输入清单 24-7 中所示的查询。
query {
suppliers {
id,
name,
city,
products
}
}
Listing 24-7A Query for Supplier Data
该查询是基本的,但它揭示了许多关于 GraphQL 查询如何工作的信息。query关键字用于区分检索数据的请求和突变,后者用于进行更改(在“进行 GraphQL 突变”一节中有描述)。查询本身被括在花括号中,也称为括号。在大括号内,指定了查询名称,在本例中是suppliers。
当查询 GraphQL 服务时,必须指定想要接收的数据字段。与总是呈现相同数据结构的 REST web 服务不同,GraphQL 允许客户端指定它想要接收的结果,这些结果包含在另一组括号中。清单 24-7 中的查询选择了id、name、city和products字段。
注意
没有允许选择所有字段的通配符。如果要接收某个数据类型的所有字段,则必须在查询中包含所有字段。
单击 Execute Query 按钮将请求发送到 GraphQL 服务器,它将返回以下结果:
...
{
"data": {
"suppliers": [
{
"id": "1",
"name": "Surf Dudes",
"city": "San Jose",
"products": ["1","2"]
},
{
"id": "2",
"name": "Goal Oriented",
"city": "Seattle",
"products": ["3","4","5"]
},
{
"id": "3",
"name": "Bored Games",
"city": "New York",
"products": ["6","7","8","9"]
}
]
}
}
...
这似乎与 REST web 服务没有太大的不同,但是即使有了这个基本的查询,客户机也能够选择它需要的字段以及它们将被表达的顺序。
查询相关数据
GraphQL 服务正在工作,它可以用来获取product和supplier数据,满足示例应用中数据表的基本需求。然而,GraphQL 最强大的特性之一是它支持查询中的相关数据,允许单个查询返回包含多种类型的结果。在清单 24-8 中,我已经将products字段更改为supplier数据类型的模式。
获取 GraphQL 服务的架构详细信息
编写模式可以最好地洞察 GraphQL 服务支持的查询,但这并不总是可能的。如果您没有编写自己的模式,首先要做的是查找开发人员文档;许多公共的 GraphQL 服务发布了全面的模式文档,比如在 https://developer.github.com/v4 描述的 GitHub API。
许多服务还支持 GraphiQL 或类似的工具,其中大部分都支持模式导航。例如,GraphiQL 通过它的Docs链接可以很容易地浏览模式,让您浏览服务支持的查询和变化。
如果没有文档和对 GraphQL 的支持,您可以使用 graph QL 自省特性来发送关于模式的查询。例如,以下模式查询将列出服务支持的常规查询:
...
{
__schema {
queryType {
fields {
name
}
}
}
}
...
特殊的__schema查询数据类型用于请求关于模式的信息。你可以在 https://graphql.org/learn/introspection 找到 GraphQL 内省特性的更多细节。
type product {
id: ID!,
name: String!,
category: String!
price: Float!
}
type supplier {
id: ID!,
name: String!,
city: String!,
products: [product]
}
type Query {
products: [product],
suppliers: [supplier]
}
Listing 24-8Changing a Data Field in the schema.graphql File in the src/graphql Folder
products 字段现在返回一个由supplier对象组成的数组,而不是返回一个 ID 值数组。为了支持这种变化,我需要处理解析器使用的数据,以解析每个supplier及其相关的product对象之间的关系,如清单 24-9 所示。
var data = require("../../restData")();
module.exports = {
products: () => data.products,
suppliers: () => data.suppliers.map(s => ({
...s, products: () => s.products.map(id =>
data.products.find(p => p.id === Number(id)))
}))
}
Listing 24-9Resolving Related Data in the resolvers.js File in the src/graphql Folder
数据经过处理后,每个supplier对象都有一个products属性。products 属性是一个将解析相关数据的函数,只有当客户端请求该数据字段时才会被调用,这可以确保服务器不会获取没有被请求的数据。
使用 Control+C 停止 GraphQL 服务器,并运行在productapp文件夹中的清单 24-10 中显示的命令来再次启动它。
node graphqlServer.js
Listing 24-10Starting the GraphQL Server
导航到http://localhost:3600/graphql并将清单 24-11 中显示的查询输入到 GraphiQL 窗口的左窗格中。该查询利用对 GraphQL 模式的更改,在单个查询中请求供应商及其相关产品数据。
query {
suppliers {
id,
name,
city,
products {
name
}
}
}
Listing 24-11Querying for Related Data
当字段返回复杂类型时,如product,查询必须选择所需的字段。清单 24-11 中添加的查询要求服务器提供每个supplier对象的id、name和city字段以及每个相关product对象的name字段。单击执行查询按钮,您将收到以下结果:
...
{
"data": {
"suppliers": [
{
"id": "1", "name": "Surf Dudes", "city": "San Jose",
"products": [{ "name": "Kayak" }, { "name": "Lifejacket" }]
},
{
"id": "2", "name": "Goal Oriented", "city": "Seattle",
"products": [{ "name": "Soccer Ball" },{ "name": "Corner Flags" },
{ "name": "Stadium" }]
},
{
"id": "3", "name": "Bored Games", "city": "New York",
"products": [{ "name": "Thinking Cap" },{ "name": "Unsteady Chair" },
{ "name": "Human Chess Board" }, { "name": "Bling Bling King" }]
}
]
}
}
...
注意,客户机指定了supplier对象和相关的product数据所需的字段,这确保了只检索应用所需的数据。
注意
除了常规查询,GraphQL 规范还包括对订阅的支持,它为服务器上正在变化的数据提供持续更新。订阅没有得到广泛或一致的支持,我在这本书里也不描述。
创建带参数的查询
GraphQL 服务器目前提供的查询允许用户选择所需的字段,但不选择结果中的对象,这是对单个对象的请求的要求。为了让客户端能够定制请求,GraphQL 支持参数,如清单 24-12 所示。
type product {
id: ID!,
name: String!,
category: String!
price: Float!
}
type supplier {
id: ID!,
name: String!,
city: String!,
products: [product]
}
type Query {
products: [product],
product(id: ID!): product,
suppliers: [supplier]
supplier(id: ID!): supplier
}
Listing 24-12Using Arguments in the schema.graphql File in the src/graphql Folder
参数在查询名称后的括号中定义,每个参数都被分配一个名称和一个类型。在清单 24-12 中,我添加了名为product和supplier的查询,每个查询都定义了一个类型为ID的id参数,并用感叹号表示为强制的。在清单 24-13 中,我为使用id值选择数据对象的查询添加了解析器。
var data = require("../../restData")();
module.exports = {
products: () => data.products,
product: (args) => data.products.find(p => p.id === parseInt(args.id)),
suppliers: () => data.suppliers.map(s => ({
...s, products: () => s.products.map(id =>
data.products.find(p => p.id === Number(id)))
})),
supplier: (args) => {
const result = data.suppliers.find(s => s.id === parseInt(args.id));
if (result) {
return {
...result,
products: () => result.products.map(id =>
data.products.find(p => p.id === Number(id)))
}
}
}
}
Listing 24-13Defining Resolvers in the resolvers.js File in the src/graphql Folder
resolver 函数接收一个对象,该对象的属性对应于查询参数。为了获得查询中指定的id值,解析器函数读取args.id属性。我可以通过析构参数对象来简化这段代码,如清单 24-14 所示。
小费
注意,我使用了parseInt函数来转换id参数进行比较。使用===在 ID 值和 JavaScript 数字值之间进行直接比较将返回false。
var data = require("../../restData")();
module.exports = {
products: () => data.products,
product: ({id}) => data.products.find(p => p.id === parseInt(id)),
suppliers: () => data.suppliers.map(s => ({
...s, products: () => s.products.map(id =>
data.products.find(p => p.id === Number(id)))
})),
supplier: ({id}) => {
const result = data.suppliers.find(s => s.id === parseInt(id));
if (result) {
return {
...result,
products: () => result.products.map(id =>
data.products.find(p => p.id === Number(id)))
}
}
}
}
Listing 24-14Destructing Arguments in the resolvers.js File in the src/graphql Folder
重启 GraphQL 服务器,并在 graph QL 窗口中输入清单 24-15 中所示的查询。
query {
supplier(id: 1) {
id,
name,
city,
products {
name
}
}
}
Listing 24-15Querying with an Argument
该查询请求id值为 1 的供应商对象,并请求相关产品的id、name和city字段以及name字段,产生以下结果:
...
{
"data": {
"supplier": {
"id": "1",
"name": "Surf Dudes",
"city": "San Jose",
"products": [{ "name": "Kayak" },{ "name": "Lifejacket" }]
}
}
}
...
向字段添加参数
可以为单个字段定义参数,这允许客户端更具体地指定它需要的数据。在清单 24-16 中,我为supplier类型的模式定义添加了一个参数,这将允许客户通过名称过滤相关的产品对象。
type product {
id: ID!,
name: String!,
category: String!
price: Float!
}
type supplier {
id: ID!,
name: String!,
city: String!,
products(nameFilter: String = ""): [product]
}
type Query {
products: [product],
product(id: ID!): product,
suppliers: [supplier]
supplier(id: ID!): supplier
}
Listing 24-16Adding a Field Argument in the schema.graphql File in the src/graphql Folder
字段products已被重新定义,以接收字符串nameFilter参数。没有使用感叹号,这意味着参数是可选的。如果没有使用值,将使用空字符串的默认值。该参数的实现如清单 24-17 所示。
var data = require("../../restData")();
const mapIdsToProducts = (supplier, nameFilter) =>
supplier.products.map(id => data.products.find(p => p.id === Number(id)))
.filter(p => p.name.toLowerCase().includes(nameFilter.toLowerCase()));
module.exports = {
products: () => data.products,
product: ({id}) => data.products
.find(p => p.id === parseInt(id)),
suppliers: () => data.suppliers.map(s => ({
...s, products: ({nameFilter}) => mapIdsToProducts(s, nameFilter)
})),
supplier: ({id}) => {
const result = data.suppliers.find(s => s.id === parseInt(id));
if (result) {
return {
...result,
products: ({ nameFilter }) => mapIdsToProducts(result, nameFilter)
}
}
}
}
Listing 24-17Implementing a Field Argument in the resolvers.js File in the src/graphql Folder
为了支持字段参数,解析supplier对象的products属性的函数接受一个参数,该参数被解构以获得nameFilter值,并用于按名称过滤相关的产品对象。重启 GraphQL 服务器,并将清单 24-18 中所示的查询输入到 graph QL 中,查看字段参数是如何在查询中使用的。
query {
supplier(id: 1) {
id,
name,
city,
products(nameFilter: "ak") {
name
}
}
}
Listing 24-18Querying with a Field Argument
单击 Execute Query 按钮,您将看到以下结果,这些结果显示相关的产品对象已经被过滤,因此只包括那些名称字段包含ak的对象。
...
{
"data": {
"supplier": {
"id": "1",
"name": "Surf Dudes",
"city": "San Jose",
"products": [{ "name": "Kayak" }]
}
}
}
...
警告
每个请求都会调用用于接收字段参数的方法,这会给服务器带来大量工作。对于复杂的结果,可以考虑使用记忆化包,比如fast-memoize ( https://github.com/caiogondim/fast-memoize.js )。
因为字段参数应用于类型而不是特定的查询,所以该筛选器可用于任何包含相关产品数据的供应商数据查询。将清单 24-19 中所示的查询输入到 GraphiQL 中进行演示。
query {
suppliers {
id,
name,
city,
products(nameFilter: "g") {
name
}
}
}
Listing 24-19Using a Field Argument in Another Query
单击“执行查询”按钮,您将看到结果中每个供应商对象的相关产品数据已经被过滤。
...
{
"data": {
"suppliers": [
{
"id": "1",
"name": "Surf Dudes",
"city": "San Jose",
"products": []
},
{
"id": "2",
"name": "Goal Oriented",
"city": "Seattle",
"products": [{ "name": "Corner Flags" }]
},
{
"id": "3",
"name": "Bored Games",
"city": "New York",
"products": [{ "name": "Thinking Cap" }, { "name": "Bling Bling King"}]
}
]
}
}
...
制造 GraphQL 突变
突变用于要求 GraphQL 服务器对其数据进行更改。使用特殊的Mutation类型将突变添加到模式中,有两种广泛的方法可用,如清单 24-20 所示。
type product {
id: ID!,
name: String!,
category: String!
price: Float!
}
type supplier {
id: ID!,
name: String!,
city: String!,
products(nameFilter: String = ""): [product]
}
type Query {
products: [product],
product(id: ID!): product,
suppliers: [supplier]
supplier(id: ID!): supplier
}
input productInput {
id: ID, name: String!, category: String!, price: Int!
}
type Mutation {
storeProduct(product: productInput): product
storeSupplier(id: ID, name: String!, city: String!, products: [Int]): supplier
}
Listing 24-20Defining Mutations in the schema.graphql File in the src/graphql Folder
第一种变异称为storeProduct,使用专用的输入类型,允许客户端提供值来描述所需的更改。输入类型是使用input关键字定义的,支持与常规类型相同的特性。在清单中,我定义了一个名为productInput的输入类型,它有一个可选的id字段和强制的name、category和price字段。这基本上是已经在模式中定义的product类型的复制,这是一种常见的方法,因为您不能使用常规类型作为突变的参数。
storeSupplier突变采用了一种简单的方法,即定义多个参数,允许客户端在不需要输入类型的情况下表达数据对象的细节。这对于基本突变是一种有效的方法,但是对于复杂突变来说可能变得不实用。两种变异都会产生一个结果,该结果为客户端提供了一个对象的权威视图,该视图是作为变异的结果而创建或更新的,并使用常规查询类型来表达。在清单 24-21 中,我为突变添加了解析器。
var data = require("../../restData")();
const mapIdsToProducts = (supplier, nameFilter) =>
supplier.products.map(id => data.products.find(p => p.id === Number(id)))
.filter(p => p.name.toLowerCase().includes(nameFilter.toLowerCase()));
let nextId = 100;
module.exports = {
products: () => data.products,
product: ({id}) => data.products
.find(p => p.id === parseInt(id)),
suppliers: () => data.suppliers.map(s => ({
...s, products: ({nameFilter}) => mapIdsToProducts(s, nameFilter)
})),
supplier: ({id}) => {
const result = data.suppliers.find(s => s.id === parseInt(id));
if (result) {
return {
...result,
products: ({ nameFilter }) => mapIdsToProducts(result, nameFilter)
}
}
},
storeProduct({product}) {
if (product.id == null) {
product.id = nextId++;
data.products.push(product);
} else {
product = { ...product, id: Number(product.id)};
data.products = data.products
.map(p => p.id === product.id ? product : p);
}
return product;
},
storeSupplier(args) {
const supp = { ...args, id: Number(args.id)};
if (args.id == null) {
supp.id = nextId++;
data.suppliers.push(supp)
} else {
data.suppliers = data.suppliers.map(s => s.id === supp.id ? supp: s);
}
let result = data.suppliers.find(s => s.id === supp.id);
if (result) {
return {
...result,
products: ({ nameFilter }) => mapIdsToProducts(result, nameFilter)
}
}
}
}
Listing 24-21Implementing Mutations in the resolvers.js File in the src Folder
这些变异被实现为接收参数的函数,就像查询一样。这些变化使用 ID 字段来确定客户机是更新现有对象还是存储新对象,并且它们更新查询使用的表示数据以反映变化。要更新带有storeProduct突变的产品,重启服务器并将清单 24-22 中所示的 GraphQL 输入 graph QL。
mutation {
storeProduct(product: {
id: 1,
name: "Green Kayak",
category: "Watersports",
price: 290
}) {
id, name, category, price
}
}
Listing 24-22Using the storeProduct Mutation
使用关键字mutation执行突变,它是上一个示例中使用的关键字query的对应关键字。指定了突变的名称,以及提供了id、name、category和price的product参数。然后指定结果中需要的字段,在这种情况下,选择产品定义的所有字段。
单击执行查询按钮,您将看到以下结果:
...
{
"data": {
"storeProduct": {
"id": "1",
"name": "Green Kayak",
"category": "Watersports",
"price": 290
}
}
}
...
为了确认变异已经生效,使用 GraphiQL 执行清单 24-23 中的查询。
query {
product(id: 1) {
id, name, category, price
}
}
Listing 24-23Querying Product Data
当您执行查询时,您将看到以下结果,这些结果反映了突变所做的更改:
...
{
"data": {
"product": {
"id": "1",
"name": "Green Kayak",
"category": "Watersports",
"price": 290
}
}
}
...
使用不依赖于输入类型的变异的过程是相似的,如清单 24-24 所示。
mutation {
storeSupplier(
name: "AcmeCo",
city: "Chicago",
products: [1, 3]
){ id, name, city, products {
name
}
}
}
Listing 24-24Using a Mutation Without an Input Type
执行查询时,将创建一个新的供应商,并显示以下结果:
...
{
"data": {
"storeSupplier": {
"id": "100",
"name": "AcmeCo",
"city": "Chicago",
"products": [{ "name": "Green Kayak" }, { "name": "Soccer Ball" }]
}
}
}
...
注意,变异使用product字段中的id值来表示供应商和产品对象之间的关系,但是结果包括产品名称。变异从更新的表示数据中获得结果,表明变异的结果不需要与它接收的数据直接相关。
其他 GraphQL 特性
为了完成这一章,我将描述一些建立在前面描述的基础上的有用的特性。这些都是可选的,但是它们可以用来使 GraphQL 服务更容易使用。
使用请求变量
GraphQL 变量旨在允许一个请求被定义一次,然后在每次使用时使用参数进行定制,而不强制客户端为每个操作动态生成和序列化完整的请求数据。清单 24-25 中显示的查询定义了一个变量,该变量用作产品查询的参数。
query ($id: ID!) {
product(id: $id) {
id, name, category, price
}
}
Listing 24-25A Query with a Variable
变量被应用于查询或变异,并使用以美元符号($字符)开头的名称和分配的类型进行定义。在这种情况下,查询定义了一个名为id的变量,其类型是强制的ID。在查询内部,变量被用作$id,并被传递给product查询参数。
要使用该变量,请将查询输入 GraphiQL 展开窗口左下方的查询变量部分。并输入清单 24-26 所示的代码。
{
"id": 2
}
Listing 24-26Defining a Value for a Variable
这为id变量提供了值 2。点击执行查询按钮,查询和变量将被发送到 GraphQL 服务器,结果是id为 2 的产品对象被选中,如图 24-3 所示。
图 24-3
使用查询变量
使用 GraphiQL 时,变量可能看起来没什么用,但它们可以简化客户端开发,如第二十四章所示。
提出多个请求
一个操作可以包含多个请求或变异。在 GraphiQL 窗口中输入清单 24-27 中所示的查询。
query {
product(id: 1) {
id, name, category, price
},
supplier(id: 1) {
id, name, city
}
}
Listing 24-27Making Multiple Queries
查询由逗号分隔,包含在外层大括号中,跟在关键字query后面。单击 Execute Query 按钮,您将看到以下输出,它将两个查询的结果合并成一个响应:
...
{
"data": {
"product": {
"id": "1",
"name": "Kayak",
"category": "Watersports",
"price": 275
},
"supplier": {
"id": "1",
"name": "Surf Dudes",
"city": "San Jose"
}
}
}
...
请注意,每个查询的名称用于表示其响应部分,这使得区分来自product和supplier查询的结果变得容易。当您希望多次使用同一个查询时,这可能会带来一个问题,因此 GraphQL 支持别名,即分配一个应用于结果的名称。将清单 24-28 中所示的查询输入到 GraphiQL 中。
query {
first: product(id: 1) {
id, name, category, price
},
second: product(id: 2) {
id, name, category, price
}
}
Listing 24-28Using a Query Alias
别名出现在查询之前,后跟一个冒号(:字符)。在清单中,有两个product查询被赋予了别名first和second。单击 Execute Query 按钮,您将看到这些名称是如何在查询结果中使用的。
...
{
"data": {
"first": {
"id": "1",
"name": "Kayak",
"category": "Watersports",
"price": 275
},
"second": {
"id": "2",
"name": "Lifejacket",
"category": "Watersports",
"price": 48.95
}
}
}
...
使用查询片段进行字段选择
从每个查询中选择结果字段的要求会导致客户端中的重复,如清单 24-28 ,其中first和second查询都选择了id、name、category和price字段。可以使用 GraphQL 片段特性定义一次字段选择,然后应用于多个请求。在清单 24-29 中,我定义了一个片段,并在查询中使用了它。
fragment coreFields on product {
id, name, category
}
query {
first: product(id: 1) {
...coreFields,
price
},
second: product(id: 2) {
...coreFields
}
}
Listing 24-29Using a Query Fragment
片段是使用关键字fragment和on定义的,并且特定于单一类型。在清单 24-29 中,该片段被命名为coreFields,并为product对象定义。spread 运算符用于应用片段,片段可以与常规字段选择混合使用。单击执行查询按钮,您将看到以下结果:
...
{
"data": {
"first": {
"id": "1",
"name": "Kayak",
"category": "Watersports",
"price": 275
},
"second": {
"id": "2",
"name": "Lifejacket",
"category": "Watersports"
}
}
}
...
摘要
在这一章中,我介绍了 GraphQL。我解释了模式及其解析器的作用,并演示了为静态数据创建简单的 GraphQL 服务的过程。我向您展示了如何定义查询来从 GraphQL 服务中获取数据,以及如何使用突变来进行更改。本章中的所有示例都是使用 GraphQL 工具执行的,在下一章中,我将向您展示如何在 React 应用中使用 graph QL。