graphql在react中的新手入门指南

avatar
前端 @滴滴出行

写在开头

因为项目组采用的技术栈是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。构建完成后获得一个结构如图的项目。 image-20200924163336136

下载apollo和graphql的依赖

$ npm install @apollo/client graphql

下载依赖的工程中我们同时对项目做一些改造,将app.tsx中的不需要内容删除。以及项目不需要的文件删除,也可以不删除,反正不影响我们。

image-20200924163840040

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优先
cacheapollo客户端的缓存策略推荐的是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启动项目,可以看到页面拿到了我的数据:

image-20200924170619245

回到代码中,这里我们使用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,结果如下:

image-20200924184258049

轮询和刷新

其实apollo提供的服务中,对服务端返回的结果是有缓存的,如果参数一样,返回的结果也会优先从缓存里拿,但服务端的数据可能已经变了,官方提供了两种方式,解决这个问题,但我觉得不仅仅是用来解决这个问题啊

轮询(Polling)

polling提供了一个定时的查询,启用方式是在useQuery的配置传入一个pollInterval的参数,默认值为0,即不轮询

const { loading, error, data } = useQuery(GET_BlOG, {
    variables: { id: 1 },
    pollInterval: 1000,
  })

依然是上面的例子,但加入了pollInterval,值为1000,即1秒轮询一次,

image-20200924184810253

可以看到浏览器在不断的发出请求。我感觉实用的场景不多。业务中需要轮询的地方大多也还需要自己封装,做一些边界条件

刷新(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,查询结果也改变了:

image-20200924190707597

个人感觉这是一个很好用的特性,也是一个需要注意的特性,如果不注意可能会带来些意外的变化

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的。