第十章 GraphQL接口交互

  • GraphQL query and mutation syntax
  • Using axios as a GraphQL client
  • Using Apollo GraphQL client
  • Working with cached data in Apollo

GraphQL很少被用到,本章仅作简单介绍和使用。

GraphQL是由Facebook开源的web API数据读写语言。它允许客户端,在一个请求中返回指定的数据内容。

GraphQL query and mutation syntax

首先介绍一下语法。

Reading GraphQL data

Basic query

  1. 首先打开 .
1
https://developer.github.com/v4/explorer/
  1. 输入下面内容并执行 .
1
2
3
4
5
query {
viewer {
name
}
}

其中:

  • query关键字开启了查询,它是可选的。
  • viewer是我们需要获取的对象。
  • nameviewer的名字,作为返回。

执行后返回的是JSON对象。

  1. 我们可以从结果部分查看文档说明。
  2. 从文档得知,我们还可以添加额外的查询字段:
1
2
3
4
5
6
query {
viewer {
name
avatarUrl
}
}

Returning nested data

复杂一点,我们希望查询github的start和issue的数量并返回。

  1. 首先输入下面查询:
1
2
3
4
5
6
query {
repository(owner:"facebook", name: "react") {
name
description
}
}

这次传递了两个参数,查找owner为Facebook,name为react的仓储.以及将 name 和 description作为返回值。

  1. 下面希望获取得到star的数量。
1
2
3
4
5
6
7
8
9
query {
repository(owner:"facebook",name:"react") {
name
description
stargazers {
totalCount
}
}
}
  1. 指定别名:
1
2
3
stargazers {
stars:totalCount
}

执行查询语句后,可以看到返回的star已被别名代替:

1
2
3
4
5
6
7
8
9
{
"data": {
"repository": {
"name": "react",
"description": "A declarative, efficient, and flexible
JavaScript library for building user interfaces.", "stargazers": {
"stars": 114998 }
} }
}
  1. 查找最近的5个issues信息,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
repository(owner: "facebook", name:"react") {
name
description
stargazers {
stars:totalCount
}
issues(last: 5) {
edges {
node {
id
title
url
publishedAt
}
}
}
}
}

这里的查询用edgesnode包装了结构,用于cursor-based的分页。

Query parameters

前面的查询是以硬编码的方式,我们希望定义变量查询。

  1. 我们可以在query关键字后面添加查询变量。这些参数需要声明它的类型、变量名。其中变量名要以$开头。类型后要带!。如下:
1
2
3
4
5
query ($org: String!, $repo: String!) {
repository(owner:$org, name:$repo) {
...
}
}
  1. 在执行之前,我们需要在Query Variables面板添加变量入参:
1
2
3
4
{
"org": "facebook",
"repo": "react"
}

Writing GraphQL data

GraphQL的数据写入需要创建mutation

  1. 要给github的repo加星,首先需要获得id.
1
2
3
4
5
6
query($org: String!, $repo: String!) {
repository(owner:$org, name:$repo) {
id
...
}
}
  1. 将上面查询的返回id拷贝,该id格式如下:
1
MDEwOlJlcG9zaXRvcnkxMDI3MDI1MA==
  1. 创建第一个mutation
1
2
3
4
5
6
7
8
9
mutation ($repoId: ID!) {
addStar(input: { starrableId: $repoId }) {
starrable {
stargazers {
totalCount
}
}
}
}

其中:

  • 数据变更开始于mutation关键字
  • 参数可以放在括号内
  • addStar是一个mutation函数,包含一个input参数
  • input实际上是一个对象,包含字段starrabledId,即我们需要星标的id,这里传入了$repoId
  • 之后我们指定了它的返回内容
  1. Query Variables面板填入参数:
1
2
3
{
"repoId": "MDEwOlJlcG9zaXRvcnkxMDI3MDI1MA=="
}
  1. 执行之后发现star数增加了

Using axios as a GraphQL client

Getting a GitHub personal access token

在此之前,首先需要到GitHub获取一个token。

Creating our app

(略)

Querying the GraphQL server

现在有了TypeScript版的React应用了,下面使用axios来进行GraphQL的查询:

  1. Header.tsx,创建两个接口表示GraphQL查询的返回数据:
1
2
3
4
5
6
7
8
9
interface IViewer {
name: string;
avatarUrl: string;
}
interface IQueryResult {
data: {
viewer: IViewer;
};
}
  1. Header组件创建一些状态块:
1
const [viewer, setViewer]: [IViewer, (viewer: IViewer) => void] = React.useState({name: "", avatarUrl: ""});
  1. 在组件被挂载时声明周期中开始GraphQL的查询。我们使用了useEffect函数:
1
2
3
React.useEffect(() => {
// TODO - make a GraphQL query
}, []);

