1
GraphQL 是什么
GraphQL 是一种 API 的查询语言,不同于 RESTful API 的一个请求获取一个资源的指定数据,GraphQL 将资源和请求方式分离,可以由前端自己定义需要获取资源的哪些数据,也可以通过一个请求获取服务端多个资源的数据,解决了前端数据过度获取以及后端开发需要感知业务场景的问题。
举个例子,假设开发一个图书管理系统,产品给你几个页面的开发任务,一个展示全部书籍,一个展示全部作者,一个展示书籍详情,一个展示作者详情。RESTful API 根据产品的数据要求,开发 4 个接口来返回数据。GraphQL 声明了对应的两个数据类型(Book, Author),提供对应数据类型的列表和详情的查询方法,让前端根据产品需求按需请求数据。
过了几天,产品不出意外的来增加需求了,要求在书籍列表上加一个字段,在书籍详情增加对应作者信息。RESTful API 无奈修改了列表接口,然后让前端在详情页面多调用一个详情接口拿新增的信息,前端觉得应该后端改接口把新增的信息加到原来的接口里,然后又是喜闻乐见的前后端接口争执。GraphQL 让前端在原来请求的基础上多请求几个字段,不用改接口就完成了新增的需求。
贴个代码,标注了例子中需要新增的字段。
type Book {
id: ID
title: String
author: Author
type: String
}
type Author {
id: ID
name: String
}
// 请求字段
query {
books() {
id
title
type // 新增字段
}
}
query {
book(id: 1) {
id
title
author { // 新增字段
id
name
}
}
}
// 得到的结果
{
"books": [
{
"id": 1,
"title": "一本书的名字"
}, {
"id": 2,
"title": "另一本书的名字"
}
]
}
{
"book": {
"id": 1,
"title": "一本书的名字",
"author": {
"name": "这是个作者"
}
}
}
2
引入 apollo-client
虽然可以直接调用 GraphQL API,但是考虑到效率和开发舒适度,还是推荐引入一个客户端类库来协助开发,GraphQL 有着不少的客户端类库,比较熟知的有 graphql-request、relay、urql、graphql-hooks 和本文要讲到的 apollo-client。
apollo-client 结合 React,将数据的请求、修改,请求的报错通过 hooks 封装,不再需要实现请求数据管理相关的逻辑,拿到数据响应式更新页面或者提示报错信息就好了。
如果项目中需要前端缓存,那就还得额外考虑 GraphQL 对缓存不是特别友好的这一点(没有唯一的 url,请求字段不固定等问题),因此 apollo-client 提供了一套开箱即用的缓存方案,只要简单的几个配置,就能解决大部分的 GraphQL 缓存问题。
除了上面列的这些,apollo-client 还提供了很多的钩子和组件,并且有着活跃的生态社区,这些都能为开发提供不小的帮助,后面会结合我们的实际情况对以上做一些具体的说明。
3
初始配置
1.ApolloClient实例
ApolloLink
ApolloLink 将客户端向服务发送请求的流程定义成了按顺序执行的链式对象链,支持拓展或替换实现自定义。默认情况下,客户端使用 HttpLink 将请求发送到服务器。
得益于这种设计方式,我们可以轻易的实现对请求 header 增加 Authorization、对请求报错做集中处理等功能。你也可在其中增加其他功能逻辑,比如计算请求总用时、请求数据统计等。
如果有上传文件的需求,可以引入 apollo-upload-client 库,只需要将 HttpLink 替换成对应的 createUploadLink 方法就可以了。
import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client'
import { onError } from '@apollo/client/link/error'
import { ApolloLink } from '@apollo/client/core'
import { createUploadLink } from 'apollo-upload-client'
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
// 错误处理逻辑
}
if (networkError) {
const message = '网络异常,请稍后再试'
// 错误处理逻辑
}
})
const authLink = new ApolloLink((operation, forward) => {
operation.setContext(({ headers }) => ({
headers: {
...headers,
Authorization: '...'
},
}))
return forward(operation)
})
const httpLink = new HttpLink({
uri: '...'
})
// const httpLink = createUploadLink({
// uri: '...'
// })
export const client = new ApolloClient({
link: from([ authLink, errorLink, httpLink ]),
})
缓存设置
apollo-client 提供了 5 种前端的数据缓存模式,可以在全局或者单独的 query 请求通过设置 fetchPolicy 来实现缓存配置,这边先简单介绍一下不同模式的缓存策略和使用场景,具体的缓存存储方式后文会详细介绍。
cache-first:
默认值,获取数据时先检查是否命中缓存,若命中则返回数据,否则向服务器发起请求获取数据、更新缓存并返回。适用于数据不经常变化或对数据量大且实时性要求不高的场景。
cache-and-network:
获取数据时先检查是否命中缓存,若命中则返回数据,然后无论是否命中都会向服务器发起请求获取数据、更新缓存并返回。适用于需要快速获取数据但是对数据又有实时性要求的场景。
network-only:
获取数据时不会检查是否命中缓存,直接向服务器发起请求获获取数据、更新缓存并返回。适用于实时性要求高的场景。
no-cache:
数据不会写入缓存,直接向服务器发起请求获获取数据并返回。适用场景类似 network-only,区别在于不会有缓存数据。
cache-only:
不会发起请求,直接从缓存中获取数据,如果缓存没有命中则会报错。适用于离线场景,需要提前讲数据写入缓存。
tip:实际使用中发现,如果使用 lazyQuery 或者 refetch,缓存默认值为 cache-and-network,且不能使用 cache-first 或者 cache-only。
2. ApolloProvider
现在我们得到了 apollo-client 的实例,我们需要将它传递给我们的 React 组件树以便于我们在任意组件中使用 apollo-client 提供的功能,这时候就需要用到 React 的 Context,不过我们不需要自己创建这个上下文,apollo 已经提供了 ApolloProvider 组件来实现这个场景,我们需要做的就是把它注册在根组件之上。
import { ApolloProvider } from '@apollo/client'
return (
<ApolloProvider client={client}>
<RootComponent />
</ApolloProvider>
)
4
请求
1. 查询
主要通过 apollo-client 提供的两个 react hook 的 api 实现查询请求,同时返回其他可用于渲染的字段,我们不再需要管理请求的状态和数据,只需要调用解构方法实现页面渲染。首先我们先定义请求,定义参数和返回数据格式。
export const getCustomerList = gql`
query getCustomerList(
$skip: Int = 0
$take: Int = 15
) {
customerList(
skip: $skip
take: $take
) {
items {
id
name
environments {
id
name
}
group {
id
name
}
}
}
}
`
然后通过调用 hook 函数,进行数据请求,得到解构数据,然后进行相应的渲染逻辑。
import { gql, useQuery } from '@apollo/client';
// ...
const { loading, error, data, refetch } = useQuery(getCustomerList, {
variables: {
skip: 0,
take: 15
}
});
// ...
除了自动执行的 useQuery 外,还提供了手动执行的 hook 函数。
import { gql, useLazyQuery } from '@apollo/client';
// ...
const [ getList, { loading, error, data } = useLazyQuery(getCustomerList);
getList({
variables: {
skip: 0,
take: 15
}
})
// ...
2. 修改
export const customerRename = gql`
mutation customerRename(
$name: String
) {
customerRename(
name: $name
) {
id
name
}
}
`
import { gql, useMutation } from '@apollo/client';
// ...
const [ rename, { loading, error, data } = useMutation(customerRename);
rename({
variables: {
name: '重命名'
}
}).then(() => {})
// ...
3. graphql-code-generator
我们在开发中,同时用到 ts 和 graphQL,会发现 gql 在某种层面上其实已经定义了参数和返回值的类型,并且以字符串形式编写 gql 代码,失去了高亮和格式化很容易出现一些编写错误,这个时候 graphql-code-generator 就顺理成章的引入到我们项目中了。
graphql-code-generator,可以通过维护 gql 代码,结合服务端定义的 schema,自动生成对应的数据类型和请求方法,附一个简单的例子。
"scripts": {
"codegen": "graphql-codegen --config codegen.yml"
}
overwrite: true
schema: "http://localhost:8000/graphql" // 服务地址
documents: "src/graphql/*.gql" // gql文件路径
generates:
src/generated/graphql.ts: // 文件生成路径
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
hooks:
afterOneFileWrite:
- eslint --fix
// 修改后的gql不再以字符串形式维护,结合编译器插件可以完成高亮、语法检查等功能
query getCustomerList(
$skip: Int = 0
$take: Int = 15
) {
customerList(
skip: $skip
take: $take
) {
items {
id
name
environments {
id
name
}
group {
id
name
}
}
}
}
export type Environment = {
__typename?: 'Environment';
id: Scalars['String'];
name?: Maybe<Scalars['String']>;
}
export type Group = {
__typename?: 'Group';
id: Scalars['String'];
name?: Maybe<Scalars['String']>;
}
export type GetCustomerListQueryVariables = Exact<{
skip?: Maybe<Scalars['Int']>;
take?: Maybe<Scalars['Int']>;
}>;
export type Customer = {
__typename?: 'Customer';
id: Scalars['String'];
name?: Maybe<Scalars['String']>;
group?: Maybe<Group>;
environments: Array<Environment>;
}
export type CustomerResponse = {
__typename?: 'CustomerResponse';
items: Array<Customer>;
};
export type GetCustomerListQuery = (
{ __typename?: 'Query' }
& { customerList: (
{ __typename?: 'CustomerResponse' }
& Pick<CustomerResponse, 'total'>
& { items: Array<(
{ __typename?: 'Environment' }
& { environments: Array<(
{ __typename?: 'Environment' }
& Pick<Environment, 'id' | 'name'>
)>, group?: Maybe<(
{ __typename?: 'Group' }
& Pick<Group, 'id' | 'name'>
)> }
)> }
) }
);
export const GetCustomerListDocument = gql`
query getCustomerList(
$skip: Int = 0
$take: Int = 15
) {
customerList(
skip: $skip
take: $take
) {
items {
id
name
environments {
id
name
}
group {
id
name
}
}
}
}
`
export function useGetCustomerListQuery(baseOptions?: Apollo.QueryHookOptions<GetCustomerListQuery, GetCustomerListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetCustomerListQuery, GetCustomerListQueryVariables>(GetCustomerListDocument, options);
}
export function useGetCustomerListLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCustomerListQuery, GetCustomerListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetCustomerListQuery, GetCustomerListQueryVariables>(GetCustomerListDocument, options);
}
export type GetCustomerListQueryHookResult = ReturnType<typeof useGetCustomerListQuery>;
export type GetCustomerListLazyQueryHookResult = ReturnType<typeof useGetCustomerListLazyQuery>;
export type GetCustomerListQueryResult = Apollo.QueryResult<GetCustomerListQuery, GetCustomerListQueryVariables>;
可以看到自动创建后的文件中,有对于请求方法入参和返回值的数据定义,同时对 gql 中每一个请求方法都创建了对应的 hook 函数。
// 修改后的调用方式,mutation同理
import {
useGetCustomerListQuery,
useGetCustomerListLazyQuery,
} from '@apollo/client';
const { loading, error, data, refetch } = useGetCustomerListQuery({
variables: {
skip: 0,
take: 15
}
});
const [ getList, { loading, error, data } = useGetCustomerListLazyQuery(getCustomerList);
getList({
variables: {
skip: 0,
take: 15
}
})
5
缓存数据存储
大多数情况请求数据的缓存都是服务端考虑的问题,前端只需要通过浏览器的缓存策略就可以达到请求数据缓存的效果。在某些需求场景下,前端也会在本地完成请求数据的缓存,RestFul API 通过请求 url 和参数作标识以 key-value 的形式存储请求数据,但是之前也提到 GraphQL 的请求 url 不唯一,那这种情况下,apollo-client 是怎么实现本地数据缓存的呢?
先看例子,还是前面的 getCustomerList 请求,然后我们会得到一系列的缓存数据。
{
ROOT_QUERY: customerList({"skip":0,"take":15}): {
items: [
{ __ref: 'CustomerInfo:10001' },
{ __ref: 'CustomerInfo:10002' },
{ __ref: 'CustomerInfo:10003' }
// ....
]
},
"CustomerInfo:10001": {
__typename: 'CustomerInfo',
id: 10001,
name: '客户1',
environments: [
{ __ref: 'EnvironmentInfo:1' }
],
group: { __ref: 'GroupInfo:10' }
},
"EnvironmentInfo:1": {
__typename: 'EnvironmentInfo',
id: 1,
name: '环境1'
},
"GroupInfo:10": {
__typename: 'GroupInfo',
id: 10,
name: '用户组10'
}
}
这些数据是怎么来的呢?
首先,apollo-client 会把请求返回的数据根据定义的数据类型拆分,默认给每个对象生成一个 __typename 字段作为数据类型的标识,然后通过 __typename 和 id(或 _id) 的串联格式作为缓存数据的 key,形成一个个 key-value 形式的数据对象。
拆分后每个数据对象和请求返回的数据之间,通过 __ref 字段做关联,然后再在 ROOT_QUERY 中以查询方法名+参数做请求缓存的 key,以此完整整个请求的数据缓存。
得益于数据拆分缓存的方式,我们在执行 mutation 方法修改某个对象时,得到的修改后的返回值可以直接更新当前缓存,闭环整个请求-修改的缓存链路。
不过我们实际在项目中使用时,因为对缓存的使用率不高,默认缓存逻辑在返回值中增加 __typename 字段更像是一种脏数据插入,所以在配置项中取消了默认增加该字段的配置,这种情况下的缓存数据存储就会变成类似 Restful API 直接以接口加参数的形式存储,不会再拆分到数据类型的对象。
// 修改配置
new ApolloClient({
cache: new InMemoryCache({ addTypename: false }),
})
// 缓存结果
{
ROOT_QUERY: customerList({"skip":0,"take":15}): {
items: [
{
id: 10001,
name: '客户1',
environments: [
{
id: 1,
name: '环境1'
},
...
],
groupInfo: {
id: 10,
name: '用户组10'
}
},
// ....
]
},
}
除了以上场景闭环对缓存的维护逻辑,apollo-client 还提供了方法支持手动对缓存数据进行查询和修改操作,满足不同场景下对缓存的需求。
export const getCustomerInfo = gql`
query getCustomerInfo($id: Int!) {
customerInfo(id: $id) {
id
name
}
}
`
// 获取id为1的客户信息缓存,没有缓存信息则返回null
client.readQuery({
query: getCustomerInfo,
variables: {
id: 1,
},
});
// 写入缓存信息(更新/新建)
client.writeQuery({
query: getCustomerInfo,
data: {
todo:
__typename: 'Customer';
id: 1,
name: '一个客户'
},
},
variables: {
id: 1
}
});
6
结语
我们项目中对于 apollo-client 的使用暂时局限于这些,谈不上最佳实践,只是一些使用分享,还有许多的东西,例如订阅、缓存的更多用法、服务端渲染等值得我们继续探索和研究。
参考资料
[1] GraphQL: https://graphql.cn/
[2] apollo docs: https://www.apollographql.com/docs/react/
[3] graphql-code-generator: https://www.graphql-code-generator.com/docs/guides/react
[4] graphql-hooks: https://github.com/nearform/graphql-hooks
[5] relay: https://github.com/facebook/relay
[6] urql: https://github.com/FormidableLabs/urql
[7] graphql-request: https://github.com/prisma-labs/graphql-request
●●●
●●●
👇 点击阅读原文,直接体验 Demo