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
作者:觀遠 Atlas 團隊,集 BI 與 AI 之大成,通資料分析與業務決策之鍊路,以低代碼、元件化方式,為企業打造可插拔的創新資料應用,打通智能決策落地的最後一公裡,助力企業實作業務自主與決策更新。
來源-微信公衆号:觀遠資料技術團隊
出處:https://mp.weixin.qq.com/s/_XZeU2xxgzTGcTnwu6W8KQ