第二个参数使用了一个空数组,这样仅在组件被挂载时进行查询,而不是每次都查询。

  1. 然后使用axios进行GraphQL的查询操作:
1
2
3
4
5
6
7
8
9
10
React.useEffect(() => {
axios.post<IQueryResult>("https://api.github.com/graphql", {
query: `query {
viewer {
name
avatarUrl
}
}`
})
}, []);

GraphQL要求使用HTTP POST方法,因为查询语句在方法体内。

  1. 另外还需要在标头带上bearer token进行认证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
axios.post<IQueryResult>(
'https://api.github.com/graphql',
{
query: `query {
viewer {
name
avatarUrl
}
}`
},
{
headers: {
Authorization: 'bearer our-bearer-token',
},
},
);

这里 token用前面申请的真实token。

  1. 现在还没有对返回内容做处理,因此:
1
2
3
4
5
6
axios.post<IQueryResult>(
...,
).then(response => {
setViewer(response.data.data.viewer);
});

  1. 现在数据已经存入了state,对页面内容渲染即可:
1
2
3
4
5
6
7
return (
<div>
<img src={viewer.avatarUrl} className="avatar" />
<div className="viewer">{viewer.name}</div>
<h1>GitHub Search</h1>
</div>
);
  1. 添加一些样式到App.css:
1
2
3
4
.avatar {
width: 60px;
border-radius: 50%;
}

回到运行的页面,可以看到我们的头像和标题。

我们发现,所有的GraphQL的请求都是HTTP POST,所有请求都指向同一个地址,请求的参数不是在URL地址,而是来源于请求体。因此,当我们使用诸如axios这种HTTP标准库会觉得怪怪的。

我们希望有一种更自然的方式。

Using Apollo GraphQL client

Apollo是一个用于交互GraphQL服务端的客户端库。相比axios它有更多的优势,譬如对数据的读写缓存。

Installing Apollo client

  1. 首先安装相应的包:
1
npm install apollo-boost react-apollo graphql
  • apollo-boost包含了Apollo客户端的所有内容
  • react-apollo包含有用于交互GraphQL服务的React 组件
  • graphql是个用于解析GraphQL查询的核心包
  1. 另外需要安装一些TypeScript的类型:
1
npm install @types/graphql --save-dev
  1. 还需要确保编译时,应该包含es2015esNext的依赖库。在tsconfig.json添加lib中:
1
2
3
4
5
6
7
8
{
"compilerOptions": {
"target": "es5",
"lib": ["es2015", "dom", "esnext"],
...
},
...
}

Migrating from axios to Apollo

迁移到apollo上。

Adding an Apollo provider

首先从App.tsx入手,将会定义我们的Apollo客户端,并_提供_给App所有子组件下使用。

  1. App.tsx,导入依赖:
1
2
import ApolloClient from "apollo-boost"
import{ ApolloProvider } from "react-apollo";
  1. App类组件前面,创建客户端ApolloClient
1
2
3
4
5
6
const client = new ApolloClient({
uri: "https://api.github.com/graphql",
headers: {
authorization: `Bearer our-bearer-token`
}
});
  1. 最后一步是使用ApolloProvider组件并传入ApolloClient参数即可。
1
2
3
4
5
6
7
8
9
10
11
public render() {
return (
<ApolloProvider client={client}>
<div className="App">
<header className="App-header">
<Header />
</header>
</div>
</ApolloProvider>
);
}

现在ApolloClient已经设置好了,下面开始交互内容。

Using the query component to query GraphQL

我们现在使用Query组件来获取GitHub的名字和头像,替换掉axios代码:

  1. 导入下面依赖,并删掉原来的axios
1
2
import gql from "graphql-tag";
import { Query } from "react-apollo";
  1. IViewer接口内容保留,但需要和IQueryResult拧在一起:
1
2
3
interface IQueryResult {
viewer: IViewer;
}
  1. 定义查询:
1
2
3
4
5
6
7
8
const GET_VIEWER = gql`
{
viewer {
name
avatarUrl
}
}
`

我们字面量声明了GET_VIEWER查询。在前面带有gql看起来有些古怪。这个模板字面量不应该用括号吗?实际上gql是一个函数,它会解析该函数后面的查询语句。

  1. 为了Type safety,创建一个新的组件GetViewerQuery,并定义结果的返回类型:
1
class GetViewerQuery extends Query<IQueryResult> {}
  1. 我们不再需要状态,因此移除viewersetViewer变量。
  2. 因为不需要axios查询了,useEffect也删掉。
  3. 我们使用GetViewerQuery组件进行查询:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
