laitimes

The practice and application of GraphQL in Guanyuan data

author:Flash Gene

1

What is GraphQL

GraphQL is an API query language, different from RESTful API to obtain the specified data of a resource by a request, GraphQL separates resources from the request mode, and can define which data of resources need to be obtained by the front-end itself, and can also obtain the data of multiple resources on the server side through a single request, which solves the problem of excessive acquisition of front-end data and the need for back-end development to be aware of business scenarios.

The practice and application of GraphQL in Guanyuan data

For example, let's say you develop a book management system, and the product gives you several pages of development tasks, one to show all the books, one to show all the authors, one to show the book details, and one to show the author details. The RESTful API develops 4 interfaces to return data based on the data requirements of the product. GraphQL declares two corresponding data types (Book, Author), provides a list of corresponding data types and query methods for details, and allows the front-end to request data on demand according to product requirements.

After a few days, the product unsurprisingly increased the demand, requiring a field to be added to the list of books and the corresponding author information to be added to the book details. The RESTful API had no choice but to modify the list interface, and then let the front-end call one more detail interface on the details page to get the new information, and the front-end felt that the back-end should change the interface to add the new information to the original interface, and then there was a dispute between the front-end and back-end interfaces. GraphQL allows the front-end to request a few more fields on top of the original request, and the new requirements can be completed without changing the interface.

Paste a code that indicates the fields that need to be added in the example.

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

Although you can directly call the GraphQL API, considering the efficiency and development comfort, it is recommended to introduce a client-side class library to assist in development, GraphQL has a number of client-side class libraries, the more well-known ones are graphql-request, relay, urql, graphql-hooks and apollo-client mentioned in this article.

apollo-client combines React to encapsulate the request, modification, and error report of the request through hooks, so that you no longer need to implement the logic related to request data management, and you can get the data responsive update page or prompt the error message.

If you need front-end caching in your project, you have to consider that GraphQL is not particularly cache-friendly (there is no unique url, the request field is not fixed, etc.), so apollo-client provides a set of out-of-the-box caching solutions, which can solve most of the GraphQL caching problems with just a few simple configurations.

In addition to the above, apollo-client also provides a lot of hooks and components, and has an active ecological community, which can provide a lot of help for development, and we will make some specific explanations of the above in combination with our actual situation later.

3

Initial configuration

1.ApolloClient实例

ApolloLink

ApolloLink defines the process of a client sending a request to a service as a chained chain of objects that is executed sequentially, and can be extended or replaced to be customized. By default, the client uses HttpLink to send requests to the server.

Thanks to this design, we can easily add Authorization to the request header and centralize the processing of request errors. You can also add other functional logic, such as calculating the total time spent on requests, collecting data statistics, etc.

If you need to upload files, you can import the apollo-upload-client library, and you only need to replace HttpLink with the corresponding createUploadLink method.

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 ]),
})           

Cache settings

apollo-client provides 5 front-end data caching modes, which can be configured by setting fetchPolicy in a global or individual query request.

  • cache-first:

The default value is to check whether the cache is hit when getting the data, and return the data if it does, otherwise it sends a request to the server to get the data, update the cache, and return. It is suitable for scenarios where data does not change frequently, or requires a large amount of data and does not require high real-time performance.

  • cache-and-network:

When fetching data, it first checks whether the cache is hit, returns the data if it does, and then makes a request to the server to get the data, update the cache, and return regardless of whether it is hit. It is suitable for scenarios that require fast data acquisition but require real-time data.

  • network-only:

When fetching data, it does not check whether the cache is hit, and directly initiates a request to the server to get the data, update the cache, and return. It is suitable for scenarios with high real-time requirements.

  • no-cache:

The data is not written to the cache, and a request is made directly to the server to get the data and return. The use case is similar to network-only, except that there is no cached data.

  • cache-only:

No request is made, data is fetched directly from the cache, and an error is reported if the cache is not hit. It is suitable for offline scenarios, and you need to write data to the cache in advance.

tip: In actual use, if you use lazyQuery or refetch, the default value of cache is cache-and-network, and cache-first or cache-only cannot be used.

2. ApolloProvider

