- 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
| https://developer.github.com/v4/explorer/
|
- 输入下面内容并执行 .
1 2 3 4 5
| query { viewer { name } }
|
其中:
query
关键字开启了查询,它是可选的。
viewer
是我们需要获取的对象。
name
为viewer
的名字,作为返回。
执行后返回的是JSON对象。
- 我们可以从结果部分查看文档说明。
- 从文档得知,我们还可以添加额外的查询字段:
1 2 3 4 5 6
| query { viewer { name avatarUrl } }
|
Returning nested data
复杂一点,我们希望查询github的start和issue的数量并返回。
- 首先输入下面查询:
1 2 3 4 5 6
| query { repository(owner:"facebook", name: "react") { name description } }
|
这次传递了两个参数,查找owner
为Facebook,name为react的仓储.以及将 name 和 description作为返回值。
- 下面希望获取得到star的数量。
1 2 3 4 5 6 7 8 9
| query { repository(owner:"facebook",name:"react") { name description stargazers { totalCount } } }
|
- 指定别名:
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 } } } }
|
- 查找最近的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 } } } } }
|
这里的查询用edges
和node
包装了结构,用于cursor-based的分页。
Query parameters
前面的查询是以硬编码的方式,我们希望定义变量查询。
- 我们可以在
query
关键字后面添加查询变量。这些参数需要声明它的类型、变量名。其中变量名要以$
开头。类型后要带!
。如下:
1 2 3 4 5
| query ($org: String!, $repo: String!) { repository(owner:$org, name:$repo) { ... } }
|
- 在执行之前,我们需要在Query Variables面板添加变量入参:
1 2 3 4
| { "org": "facebook", "repo": "react" }
|
Writing GraphQL data
GraphQL的数据写入需要创建mutation
。
- 要给github的repo加星,首先需要获得
id
.
1 2 3 4 5 6
| query($org: String!, $repo: String!) { repository(owner:$org, name:$repo) { id ... } }
|
- 将上面查询的返回
id
拷贝,该id
格式如下:
1
| MDEwOlJlcG9zaXRvcnkxMDI3MDI1MA==
|
- 创建第一个
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
- 之后我们指定了它的返回内容
- 在Query Variables面板填入参数:
1 2 3
| { "repoId": "MDEwOlJlcG9zaXRvcnkxMDI3MDI1MA==" }
|
- 执行之后发现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的查询:
- 在
Header.tsx
,创建两个接口表示GraphQL查询的返回数据:
1 2 3 4 5 6 7 8 9
| interface IViewer { name: string; avatarUrl: string; } interface IQueryResult { data: { viewer: IViewer; }; }
|
- 在
Header
组件创建一些状态块:
1
| const [viewer, setViewer]: [IViewer, (viewer: IViewer) => void] = React.useState({name: "", avatarUrl: ""});
|
- 在组件被挂载时声明周期中开始GraphQL的查询。我们使用了
useEffect
函数:
1 2 3
| React.useEffect(() => { // TODO - make a GraphQL query }, []);
|
第二个参数使用了一个空数组,这样仅在组件被挂载时进行查询,而不是每次都查询。
- 然后使用
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
方法,因为查询语句在方法体内。
- 另外还需要在标头带上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 2 3 4 5 6
| axios.post<IQueryResult>( ..., ).then(response => { setViewer(response.data.data.viewer); });
|
- 现在数据已经存入了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> );
|
- 添加一些样式到
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
| npm install apollo-boost react-apollo graphql
|
apollo-boost
包含了Apollo客户端的所有内容
react-apollo
包含有用于交互GraphQL服务的React 组件
graphql
是个用于解析GraphQL查询的核心包
- 另外需要安装一些TypeScript的类型:
1
| npm install @types/graphql --save-dev
|
- 还需要确保编译时,应该包含
es2015
和esNext
的依赖库。在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
所有子组件下使用。
- 在
App.tsx
,导入依赖:
1 2
| import ApolloClient from "apollo-boost"; import{ ApolloProvider } from "react-apollo";
|
- 在
App
类组件前面,创建客户端ApolloClient
:
1 2 3 4 5 6
| const client = new ApolloClient({ uri: "https://api.github.com/graphql", headers: { authorization: `Bearer our-bearer-token` } });
|
- 最后一步是使用
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
代码:
- 导入下面依赖,并删掉原来的
axios
:
1 2
| import gql from "graphql-tag"; import { Query } from "react-apollo";
|
IViewer
接口内容保留,但需要和IQueryResult
拧在一起:
1 2 3
| interface IQueryResult { viewer: IViewer; }
|
- 定义查询:
1 2 3 4 5 6 7 8
| const GET_VIEWER = gql` { viewer { name avatarUrl } } `
|
我们字面量声明了GET_VIEWER
查询。在前面带有gql
看起来有些古怪。这个模板字面量不应该用括号吗?实际上gql
是一个函数,它会解析该函数后面的查询语句。
- 为了Type safety,创建一个新的组件
GetViewerQuery
,并定义结果的返回类型:
1
| class GetViewerQuery extends Query<IQueryResult> {}
|
- 我们不再需要状态,因此移除
viewer
和setViewer
变量。
- 因为不需要
axios
查询了,useEffect
也删掉。
- 我们使用
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 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 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的选框。
- 首先创建一个
RepoSearch.tsx
,导入相应的依赖:
1 2 3
| import * as React from "react"; import gql from "graphql-tag"; import { ApolloClient } from "apollo-boost";
|
- 由于需要带入
ApolloClient
作为prop,添加一个接口实现:
1 2 3
| interface IProps { client: ApolloClient<any>; }
|
- 架设组件:
1 2 3 4
| const RepoSearch: React.FC<IProps> = props => { return null; } export default RepoSearch;
|
- 导入的
App.tsx
:
1
| import RepoSearch from "./RepoSearch";
|
- 添加到
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
实现一个表单,允许用户输入组织名和仓储名:
- 在
RepoSearch.tsx
,定义搜索字段接口:
1 2 3 4
| interface ISearch { orgName: string; repoName: string; }
|
- 创建一个变量来装载
search
状态,
1 2 3 4
| const RepoSearch: React.SFC<IProps> = props => { const [search, setSearch]: [ISearch, (search: ISearch) => void] = React.useState({orgName: "", repoName: ""}); return null; }
|
- 定义
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 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 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}); };
|
- 最后一个操作是实现
search
的处理:
1 2 3 4 5
| const handleSearch = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); // TODO - make GraphQL query };
|
至此,表单的创建已经完成。
Implementing the search query
现在的关键问题是,如何查询。
- 首先创建返回接口:
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 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 2 3
| interface IQueryResult { repository: IRepo; }
|
- 创建查询模板字面量(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 } } } } } `;
|
不同的是,此次查询带有参数。
- 另外需要存储返回的数据到状态中。因此创建变量
repo
,
1
| const [repo, setRepo]: [IRepo, (repo: IRepo) => void] = React.useState(defaultRepo);
|
- 存储一些错误信息:
1
| const [searchError, setSearchError]: [string, (searchError: string) => void] = React.useState("");
|
- 更新
handleSearch
箭头函数,
1 2 3 4
| const handleSearch = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); setSearchError(""); };
|
- 传入
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 }); };
|
- 将参数传入给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 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 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
既然数据已经获取到了,需要将数据展现出来:
- 渲染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>
|
- 渲染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 2 3 4 5
| {repo.id && ( ... {searchError && <div>{searchError}</div>} </div>
|
- 添加
App.css
样式:
1 2 3
| .repo-search h4 { text-align: center; }
|
Implementing a mutation with Apollo
我们可以在React中使用GraphQL的mutation
。
- 首先导入
Mutation
组件:
1
| import { Mutation } from "react-apollo";
|
- 创建
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 } } } } `;
|
- 在渲染代码部分,加入
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
作为入参
Mutation
组件有一个子函数addStar
允许我们访问点赞数。
1 2 3 4 5 6 7 8 9 10 11
| <Mutation ... > {(addStar) => ( <div> <button onClick={() => addStar()}> Star! </button> </div> )} </Mutation>
|
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 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
:
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, }, }, ]}> ...
|
- 如果给一个repo点赞,点赞的数字并没有立即更新。然而,点击
Search
按钮后才更新。
因此,数据也并没有立即更新,这种方式也不是很好,用户体验不太理想。
Updating the cache after a Mutation
幸运的是,Mutation
组件还有另外一个方法update
,我们可以对缓存信息进行更新。
- 删掉之前的
refetchQueries
。
- 实现
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 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 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 } } }}
|
- 然后使用
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 } }) }}
|
- 最后一步是更新本地的state数据:
1 2 3 4 5
| update={cache => { ... cache.writeQuery(...); setRepo(newData); }}
|
这样,当给某个repo点赞,可以看到数据立即刷新了。
Summary
GraphQL目前不是很少用,虽然某些情况下很灵活,提供了树形结构的数据。查询和返回结构一致。只有一个请求地址。请求方法只有一个POST。查询参数灵活多变。
这里还介绍了Apollo
客户端的实现,相比于axios
。只需要在顶层组件实现一个provider
即可,类似于Router的实现。
还介绍了Mutation
组件的实现,以及对内存的处理。
不常用的原因也可想而知,不便于维护,没有版本,对于简单的接口来说有点臃肿。
鉴于GraphQL很少出现,此处不作习题演练。