return (
<GetViewerQuery query={GET_VIEWER}>
{({data}) => {
if (!data || !data.viewer) {
return null;
}
return (
<div>
<img src={data.viewer.avatarUrl} className="avatar" alt={"avatar"}/>
<div className="viewer">{data.viewer.name}</div>
<h1>GitHub Search</h1>
</div>
);
}}
</Query>
);
  1. 如果我们希望获取其它信息内容。例如查询数据是否正在加载中。可以改为:
1
2
3
4
5
6
7
8
9
10
return (
<GetViewerQuery query={GET_VIEWER}>
{({ data, loading}) => {
if (loading) {
return <div className="viewer">Loading...</div>;
...
}
}}
</GetViewerQuery>
)
  1. 又或者想要获取错误信息:
1
2
3
4
5
6
7
8
9
10
return (
<GetViewerQuery query={GET_VIEWER}>
{({ data, loading, error}) => {
if (error) {
return <div className="viewer">{error.toString()}</div>;
...
}
}}
</GetViewerQuery>
)

Apollo的实现非常优雅。它可以确保Query组件在正确的位置获取得到对应的数据。

Adding a repository search component

根据已有的查询功能,我们希望有一个搜索repository的选框。

  1. 首先创建一个RepoSearch.tsx,导入相应的依赖:
1
2
3
import * as React from "react";
import gql from "graphql-tag";
import { ApolloClient } from "apollo-boost";
  1. 由于需要带入ApolloClient作为prop,添加一个接口实现:
1
2
3
interface IProps {
client: ApolloClient<any>;
}
  1. 架设组件:
1
2
3
4
const RepoSearch: React.FC<IProps> = props => {
return null;
}
export default RepoSearch;
  1. 导入的App.tsx
1
import RepoSearch from "./RepoSearch";
  1. 添加到ApolloClient下面:
1
2
3
4
5
6
7
8
<ApolloProvider client={client}>
<div className="App">
<header className="App-header">
<Header />
</header>
<RepoSearch client={client} />
</div>
</ApolloProvider>

Implementing the search form

实现一个表单,允许用户输入组织名和仓储名:

  1. RepoSearch.tsx,定义搜索字段接口:
1
2
3
4
interface ISearch {
orgName: string;
repoName: string;
}
  1. 创建一个变量来装载search状态,
1
2
3
4
const RepoSearch: React.SFC<IProps> = props => {
const [search, setSearch]: [ISearch, (search: ISearch) => void] = React.useState({orgName: "", repoName: ""});
return null;
}
  1. 定义search表单:
1
2
3
4
5
6
7
8
9
10
11
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>
)
  1. 添加样式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.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;
}
  1. 实现表单事件:
1
2
3
4
5
6
7
const handleOrgNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch({...search, orgName: e.currentTarget.value});
};

const handleRepoNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch({...search, repoName: e.currentTarget.value});
};
  1. 最后一个操作是实现search的处理:
1
2
3
4
5
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// TODO - make GraphQL query
};

至此,表单的创建已经完成。

Implementing the search query

现在的关键问题是,如何查询。

  1. 首先创建返回接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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的返回一样。

  1. 需要一个默认的初始化值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const defaultRepo: IRepo = {
id: '',
name: '',
description: '',
viewerHasStarred: false,
stargazers: {
totalCount: 0,
},
issues: {
edges: [
{
node: {
id: '',
title: '',
url: '',
},
},
],
},
};
  1. 定义查询结果接口:
