天天看點

GraphQL 在觀遠資料的實踐與應用

作者:閃念基因

1

GraphQL 是什麼

GraphQL 是一種 API 的查詢語言,不同于 RESTful API 的一個請求擷取一個資源的指定資料,GraphQL 将資源和請求方式分離,可以由前端自己定義需要擷取資源的哪些資料,也可以通過一個請求擷取服務端多個資源的資料,解決了前端資料過度擷取以及後端開發需要感覺業務場景的問題。

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

作者:觀遠 Atlas 團隊,集 BI 與 AI 之大成,通資料分析與業務決策之鍊路,以低代碼、元件化方式,為企業打造可插拔的創新資料應用,打通智能決策落地的最後一公裡,助力企業實作業務自主與決策更新。

來源-微信公衆号:觀遠資料技術團隊

出處:https://mp.weixin.qq.com/s/_XZeU2xxgzTGcTnwu6W8KQ