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 (() => { }, []);
第二个参数使用了一个空数组,这样仅在组件被挂载时进行查询,而不是每次都查询。
然后使用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 >
实现一个表单,允许用户输入组织名和仓储名:
在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 (); };
至此,表单的创建已经完成。
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 => { }}
更新缓存:
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很少出现,此处不作习题演练。