1
2
3
interface IQueryResult {
repository: IRepo;
}
  1. 创建查询模板字面量(tagged template literal):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
}
}
}
}
}
`;

不同的是,此次查询带有参数。

  1. 另外需要存储返回的数据到状态中。因此创建变量repo
1
const [repo, setRepo]: [IRepo, (repo: IRepo) => void] = React.useState(defaultRepo);
  1. 存储一些错误信息:
1
const [searchError, setSearchError]: [string, (searchError: string) => void] = React.useState("");
  1. 更新handleSearch箭头函数,
1
2
3
4
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSearchError("");
};
  1. 传入ApolloClient进行查询:
1
2
3
4
5
6
7
8
9
10
11
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

setSearchError('');

props.client
.query<IQueryResult>({
query: GET_REPO
});
};

  1. 将参数传入给query语句:
1
2
3
4
5
6
7
8
9
10
11
12
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

setSearchError('');

props.client
.query<IQueryResult>({
query: GET_REPO,
variables: {orgName: search.orgName, repoName: search.repoName},
});
};

  1. 处理返回内容:
1
2
3
4
5
6
7
8
9
10
11
12
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

setSearchError('');

props.client
.query<IQueryResult>(...)
.then(response => {
setRepo(response.data.repository);
});
};

  1. 处理异常信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

setSearchError('');

props.client
.query<IQueryResult>(...)
.then(...)
.catch(error => {
setSearchError(error.message);
});
};

Rendering the result

既然数据已经获取到了,需要将数据展现出来:

  1. 渲染github点赞部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>

  1. 渲染repository的列表:
1
2
3
4
5
6
7
8
9
10
11
12
13
...
<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>

  1. 显示错误信息:
1
2
3
4
5
  {repo.id && (
...
{searchError && <div>{searchError}</div>}
</div>

  1. 添加App.css样式:
1
2
3
.repo-search h4 {
text-align: center;
}

Implementing a mutation with Apollo

我们可以在React中使用GraphQL的mutation

  1. 首先导入Mutation组件:
1
import { Mutation } from "react-apollo";
  1. 创建mutation的查询:
1
2
3
4
5
6
7
8
9
10
11
const STAR_REPO = gql`
mutation($repoId: ID!) {
addStar(input: {starrableId: $repoId}) {
starrable {
stargazers {
totalCount
}
}
}
}
`;
  1. 在渲染代码部分,加入Mutation组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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:
  • mutation的渲染在viewer没有被点赞的repo
  • Mutation组件接收repository 的id作为入参
  1. Mutation组件有一个子函数addStar允许我们访问点赞数。
1
2
3
4
5
6
7
8
9
10
11
<Mutation
...
>
{(addStar) => (
<div>
<button onClick={() => addStar()}>
Star!
</button>
</div>
)}
</Mutation>
  1. loading属性作为第二参数:
1
2
3
4
5
6
7
8
9
10
11
<Mutation
...
>
{(addStar, { loading }) => (
<div>
<button disabled={loading} onClick={() => addStar()}>
{loading ? "Adding ..." : "Star!"}
</button>
</div>
)}
</Mutation>
  1. 处理错误信息:
1
2
3
4
5
6
7
8
{(addStar, { loading, error}) => (
<div>
<button disabled={loading} onClick={() => addStar()}>
{loading? "Adding ...": "Star!"}
</button>
{error && <div>{error.toString()}</div>}
</div>
)}

Working with cached data in Apollo

前面发现点击按钮后,数据并没有刷新,需要清理掉浏览器的缓存数据。

Clearing the caching using refetchQueries

每次发生mutation操作时,需要清理掉缓存信息。一种方式是使用refetchQueries

  1. refetchQueries接收一个数组对象,该对象的查询变量会被从缓存中删除。
1
2
3
4
5
6
7
8
9
10
11
12
13
<Mutation
mutation={STAR_REPO}
variables={{repoId: repo.id}}
refetchQueries={[
{
query: GET_REPO,
variables: {
orgName: search.orgName,
repoName: search.repoName,
},
},
]}>
...
  1. 如果给一个repo点赞,点赞的数字并没有立即更新。然而,点击Search按钮后才更新。

因此,数据也并没有立即更新,这种方式也不是很好,用户体验不太理想。

Updating the cache after a Mutation

幸运的是,Mutation组件还有另外一个方法update,我们可以对缓存信息进行更新。

  1. 删掉之前的refetchQueries
  2. 实现update操作:
1
2
3
4
5
6
7
<Mutation
mutation={STAR_REPO}
udpate={cache=> {
// Get the cached data
// update the cached data
// update our state
}}
  1. 更新缓存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Mutation
...
update={cache => {
const data: { repository: IRepo } | null = cache.readQuery({
query: GET_REPO,
variables: {
orgName: search.orgName,
repoName: search.repoName
}
});
if (data === null) {
return;
}
}}

如果没有缓存数据,我们直接return,不做任何处理。

  1. 现在有了一份来自缓存的数据了,直接对这份数据操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
update={cache => {
...
if (data === null) {
return;
}
const newData = {
...data.repository,
viewerHasStarred: true,
stargazers: {
...data.repository.stargazers,
totalCount: data.repository.stargazers.totalCount + 1
}
}
}}
  1. 然后使用writeQuery函数来更新缓存信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
update={cache => {
...
const newData={
...
};
cache.writeQuery({
query: GET_REPO,
variable: {
orgName: search.orgName,
repoName: search.repoName,
},
data: { repository: newData }
})
}}
  1. 最后一步是更新本地的state数据:
1
2
3
4
5
update={cache => {
...
cache.writeQuery(...);
setRepo(newData);
}}

这样,当给某个repo点赞,可以看到数据立即刷新了。

Summary

GraphQL目前不是很少用,虽然某些情况下很灵活,提供了树形结构的数据。查询和返回结构一致。只有一个请求地址。请求方法只有一个POST。查询参数灵活多变。

这里还介绍了Apollo客户端的实现,相比于axios。只需要在顶层组件实现一个provider即可,类似于Router的实现。

还介绍了Mutation组件的实现,以及对内存的处理。

不常用的原因也可想而知,不便于维护,没有版本,对于简单的接口来说有点臃肿。

鉴于GraphQL很少出现,此处不作习题演练。