写在开头
因为项目组采用的技术栈是graphql,而我之前使用的则是restful api,所以一开始有点蒙圈,我相信还有其他同学也有一样的情况。在查阅了部分资料,以及写过一些demo后,觉得将这部分内容沉淀下来,对自己是一份知识的梳理,也对团队内其他没接触过graohql的同学是个简单直白的指南吧。
ps::小菜鸡一枚,如有错误,请见谅并告知
ps:graphql的种种优势我就不赘述了 掘金上有一篇译文 《REST API 已死,GraphQL 长存 》
在中react的使用
graphql在前端的使用大部分一般都是基于已有的框架之上,而我们团队目前采用的是apollo,里面有很多子包,甚至有专门支持react的react-apollo,但是目前团队选择的是@apollo/client,也非常的简单好用,话不多说开始。
创建一个client
$ npx create-react-app my-app --typescript
为了快速开始,我们直接采用create-react-app构建项目,但是选择ts。构建完成后获得一个结构如图的项目。
下载apollo和graphql的依赖
$ npm install @apollo/client graphql
下载依赖的工程中我们同时对项目做一些改造,将app.tsx中的不需要内容删除。以及项目不需要的文件删除,也可以不删除,反正不影响我们。
index.tsx也同样删除不需要的代码,这样看上去清爽多了。
构建cilent的步骤是在跟组件提供一个ApolloProvider的高阶组件。ApolloProvider接受一个client的属性。代码如下:
// src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
import './index.css'
import App from './App'
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
//link:
})
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>,
document.getElementById('root')
)
这里做一些解释:new ApolloClient就是创建client示例,它就收一个对象做参数,这个对象有三个属性:
属性名 | 描述 |
---|---|
uri | 与apollo客户端通信的服务端地址,uri或者link之一是必须存在的,link优先 |
cache | apollo客户端的缓存策略推荐的是InMemoryCache,详情查阅官网缓存部分 |
link | 制定的网络层,没有太去了解,类似uri吧(后续更新上来) |
客户端的配置基本只会配置一次,我这里的后端是我用prisma-nexus快速搭建的。但本文主要讲的是前端gql的应用,就不赘述了。
请求数据
请求数据最简单的方式就是使用apollo提供的钩子函数,其中最常用的是useQuery,功能非常强大,是本文的主要内容,但也有一些限制,后面会讲,以及解决方案。
import React from 'react'
import { useQuery, gql } from '@apollo/client'
const GET_BLOGS = gql`
query {
blogs {
name
}
}
`
interface Blog {
name: string
}
function App() {
const { loading, error, data } = useQuery(GET_BLOGS)
if (loading) return <div>loding</div>
if (error) return <div>something error</div>
return (
<div className="App">
{data.blogs.map((blog: Blog) => {
return <div>{blog.name}</div>
})}
</div>
)
}
export default App
npm start启动项目,可以看到页面拿到了我的数据:
回到代码中,这里我们使用gql(具体是什么我也没去研究,这里用记好了)包裹我们的graphql查询语句(这里需要看一下graphql官网的语法)。查询blogs,然后去name。这样我们拿到的是blog_name组成的数组。一个最简单的请求数据过程就完成了,到这里肯定还有一些疑问。g
useQuery的特性与缺点(重点)
这里是本文的重点也是与网上其他教程不一样的地方,大部分文章大部分都是走到上面就结束了。但是经常做前端的同学,可能看到查询结果页面的时候可能就会疑惑了,这似乎是组件构建之初自动去查询的啊,可控吗?嗯,可控,不过先讲一点useQuery的其他属性。
接受参数
useQuery接受参数的方式:
import React from 'react'
import { useQuery, gql } from '@apollo/client'
const GET_BLOGS = gql`
query {
blogs {
name
}
}
`
const GET_BlOG = gql`
query($id: Int!) {
blog(id: $id) {
id
name
}
}
`
interface Blog {
name: string
id: number
}
function App() {
const { loading, error, data } = useQuery(GET_BlOG, {
variables: { id: 1 },
})
if (loading) return <div>loding</div>
if (error) return <div>something error</div>
return <div className="App">{data.blog.name}</div>
}
export default App
如上述代码,我查询了id为1的blog,结果如下:
轮询和刷新
其实apollo提供的服务中,对服务端返回的结果是有缓存的,如果参数一样,返回的结果也会优先从缓存里拿,但服务端的数据可能已经变了,官方提供了两种方式,解决这个问题,但我觉得不仅仅是用来解决这个问题啊
轮询(Polling)
polling提供了一个定时的查询,启用方式是在useQuery的配置传入一个pollInterval的参数,默认值为0,即不轮询
const { loading, error, data } = useQuery(GET_BlOG, {
variables: { id: 1 },
pollInterval: 1000,
})
依然是上面的例子,但加入了pollInterval,值为1000,即1秒轮询一次,
可以看到浏览器在不断的发出请求。我感觉实用的场景不多。业务中需要轮询的地方大多也还需要自己封装,做一些边界条件
刷新(reFetch)
刷新则是可以手动的再次刷新你的请求数据,即再发送一次请求,之前在上面有说到,useQuery默认是组件构建是请求一次。reFetch的启用方式则是将其从useQuery的result取出:
import React from 'react'
import { useQuery, gql } from '@apollo/client'
const GET_BlOG = gql`
query($id: Int!) {
blog(id: $id) {
id
name
}
}
`
interface Blog {
name: string
id: number
}
function App() {
const { loading, error, data, refetch } = useQuery(GET_BlOG, {
variables: { id: 1 },
})
if (loading) return <div>loding</div>
if (error) return <div>something error</div>
return (
<div className="App">
{data.blog.name}
<button onClick={() => refetch()}>click me torefetch</button>
</div>
)
}
export default App
启动项目,打开浏览器,刷新页面,可以看到组件构建时发送了一次graphql请求,点击按钮又会请求一次。
这里有一个问题是,reFetch的过程中是不会处罚loading,即页面会先保持之前的ui,等reFetch的结果回来了,直接刷新。如果要做一颗loading效果的话,这里需要用到另外一个属性:networkStatus,使用的时候usequery的配置里同样它notifyOnNetworkStatusChange:
import React from 'react'
import { useQuery, gql, NetworkStatus } from '@apollo/client'
const GET_BlOG = gql`
query($id: Int!) {
blog(id: $id) {
id
name
}
}
`
interface Blog {
name: string
id: number
}
function App() {
const { loading, error, data, refetch, networkStatus } = useQuery(GET_BlOG, {
variables: { id: 1 },
notifyOnNetworkStatusChange: true,
})
if (networkStatus === NetworkStatus.refetch) return <div>refetch loading</div>
if (loading) return <div>loding</div>
if (error) return <div>something error</div>
return (
<div className="App">
{data.blog.name}
<button onClick={() => refetch()}>click me torefetch</button>
</div>
)
}
export default App
突变
具体指的是,如果graphql查询依赖的变量改变了的话,graphql也会重新去查一下,听起来和refetch有点类似,但这里不是我们手动的。但是合理利用可以达到和refetch差不多的效果,代码如下
import React, { useState } from 'react'
import { useQuery, gql, NetworkStatus } from '@apollo/client'
const GET_BlOG = gql`
query($id: Int!) {
blog(id: $id) {
id
name
}
}
`
interface Blog {
name: string
id: number
}
function App() {
const [id, setId] = useState<number>(1)
const { loading, error, data, refetch, networkStatus } = useQuery(GET_BlOG, {
variables: { id },
notifyOnNetworkStatusChange: true,
})
const upId = () => {
setId(2)
}
if (networkStatus === NetworkStatus.refetch) {
alert(1)
return <div>refetch loding</div>
}
if (loading) return <div>loding</div>
if (error) return <div>something error</div>
return (
<div className="App">
{data.blog.name}
<div>
<button onClick={upId}>click me upid</button>
</div>
</div>
)
}
export default App
打开浏览器,在我点击按钮,将ID值赋值为2后,重新发送了请求,切id变为2,查询结果也改变了:
个人感觉这是一个很好用的特性,也是一个需要注意的特性,如果不注意可能会带来些意外的变化
useQuery的缺点,无法真正手动
看了这么多特性,似乎能实现很多场景下的应用,但作为一个用惯了restapi的人,怎么还是这么不爽呢?似乎总感觉不能得心应手呢?是的,无论是突变还说刷新,都不是真正意义上的手动发生请求,他们起码都会在组件构建发起一次。我并不需要啊。比如我们想点击一个按钮才查询等等情况,usequery就不实用了。因此还有另一个hook-useLazyQuery,我发现很多文章都很少提到。
useLazyQuery-真正的手动查询
useLazyQuery的大部分属性和行为useQuery类似,但需要注意的是,调用useLazyQuery之后,他不会立即执行(因此不用担心组件构建就查询了),而是会返回一个函数你去调用查询:
import React, { useState } from 'react'
import { useQuery, gql, NetworkStatus, useLazyQuery } from '@apollo/client'
const GET_BlOG = gql`
query($id: Int!) {
blog(id: $id) {
id
name
}
}
`
interface Blog {
name: string
id: number
}
function App() {
const [id, setId] = useState<number>(1)
const [getBlog, { loading, data }] = useLazyQuery(GET_BlOG, {
variables: { id },
notifyOnNetworkStatusChange: true,
})
const getData = () => {
getBlog()
}
if (loading) return <div>loding</div>
return (
<div className="App">
{data && data.blog.name}
<div>
<button onClick={getData}>click me upid</button>
</div>
</div>
)
}
export default App
打开浏览器,可以看到初始页面是没有数据的,点击按钮后,才会发送请求,获取数据,这里还要注意一点,useQuery是返回的对象,而useLazyQuery返回的是数组,第一项是查询函数,第二项才是其他值集合成的对象
最后
到此,大部分的查询场景你都能搞定了,mutation我还没看,但估计也差不太多,如果有不一样的抽空会记录下来,另外想说的一点是,无论是useQuery还是useLazyQuery都是hook的形式,不知道在class组件里用起来如何,估计还是会有些不同的,但我之前也用过client的query方法实现过原始的查询,所以即使class当然也还是可以graphql的。