Now that we have an instance of apollo-client, we need to pass it to our React component tree so that we can use the functionality provided by apollo-client in any component, and then we need to use React's Context, but we don't need to create this context ourselves, apollo already provides ApolloProvider component to implement this scenario, all we need to do is register it on top of the root component.

import { ApolloProvider } from '@apollo/client'

return (
  <ApolloProvider client={client}>
    <RootComponent />
  </ApolloProvider>
)
           

4

request

1. Inquiries

Mainly through the two react hook APIs provided by apollo-client to implement query requests, while returning other fields that can be used for rendering, we no longer need to manage the state and data of the request, we only need to call the destructuring method to achieve page rendering. First, let's define the request, define the parameters, and return data format.

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
              }
          }
      }
  }
`           

Then, by calling the hook function, the data is requested, the deconstructed data is obtained, and the corresponding rendering logic is carried out.

import { gql, useQuery } from '@apollo/client';

// ...
  const { loading, error, data, refetch } = useQuery(getCustomerList, {
    variables: {
      skip: 0,
      take: 15
    }
  });
// ...           

In addition to the automated useQuery, manually executed hook functions are also provided.

import { gql, useLazyQuery } from '@apollo/client';

// ...
  const [ getList, { loading, error, data } = useLazyQuery(getCustomerList);
  getList({
    variables: {
      skip: 0,
      take: 15
    }
  })                                                         
// ...
           

2. Modifications

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

When we use ts and graphQL at the same time in development, we will find that gql has actually defined the types of parameters and return values at a certain level, and the gql code is written in the form of strings, which is prone to some writing errors without highlighting and formatting, and graphql-code-generator is naturally introduced into our project.

graphql-code-generator can automatically generate the corresponding data types and request methods by maintaining the gql code and combining with the schema defined by the server, with a simple example.

"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>;           

You can see that the automatically created file contains data definitions for the input parameters and return values of the request method, and a corresponding hook function is created for each request method in GQL.

// 修改后的调用方式,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

Cached data storage

In most cases, the caching of request data is a problem considered by the server, and the front-end only needs to pass the browser's caching policy to achieve the effect of requesting data caching. In some demand scenarios, the front-end will also complete the caching of request data locally, and the Restful API stores the request data in the form of key-value by identifying the request URL and parameters, but it was also mentioned before that the request URL of GraphQL is not unique, so in this case, how does apollo-client implement local data caching?

Let's look at the example first, the getCustomerList request, and then we'll get a series of cached data.

{
  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'
  }
}           

Where did this data come from?

First of all, apollo-client will split the data returned by the request according to the defined data type, generate a __typename field for each object as the identification of the data type by default, and then use the concatenated format of __typename and id (or _id) as the key of the cached data to form a data object in the form of key-value.

After splitting, each data object and the data returned by the request are associated through the __ref field, and then the query method name + parameter is used as the key of the request cache in ROOT_QUERY to complete the data cache of the entire request.

Thanks to the data splitting cache method, when we execute the mutation method to modify an object, the modified return value obtained can directly update the current cache, closing the entire request-modification cache chain.

However, when we actually use it in the project, because the usage of the cache is not high, the default cache logic adds the __typename field to the return value, which is more like a dirty data insertion, so the configuration of adding this field by default is canceled in the configuration item, and the cached data storage in this case will become similar to the Restful API and directly stored in the form of interfaces and parameters, and will not be split into objects of data types.

// 修改配置
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'
        }
      },
      // ....
    ]
  },
}           

In addition to the closed-loop cache maintenance logic in the above scenarios, apollo-client also provides methods to manually query and modify cached data to meet the cache requirements in different scenarios.

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

epilogue

The use of apollo-client in our project is temporarily limited to these, not to mention best practices, just some usage sharing, and many other things, such as subscriptions, more uses of caching, server-side rendering, etc., which are worth exploring and researching.

Resources

[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

Author: Guanyuan Atlas team, integrating BI and AI, through the link between data analysis and business decision-making, creates pluggable innovative data applications for enterprises in a low-code and component-based way, opens up the last mile of intelligent decision-making, and helps enterprises achieve business autonomy and decision-making upgrades.

Source-WeChat public account: Guanyuan Data Technical Team

Source: https://mp.weixin.qq.com/s/_XZeU2xxgzTGcTnwu6W8KQ