记在 React 项目中使用 GraphQL

545 阅读4分钟

记在 React 项目中使用 GraphQL

概述

最近在项目中对接了GraphQL,遇到了一些困难和疑惑,记录下来供后续参考。

GraphQL是一种的查询语言;作为前端开发,我们可以通过编写GraphQL query自定义返回数据的结构,来更加方便/贴合视图层面的消费。

例子:

GraphQL:

{
	hero {
		name
		height
		mass
	}
}

数据返回:

{
	"hero": {
		"name": "Luke Skywalker",
		"height": 1.72,
		"mass": 77
	}
}

关于对GraphQL的定义不再赘述,更多可以在GraphQL网站上查阅。

然而GraphQL网站只是对关于这个查询语言的描述/规范,并没有相关的实现。也就是说,如果我们需要在项目中落地,需要另找针对JavaScript的实现(资源包)。

GraphQL简单来说包含两面:后端实现和客户端实现。由于我方后端比较给力,已经做好了后端实现,我们需要做的只需要做客户端的事情。

在 GraphQL网站上列举了很多 JavaScript 的实现,我们选择了最热门的 Apollo Client。

开始

安装依赖

假定已经使用Create React App的生成了基础的项目模板以后,在主目录运行:

yarn add @apollo/client graphql

# 或者

npm install @apollo/client graphql -S

其中:

  • @apollo/client apollo 客户端,包含了缓存/状态管理/错误处理(react hooks api),和视图层面(React组件)等 API;
  • graphql 包含了基础 GraphQL 查询语句分析功能。这个包我们通常不会直接使用到,只是 @apollo/client 的依赖 。

初始化client实例

新建文件 src/lib/graphql.ts 存放client实例,方便我们后续在应用中导入:

import { ApolloClient, InMemoryCache } from '@apollo/client'

let graphUrl = '//somethingcool.com/graphql'

// 区分开发/测试环境与生产环境,此处的 process.env.mode 为 webpackDefinePlugin 自定义内容;实际情况自行按需编写
if (process.env.NODE_ENV === 'development' || process.env.mode === 'dev') {
  graphUrl = '//somethingcool-test.com/graphql'
}

export const client = new ApolloClient({
  uri: graphUrl,
  cache: new InMemoryCache(),
  credentials: 'include'
})

注意,有时候开发主域和接口的域名不一定相同,由于不符合浏览器同源策略,这就会出现跨域问题。在服务端配置支持 CORS 的前提下,在服务端需要获得cookie的话,需要配置 credentials: 'include'

cache 字段普通使用 new InMemoryCache() 即可,apolloCilent 会缓存我们已经请求过的数据,下次再发出同样的请求的话,apollo 会返回对应条目的记录,而不再发出已有的数据请求。

创建 src/lib/gql.ts 文件, 用来统一存放 query,方便后续统一管理:

import { gql } from '@apollo/client'

// 查询关注列表
export const FollowingListQuery = gql`
  query ($uids: [ID!]!) {
    getLoginUser {
      uid
      filterOutAttentionUids(uids: $uids)
    }
  }
`

// 关注
export const Follow = gql`
  mutation ($uid: ID!) {
    subscribe(uid: $uid) {
      code
      errorMessage
      success
    }
  }
`

// 取消关注
export const UnFollow = gql`
  mutation ($uid: ID!) {
    unsubscribe(uid: $uid) {
      code
      errorMessage
      success
    }
  }
`

可以看出,query 是没有代码上的依赖,只是描述了query 和 mutation 所需要的信息。

最后一步准备工作是让把 React 视图层和 apollo 客户端连接起来。Apollo Client 提供了一个 ApolloClientProvider , 我们把它放到组件树较高的地方,这样我们在业务组件调用如 useQuery, useMutaion 的时候,apollo 可以知道我们在操作哪个 client 。

src/entry/index.tsx :

//...
import { ApolloProvider } from '@apollo/client'
import { client } from 'src/lib/graphql' 

const App = (
  <ApolloProvider client={client}>
    <Router>
	     {/** ... */} 
    </Router>
  </ApolloProvider>
)

ReactDom.render(App, document.getElementById('app'))

在业务中使用

准备工作已经完成,开始做业务开发,这次要实现一个关注列表。因为需要用到的接口不完全可以用 useQuery 完成,所以套用了 ahooksusePagination 组合了下:

import { client } from 'src/lib/graphql'
import { usePagination } from 'ahooks'
import { FollowingListQuery } from 'src/lib/gql'

export const Home = () => {
  const { mutate, refresh, data, pagination, loading, error } = usePagination(
    ({ current, pageSize }) => fetchers['/anchor-list']({
      ...extra,
      pageNo: current - 1,
      pageSize
    })
      .then((res) => {
        const uids = res.data
        if (uids?.length) {
          // 请求关注数据
          return client
            .query({ query: FollowingListQuery, variables: { uids } })
            .then(({ data }) => {
              const filterOutAttentionUids = data?.getLoginUser?.filterOutAttentionUids || [] // 已关注用户列表
              const anchorList = res.data?.anchorList
              if (anchorList && anchorList.length) {
                for (const anchor of anchorList) {
                  anchor.isSubscribed = filterOutAttentionUids.includes(`${anchor.uid}`) // 如果在关注列表,标记
                }
              }
              return res
            })
        } else {
          return res
        }
      })
      .then((res) => ({
        list: res.data?.anchorList || [],
        total: res.data?.total || 0
      }))
  )

	return // ...
}

关注这段:

client
    .query({ query: FollowingListQuery, variables: { uids } })
    .then(res => {})

我们先前在 FollowingListQuery 指定的 $uids 可以如此在 variables 字段传入。client.query

返回一个promise 所以我们很方便把数据处理逻辑串联起来。

插播个提示:usePagination 返回的 mutate 帮助函数十分便利。在这个例子中,点击了关注以后,需要在本地更新 isSubscribed ****标识,但是不需要重新发起请求的时候,我们就使用了 mutate 临时改了本地数据。

接下来,我们实现,关注/取消关注按钮组件:

import { useMutation } from '@apollo/client'
import { Follow, UnFollow } from 'src/lib/gql' 
// ...

export const SubscriptionBtnSwitcher: FC<Props> = ({ userInfo }) => {
  const [subscribe, { loading: subscribing }] = useMutation(Follow)
  const [unSubscribe, { loading: unSubscribing }] = useMutation(UnFollow)

	return (
    <ListActionContext.Consumer>
      {({ mutate }) =>
        userInfo.isSubscribed ? (
          <Button
            type='weak'
            loading={unSubscribing}
            onClick={async () => {
              const { data } = await unSubscribe({ variables: { uid: userInfo.uid } })
              // mutate local value
              if (data.unsubscribe.code === 0) {
                mutate((data) => {})
              } else {
                Toast.info({ text: '取消关注失败' })
              }
            }}
          >
            取消关注
          </Button>
        ) : (
          <Button
            type='emphasis'
            loading={subscribing}
            onClick={async () => {
              const { data } = await subscribe({ variables: { uid: userInfo.uid } })
              // mutate local value
              if (data.subscribe.code === 0) {
                mutate((data) => {})
              } else {
                Toast.info({ text: '关注失败' })
              }
            }}
          >
            关注
          </Button>
        )
      }
    </ListActionContext.Consumer>
  )
}
}

可以看出,useMutation 的api设计和 React 视图层结合得十分合适:

const [subscribe, { loading: subscribing }] = useMutation(Follow) 

useMutation 返回第一个参数我们可以用来给业务调用。在这个例子中,点击关注按钮之后调用


await subscribe({ variables: { uid: userInfo.uid } })

同 client.query 一致,我们通过 variables 传入参数。除此之外,useMutation还返回了第二个参数:{ loading: subscribing } 我们可以在操作结束之前在视图展示加载状态。

总结

经过这么多次的迭代,GraphQL生态已经足够完善,再加上 React 本身的升级,譬如 React Hooks 等特性等结合起来,前端开发体验会变得越来健壮与高效。

参考: