react + graphql + apollo-client技術棧簡要介紹(基于官方文檔v2.5)

歡迎進入我的博客閱覽此文章。


本文檔諸多例子來源于官方文檔,但也在編寫過程中添加了我對這套技術棧的一些理解,如果你更喜歡看官方文檔,請移步官網/官方文檔

為什么要使用apollo?
沒有redux繁瑣的action、reducer、dispatch……讓全局管理store變得簡單、直白!

使用redux管理狀態,重心是放在如何去拿數據上;而apollo把重心放在需要什么數據上。理解這一點非常重要!

好了,廢話不多說,我們立即開始!

準備工作

創建React項目

  1. 你可以使用create-react-app快速創建一個React應用,不熟悉create-react-app的小伙伴可以先行了解。
npm i create-react-app -g

create-react-app react-apollo-client-demo --typescript

cd react-apollo-client-demo

npm start
  1. 也可以在codesandbox上在線搭建React項目。方便快捷!

搭建GraphQL服務

  1. 你可以在github上fork graphpack項目,然后使用github賬號登錄codesandbox并導入該項目,即可零配置搭建一個在線的GraphQL服務。
    本文檔在編寫時在codesandbox上搭建了一個服務,可供參考:https://kdvmr.sse.codesandbox.io/
  2. 也可以在本地搭建自己的GraphQL服務,因不在本文檔討論范圍,所以暫不提供搭建步驟。

安裝需要的包

既然本文講的是graphql + react + apollo開發React App,所以需要安裝以下包來支撐,以前使用的redux、react-redux等包可以丟到一邊了。

PS:在apollo 1.0時代,本地狀態管理功能(本文檔后面作了介紹)還依賴于redux等相關技術。但現在apollo已經升級到2.0時代,已經完全拋棄了redux的依賴。

npm install apollo-boost react-apollo graphql --save

我們來看一下這三個包的作用:

  • apollo-boost:包含設置Apollo Client所需的核心包。如果你需要按自己的意愿定制化項目,可以自行選擇安裝單獨的包:
    apollo-client:apollo客戶端包
    • apollo-cache-inmemory:官方推薦的緩存包
    • apollo-link-http:用于獲取遠程數據的包
    • apollo-link-error:用于處理錯誤的包
    • apollo-link-state:本地狀態管理的包(2.5版本已集成到apollo-client
  • react-apollo:react的圖層集成(用react的組件方式來使用apollo)
  • graphql:解析GraphQL查詢

實例化Apollo客戶端

需要注意一點的是apollo-boostapollo-client都提供了ApolloClient,但是兩者需要的參數有一點差別。具體見各自API:

我們看一下使用apollo-boost實例化Apollo客戶端:

import ApolloClient from 'apollo-boost'

const client = new ApolloClient({
    // 如果你實在找不到現成的服務端,可以使用apollo官網提供的:https://48p1r2roz4.sse.codesandbox.io或者本教程的服務:https://kdvmr.sse.codesandbox.io/
    uri: '你的GraphQL服務鏈接'
})

以及使用apollo-client實例化Apollo客戶端:

import ApolloClient from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'

const client = new ApolloClient({
  link: createHttpLink({ 
    uri: '你的GraphQL服務鏈接' 
  })
})

編寫GraphQL查詢語句

如果對GraphQL語法不是很了解,請先移步graphQL基礎實踐

為了演示GraphQL查詢,我們暫且使用普通的請求看一段代碼:

import { gql } from 'apollo-boost'

// 實例化 Apollo 客戶端

client.query({
  query: gql`
    {
      rates(currency: "CNY") {
        currency
      }
    }
  `
}).then(result => console.log(result));

除了從apollo-boost導入gql,你還可以從graphql-tag這個包導入:

import gql from 'graphql-tag';

gql`...`

顯而易見,gql()的作用是把查詢字符串解析成查詢文檔。

連接Apollo客戶端到React

// ...
import React from 'react'
import { ApolloProvider } from 'react-apollo'

const App: React = () => {
  // ...
  
  return (
    <ApolloProvider client={client}>
      <div>App content</div>
    </ApolloProvider>
  )
}

export default App

ApolloProvider(詳細API)有一個必需參數client

和redux一樣(redux使用<Provider/>組件包裹React App),react-apollo需要<ApolloProvider />組件來包裹整個React App,以便將實例化的client放到上下文中,就可以在組件樹的任何位置訪問到它。
另外,還可以使用withApollo來包裹組件,以在組件內部獲取到client實例(還有很多獲取實例的方法,文檔后面有介紹),
詳情請參考withApollo()

Query組件與Mutation組件

在graphql中,query操作代表查詢,mutation操作代表增、刪和改,他們對應REST API的GET與POST請求,但要注意在實際的請求過程中Query或許并不是GET請求,這里只是為了方便大家理解做的假設!

獲取數據——Query組件

import { Query } from "react-apollo";
import { gql } from "apollo-boost";

const ExchangeRates = () => (
  <Query
    query={gql`
      {
        rates(currency: "USD") {
          currency
          rate
        }
      }
    `}
  >
    {
      ({ loading, error, data }) => {
        if (loading) return <p>Loading...</p>;
        if (error) return <p>Error :(</p>;
    
        return data.rates.map(({ currency, rate }) => (
          <div key={currency}>
            <p>{currency}: {rate}</p>
          </div>
        ));
      }
    }
  </Query>
);

恭喜,你剛剛創建了第一個React Query組件!??????

代碼解析:

可以看到,在Query組件內,有一個匿名函數,這個匿名函數有一些參數,最常用的有:loadingerrordata
它們分別代表組件的加載狀態、組件的加載錯誤提示、以及組件加載到的數據。

Query組件是從react-apollo導出的React組件,它使用render prop模式與UI共享GraphQL數據。(即我們可以從組件的props獲取到GraphQL查詢返回的數據)
Query組件還有很多其他props,上面就展示了一個query屬性,其他的如:

  • children(根據查詢結果顯示要渲染的UI)
  • variables(用來傳遞查詢參數到gql())
  • skip(跳過這個查詢,比如登錄時,驗證失敗,我們使用skip跳過這個查詢,則登錄失敗)

更多props詳見Query API

更新數據——Mutation組件

更新數據包括新增、修改和刪除,這些操作統一使用Mutation組件。
Mutation組件和Query組件一樣,使用render prop模式,但props有差別,Mutation API

import gql from 'graphql-tag';
import { Mutation } from "react-apollo";

const ADD_TODO = gql`
  mutation AddTodo($type: String!) {
    addTodo(type: $type) {
      id
      type
    }
  }
`;

const AddTodo = () => {
  let input;

  return (
    <Mutation mutation={ADD_TODO}>
      {
        (addTodo, { data }) => (
          <div>
            <form
              onSubmit={e => {
                e.preventDefault();
                addTodo({ variables: { type: input.value } });
                input.value = "";
              }}
            >
              <input
                ref={node => {
                  input = node;
                }}
              />
              <button type="submit">Add Todo</button>
            </form>
          </div>
        )
      }
    </Mutation>
  );
};

我們來梳理一下代碼:

  • 首先,創建用于mutation(突變)的GraphQL,mutation需要一個字符串類型的參數type。它將用于mutation的GraphQL語句包裝在gql方法中,并將其傳遞給Mutation組件props
  • Mutation組件內需要一個匿名函數作為子函數(也稱為render prop函數),同Query組件,但參數有差異。
  • render prop函數的第一個參數是Mutation組件內部定義的mutate()函數。為了提高代碼可讀性,這里取名為addTodo
    也可以直接用“mutate”表示mutate函數,通過調用它來告訴Apollo Client,接下來要觸發mutation(即觸發提交表單的POST請求,在onSubmit事件里面可以看見addTodo函數被調用了)。
  • render prop函數的第二個參數是一個對象,這個對象有多個屬性,包括data(mutation的結果,POST請求的返回值)、loading(加載狀態)和error(加載過程中的錯誤信息),同Query組件

mutate函數(也就是上面命名的addTodo函數)可選地接受變量,如:

  • optimisticResponse
  • refetchQueries和update(這些函數就是后面用來更新緩存的)
  • ignoreResults:忽略mutation操作返回的結果(即忽略POST請求的返回值)

你也可以將這些值作為props傳遞給Mutation組件。詳細的介紹請移步mutate函數 API

到這里,我們能發出客戶端請求,也能得到服務器返回的結果,那接下來就著手怎么處理這些數據,然后渲染到UI上。我們看一下redux在這一步是怎么處理的:

  • dispatch觸發數據請求
  • reducer根據之前定義的action處理得到的新數據,把數據保存到store中
  • react-redux的connect連接store與React組件
  • mapStateToProps/mapDisToProps完成render prop。

以上步驟,全靠一行一行的代碼手動實現,我們再來看一下apollo是怎么處理的:

  • cache.writeQuery()

沒錯,你沒看錯,就是這一個API,搞定以上redux需要一大堆代碼才能完成的數據更新!writeQuery相當于通過一種方式來告訴Apollo Client:
我們已經成功發出POST請求并得到了返回的結果了,現在把結果給你,你更新一下本地的緩存吧!
并且如果你的數據寫得很規范(呃,其實它叫范式化,不要急,后面有介紹),甚至連這一句話都不用寫,當你執行query或mutation后,UI便會自動根據新的數據更新UI!!

更新緩存——mutation后內部自動query

有時,當執行mutation時,GraphQL服務器和Apollo緩存會變得不同步。當執行的更新取決于本地緩存中已有的數據時,會發生這種情況。例如,本地緩存了一張列表,
當刪除列表中的一項或添加一項新的數據,當我們執行mutation后,graphql服務端和本地緩存不一致,我們需要一種方法來告訴Apollo Client去更新項目列表的查詢,
以獲取我們mutation后新的項目列表數據;又或者我們僅僅使用mutation提交一張表單,本地并沒有緩存這張表單的數據,所以我們并不需要新的查詢來更新本地緩存。

下面來看一段代碼:

import gql from 'graphql-tag';
import { Mutation } from "react-apollo";

const ADD_TODO = gql`
  mutation AddTodo($type: String!) {
    addTodo(type: $type) {
      id
      type
    }
  }
`;

const GET_TODOS = gql`
  query GetTodos {
    todos
  }
`;

const AddTodo = () => {
  let input;

  return (
    <Mutation
      mutation={ADD_TODO}
      update={(cache, { data: { addTodo } }) => {
        const { todos } = cache.readQuery({ query: GET_TODOS });
        cache.writeQuery({
          query: GET_TODOS,
          data: { todos: todos.concat([addTodo]) },
        });
      }}
    >
      {addTodo => (
        <div>
          <form
            onSubmit={e => {
              e.preventDefault();
              addTodo({ variables: { type: input.value } });
              input.value = "";
            }}
          >
            <input
              ref={node => {
                input = node;
              }}
            />
            <button type="submit">Add Todo</button>
          </form>
        </div>
      )}
    </Mutation>
  );
};

通過這段代碼可以看見,update()函數可以作為props傳遞給Mutation組件,但它也可以作為prop傳遞給mutate函數,即:

// 借用上面的mutate(重命名為addTodo)函數來舉例
addTodo({
  variables: { type: input.value },
  update: (cache, data: { addTodo }) => {
    // ...
  }
})

update: (cache: DataProxy, mutationResult: FetchResult):用于在發生突變(mutation)后更新緩存

參數:

  • cache,這個參數詳細講又可以講幾節課,所以這里只簡單介紹一下,詳細API
    • cache通常是InMemoryCache的一個實例,在創建Apollo Client時提供給Apollo Client的構造函數(怎么創建的Apollo Client?請返回創建一個apollo客戶端復習一下)
    • InMemoryCache來自于一個單獨的包apollo-cache-inmemory。如果你使用apollo-boost,這個包已經被包含在里面了,無需重復安裝。
    • cache有幾個實用函數,例如cache.readQuerycache.writeQuery,它們允許您使用GraphQL讀取和寫入緩存。
    • 另外還有其他的方法,例如cache.readFragmentcache.writeFragmentcache.writeData,詳細API
  • mutationResult,一個對象,對象里面的data屬性保存著執行mutation后的結果(POST請求后得到的數據),詳細API
    • 如果指定樂觀響應,則會更新兩次update函數:一次是樂觀結果,另一次是實際結果。
    • 您可以使用您的變異結果來使用cache.writeQuery更新緩存。

對于update函數,當你在其內部調用cache.writeQuery時,更新操作會觸發Apollo內部的廣播查詢(broadcastQueries),而廣播查詢又會觸發緩存中與本次mutation相關的數據的自動更新——自動使用受影響組件的GraphQL進行查詢并更新UI。
因此當執行mutation后,我們不必手動去執行相關組件的查詢,Apollo Client在內部已經做好了所有工作,這區別于redux在dispatch后所做的一切處理數據的工作。

有時,update函數不需要為所有mutation更新緩存(比如提交了一張表單)。所以,Apollo提供單獨的cache.writeQuery方法,來觸發相關緩存的查詢,以更新本地緩存。
所以需要注意:僅僅只在update函數內部調用cache.writeQuery()才會觸發廣播行為。在其他任何地方,cache.writeQuery只會寫入緩存,并且所做的更改不會廣播到視圖層。
為了避免給代碼造成混淆,推薦在未使用update函數時,使用Apollo Client實例對象clientclient.writeQuery方法將數據寫入緩存。

解析代碼:

由于我們需要更新顯示TODOS列表的查詢,因此首先使用cache.readQuery從緩存中讀取數據。
然后,我們將mutation后得到的新todo與現有todo列表合并起來,并使用cache.writeQuery將查詢到的數據寫回緩存。
既然我們已經指定了一個update函數,那么一旦新的todo從服務器返回,我們的用戶界面就會用它進行響應性更新(廣播給其他與此緩存數據有關的組件的GraphQL查詢,讓他們及時更新更新相關緩存到UI上)。

Apollo還提供一種的方法來及時地修改本地緩存以快速渲染UI并觸發相關緩存的查詢,待查詢返回新的數據后再真正更新本地緩存,詳見樂觀更新

基于樂觀UI,如果您運行相同的查詢兩次,則不會看到加載指示符(Apollo Client返回的loading字段)。apollo會檢測當前的請求參數是否變化,然后判斷是否向服務器發送新的請求。

Apollo范式化緩存 API

import gql from 'graphql-tag';
import { Mutation, Query } from "react-apollo";

const UPDATE_TODO = gql`
  mutation UpdateTodo($id: String!, $type: String!) {
    updateTodo(id: $id, type: $type) {
      id
      type
    }
  }
`;

// 注意:這里通過graphql得到的todos數據是一個包含id和type字段的對象的數組,與 UPDATE_TODO 里面的字段(主要是id)對應
const GET_TODOS = gql`
  query GetTodos {
    todos
  }
`;

const Todos = () => (
  <Query query={GET_TODOS}>
    {({ loading, error, data }) => {
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error :(</p>;

      return data.todos.map(({ id, type }) => {
        let input;

        return (
          <Mutation mutation={UPDATE_TODO} key={id}>
            {updateTodo => (
              <div>
                <p>{type}</p>
                <form
                  onSubmit={e => {
                    e.preventDefault();
                    updateTodo({ variables: { id, type: input.value } });

                    input.value = "";
                  }}
                >
                  <input
                    ref={node => {
                      input = node;
                    }}
                  />
                  <button type="submit">Update Todo</button>
                </form>
              </div>
            )}
          </Mutation>
        );
      });
    }}
  </Query>
);

注意:這一次在mutate函數(這里命名為updateTodo)里并沒有調用update函數,在也沒有傳遞update函數給Mutation組件,但是UI會立即更新。這就是范式化緩存的魅力了。

范式化緩存——InMemoryCache在將數據保存到存儲之前對數據進行范式化,方法是將結果拆分為單個對象,為每個對象創建唯一標識符,并將這些對象存儲在展平的數據結構中(創建的唯一標識符為這些對象的鍵,成為緩存鍵)。
默認情況下,InMemoryCache將嘗試使用常見的id_id的主鍵作為唯一標識符(如果它們與對象上的__typename字段一起存在)。

image
image

如果未指定id_id,或者未指定__typename,則InMemoryCache將按照查詢到對象的層級關系依次回退到根查詢為止。

image

例如ROOT_QUERY.allPeople.0將作為數據中allPeople[0]對象的緩存鍵(cache key)被存儲到緩存的根查詢(ROOT_QUERY)下。(在展平的數據結構中,所有對象都在ROOT_QUERY下):

即使我們不打算在我們的UI中使用mutation返回的結果,我們仍然需要返回更新的ID和屬性,以便我們的UI進行自動更新。

以上代碼中,我們不需要指定update函數,因為TODOS查詢將使用緩存中更新的TODO數據自動重建查詢結果。

結合上一節介紹的update函數那樣——并非每次mutation都需要使用update函數——其原因就是依據Apollo Cache的范式化數據結構,
在盡量減少手動操作數據的情況下自動更新UI,當前后端都規范化數據后(特別是唯一標識符id的統一, __typename字段的定義),
querymutation操作后,我們幾乎不用手動處理數據,就能實現UI的自動更新。

例如:如果只需要更新緩存里面的單條數據,只需要返回這條數據的ID和要更新的屬性即可,這種情況下通常不需要使用update函數。

如果想要自定義唯一標識符,即不用默認的ID來生成緩存鍵,可以使用InMemoryCache構造函數的dataIdFromObject函數:

const cache = new InMemoryCache({
  dataIdFromObject: object => object.key || null
});

在指定自定義dataIdFromObject時,Apollo Client不會將類型名稱添加到緩存鍵,因此,如果您的ID在所有對象中不唯一,則可能需要在dataIdFromObject中包含__typename

在谷歌瀏覽器中安裝apollo devtools擴展(需要科學上網),可以清晰看到這種范式化緩存的存儲狀態。

中場休息

使用redux管理狀態,重心是放在如何去拿數據上;而apollo把重心放在需要什么數據上。理解這一點非常重要!

還記得這句話嗎?我們在本文檔開篇的時候介紹過。現在理解了嗎?現在,我們回過頭來梳理一下自己學到的知識點:

  • 當學習了怎樣去獲取數據(query)以及更新數據和修改數據(mutation)后,原來Apollo和React結合,原來組件可以這么簡單的與數據交互!
  • 當學習了Apollo緩存后,我們對Apollo數據存儲的理解又上升了一個臺階,把所有查詢回來的對象一一拆分,通過唯一標識符的形式把一個深層級的對象展平,直觀展現在緩存的根查詢中。
  • 當學習了Apollo的范式化緩存后,我們才知道,原來自動更新UI可以如此優雅!我們甚至不需要管理數據,只需按照規范傳遞數據即可!

本地狀態管理 詳情

上半場我們接觸了本地與服務端的遠程數據交互,接下來,我們將進入本地的狀態管理

Apollo Client在2.5版本具有內置的本地狀態處理功能,允許將本地數據與遠程數據一起存儲在Apollo緩存中。要訪問本地數據,只需使用GraphQL查詢即可。

而在2.5版本之前,如果想要使用本地狀態管理,必須引入已經廢棄的一個包apollo-link-state(API),
這個包在2.5版本已被廢棄,因為從2.5版本開始,這個包的功能已經集成到apollo的核心之中,不再額外維護一個單獨的包。而在apollo的1.x版本,如果要實現本地狀態管理,依然得引入redux。

Apollo Client有兩種主要方法可以執行局部狀態突變:

  • 第一種方法是通過調用cache.writeData直接寫入緩存。
    更新緩存那一節,我們已經詳細介紹過cache.writeData的用法,以及其余update函數的搭配使用。
  • 第二種方法是創建一個帶有GraphQL突變(mutation)的Mutation組件,該組件調用本地客戶端解析器(resolvers)。
    如果mutation依賴于緩存中的現有值,我們建議使用解析器(resolvers,后面兩節將介紹,目前只需知道它的存在,它和apollo-server端的resolver完全相同)。

直接寫入緩存

import React from 'react';
import { ApolloConsumer } from 'react-apollo';

import Link from './Link';

const FilterLink = ({ filter, children }) => (
  <ApolloConsumer>
    {client => (
      <Link
        onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
      >
        {children}
      </Link>
    )}
  </ApolloConsumer>
);

Apollo在ApolloConsumer組件(API)或Query組件的render prop中注入了Apollo Client實例,
所以當使用這些組件時,我們可以直接從組件的props中拿到client實例

直接寫入緩存不需要GraphQLmutate函數或resolvers函數。因此我們在上面的代碼中沒有使用它們,我們直接在onClick事件函數里面調用client.writeData來寫入緩存。

但是只建議將直接寫入緩存用于簡單寫入,例如寫入字符串或一次性寫入。
重要的是要注意直接寫入并不是作為GraphQL突變實現的,因此不應將它們包含在復雜的開發模式之中。
它也不會驗證你寫入緩存的數據是否為有效GraphQL數據的結構。
如果以上提到的任何一點對您很重要,則應選擇使用本地resolvers

@client 指令

上一節提到過,Query組件的render prop同樣包含client實例。所以配合@client指令,我們可以在Query組件中輕松地從cacheresolvers獲取本地狀態。
或許換個方式介紹大家能理解得更透徹:配合@client指令,我們可以在Query組件中輕松地從cache獲取本地狀態,或者通過resolverscache獲取本地狀態。

import React from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';

import Link from './Link';

const GET_VISIBILITY_FILTER = gql`
  {
    visibilityFilter @client
  }
`;

const FilterLink = ({ filter, children }) => (
  <Query query={GET_VISIBILITY_FILTER}>
    {({ data, client }) => (
      <Link
        onClick={() => client.writeData({ data: { visibilityFilter: filter } })}
        active={data.visibilityFilter === filter}
      >
        {children}
      </Link>
    )}
  </Query>
);

代碼解讀:

@client指令告訴Apollo Client在本地獲取數據(cache或resolvers),而不是將其發送到graphql服務器。
在調用client.writeData后,render prop函數上的查詢結果將自動更新。同時所有緩存的寫入和讀取都是同步的,所以不必擔心加載狀態(loading)。

本地解析器——resolvers

終于見到了你——resolvers!前面幾節都一筆帶過了resolvers(解析器),現在,我們來了解它到底有什么強大的功能。

如果要依賴本地狀態實現GraphQL的突變,我們只需要在本地resolvers對象中指定一個與之對應的函數即可。
在Apollo Client的實例化中,resolvers映射為一個對象,這個對象中保存著每一個用于本地突變的resolver函數
當在GraphQL的字段上找到@client指令時,Apollo Client會在resolvers對象中尋找與之對應的resolver函數,這個對應關系是通過resolvers的鍵來關聯的。

即:當執行沒有加@client指令的查詢或突變時,GraphQL文檔中的字段已預定義在了服務端,所以我們只需在查詢或突變時按照服務端定義的字段編寫GraphQL文檔即可;
當加上@client指令后,Apollo Client不會向服務端發送請求,轉而在自己內部尋找GraphQL文檔內指定的字段。但是,我們怎么去訪問本地的這些字段呢?或許他們根本就不存在。
(關于@client的運作方式,請參考官方文檔中關于——本地數據查詢流程的部分,由于篇幅原因,本文檔不再詳細介紹。)
這時,我們就需要自己定義可以訪問這些字段的方式——resolver,在解析器對象(resolvers)中定義一個解析函數(resolver),以供GraphQL查詢或突變在使用了@client指令時調用,
這樣就建立了GraphQL查詢或突變與Apollo Client之間的聯系,通過這個函數可以解析有@client指令控制的查詢或突變,因此這個函數被命名為解析函數,意指從本地解析函數中尋找GraphQL文檔中指定的字段的值。

那解析函數定義在哪里呢?

其實它在ApolloClient的構造函數中,也就是說我們實例化Apollo Client時,需要傳遞resolvers給它。

解析器,它和client-server的resolver函數完全相同:

  fieldName: (obj, args, context, info) => result;

obj {object}: 包含父字段上resolver函數返回的結果的對象,或者為DOM樹最頂層的查詢或突變的ROOT_QUERY對象
args {object}: 包含傳遞到GraphQL文檔中的所有參數的對象。例如,如果使用updateNetworkStatus(isConnected:true)觸發查詢或突變,則args{isConnected:true}
context {object}: React組件與Apollo Client網絡堆棧之間共享的上下文信息的對象。除了可能存在的任何自定義context屬性外,本地resolvers始終會收到以下內容:

  • context.client: Apollo Client的實例
  • context.cache: Apollo Cache的實例
    context.cache.readQuery, .writeQuery, .readFragment, .writeFragment, and .writeData: 一系列用于操作cache的API
  • context.getCacheKey: 使用__typenameid從cache中獲取key
    info {object}: 有關查詢執行狀態的信息。實際中,你可能永遠也不會使用到這個參數。
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const client = new ApolloClient({
  cache: new InMemoryCache(),
  resolvers: {
    Mutation: {
      toggleTodo: (_root, variables, { cache, getCacheKey }) => {
        const id = getCacheKey({ __typename: 'TodoItem', id: variables.id })
        const fragment = gql`
          fragment completeTodo on TodoItem {
            completed
          }
        `;
        const todo = cache.readFragment({ fragment, id });
        const data = { ...todo, completed: !todo.completed };
        cache.writeData({ id, data });
        return null;
      },
    },
  },
});

代碼解析:

為了切換todo的狀態,首先需要查詢緩存以找出todo當前狀態的內容,然后通過使用cache.readFragment從緩存中讀取片段來實現此目的。
此函數采用fragment和id,它對應于item的緩存鍵(cache key)。我們通過調用context中的getCacheKey并傳入項目的__typenameid來獲取緩存鍵。

一旦讀取了fragment,就可以切換todo的已完成狀態并將更新的數據寫回緩存。由于我們不打算在UI中使用mutation的返回結果,因此我們返回null,因為默認情況下所有GraphQL類型都可以為空。

下面,我們來看一下怎么調用這個toggleTodo解析函數(觸發toggleTodo突變):

import React from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

const TOGGLE_TODO = gql`
  mutation ToggleTodo($id: Int!) {
    toggleTodo(id: $id) @client
  }
`;

const Todo = ({ id, completed, text }) => (
  <Mutation mutation={TOGGLE_TODO} variables={{ id }}>
    // 特別注意,此toggleTodo非解析器里面的toggleTodo,這個toggleTodo是我們之前介紹過的mutate函數,這里被更名為‘toggleTodo’而已,不要混淆了
    {toggleTodo => (
      <li
        onClick={toggleTodo}
        style={{
          textDecoration: completed ? 'line-through' : 'none',
        }}
      >
        {text}
      </li>
    )}
  </Mutation>
);

代碼解析:

首先,我們創建一個GraphQL突變文檔,它將我們想要切換的item的id作為唯一的參數。我們通過使用@client指令標記GraphQLtoggleTodo字段來指示這是一個本地突變。
這將告訴Apollo Client調用我們本地突變解析器(resolvers)里面的toggleTodo解析函數來解析該字段。然后,我們創建一個Mutation組件,就像我們操作遠程突變一樣。
最后,將GraphQL突變傳遞給組件,并在render prop函數的UI中觸發它。

查詢本地狀態

查詢本地數據與查詢GraphQL服務器非常相似。唯一的區別是本地查詢在字段上添加了@client指令,以指示它們應該從Apollo Client cache或resolvers中解析。

我們來看一個例子:

import React from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';

import Todo from './Todo';

const GET_TODOS = gql`
  {
    todos @client {
      id
      completed
      text
    }
    visibilityFilter @client
  }
`;

const TodoList = () => (
  <Query query={GET_TODOS}>
    {
      ({ data: { todos, visibilityFilter } }) => (
        <ul>
          {
            getVisibleTodos(todos, visibilityFilter).map(todo => (
              <Todo key={todo.id} {...todo} />
            ))
          }
        </ul>
      )
    }
  </Query>
);

代碼解析:

創建GraphQL查詢并將@client指令添加到GraphQL文檔的todos和visibilityFilter字段。
然后,我們將查詢傳遞給Query組件@client指令讓Query組件知道應該從Apollo Client緩存中提取todos和visibilityFilter,或者使用預定義的本地resolver解析。

由于上面的查詢在安裝組件后立即運行,如果cache中沒有item或者沒有定義任何本地resolver,我們該怎么辦?

我們需要在運行查詢之前將初始狀態寫入緩存,以防止錯誤輸出。

初始化本地狀態

通常,我們需要將初始狀態寫入緩存,以便在觸發mutation之前查詢數據的所有組件都不會出錯。
要實現此目的,可以使用cache.writeData為初始值準備緩存。

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: { /* ... */ },
});

cache.writeData({
  data: {
    todos: [],
    visibilityFilter: 'SHOW_ALL',
    networkStatus: {
      __typename: 'NetworkStatus',
      isConnected: false,
    },
  },
});

注意:Apollo v2.4和v2.5寫入初始本地狀態的方式不一樣,詳情參考官方API

重置本地狀態/緩存

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: { /* ... */ },
});

const data = {
  todos: [],
  visibilityFilter: 'SHOW_ALL',
  networkStatus: {
    __typename: 'NetworkStatus',
    isConnected: false,
  },
};

cache.writeData({ data });

client.onResetStore(() => cache.writeData({ data }));

使用client.onResetStore方法可以重置緩存。

同時請求本地狀態和遠程數據

mutation ToggleTodo($id: Int!) {
  toggleTodo(id: $id) @client
  getData(id: $id) {
    id,
    name
  }
}

只需在需要從本地查詢的字段后面加上@client指令即可。

使用@client字段作為變量

在同一個graphql語句中,還可以將從本地查到的狀態用于下一個查詢,通過@export指令

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';

const query = gql`
  query currentAuthorPostCount($authorId: Int!) {
    currentAuthorId @client @export(as: "authorId")
    postCount(authorId: $authorId)
  }
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
  cache,
  resolvers: {},
});

cache.writeData({
  data: {
    currentAuthorId: 12345,
  },
});

// ... run the query using client.query, the <Query /> component, etc.

在上面的示例中,currentAuthorId首先從緩存加載,然后作為authorId變量(由@export(as:“authorId”)指令指定)傳遞到后續postCount字段中。
@export指令也可用于選擇集中的特定字段,如:

@export指令還可以用于選擇集中的特定字段

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';

const query = gql`
  query currentAuthorPostCount($authorId: Int!) {
    currentAuthor @client {
      name
      authorId @export(as: "authorId")
    }
    postCount(authorId: $authorId)
  }
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
  cache,
  resolvers: {},
});

cache.writeData({
  data: {
    currentAuthor: {
      __typename: 'Author',
      name: 'John Smith',
      authorId: 12345,
    },
  },
});

// ... run the query using client.query, the <Query /> component, etc.

@export指令使用不僅限于遠程查詢;它還可以用于為其他@client字段或選擇集定義變量:(注意以下代碼中GraphQL文檔的currentAuthorIdpostCount字段之后都有@client指令)

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';

const query = gql`
  query currentAuthorPostCount($authorId: Int!) {
    currentAuthorId @client @export(as: "authorId")
    postCount(authorId: $authorId) @client
  }
`;

const cache = new InMemoryCache();
const client = new ApolloClient({
  cache,
  resolvers: {
    Query: {
      postCount(_, { authorId }) {
        return authorId === 12345 ? 100 : 0;
      },
    },
  },
});

cache.writeData({
  data: {
    currentAuthorId: 12345,
  },
});

// ... run the query using client.query, the <Query /> component, etc.

動態注入resolver

有時,當我們在APP中使用了代碼拆分,如使用react-loadable時,我們并不是很希望所有的resolver都在初始化Apollo客戶端的統一寫在一起,而是希望單獨拆分到各自的模塊中,這樣在APP編譯后,
每個模塊各自resolver將包含在自己的包中,這樣也有助于減少入口文件的大小,使用addResolverssetResolvers即可辦到(API),
例如以下代碼:

import Loadable from 'react-loadable';

import Loading from './components/Loading';

export const Stats = Loadable({
  loader: () => import('./components/stats/Stats'),
  loading: Loading,
});
import React from 'react';
import { ApolloConsumer, Query } from 'react-apollo';
import gql from 'graphql-tag';

const GET_MESSAGE_COUNT = gql`
  {
    messageCount @client {
      total
    }
  }
`;

const resolvers = {
  Query: {
    messageCount: (_, args, { cache }) => {
      // ... calculate and return the number of messages in
      // the cache ...
      return {
        total: 123,
        __typename: 'MessageCount',
      };
    },
  },
};

const MessageCount = () => {
  return (
    <ApolloConsumer>
      {(client) => {
        client.addResolvers(resolvers);
        return (
          <Query query={GET_MESSAGE_COUNT}>
            {({ loading, data: { messageCount } }) => {
              if (loading) return 'Loading ...';
              return (
                <p>
                  Total number of messages: {messageCount.total}
                </p>
              );
            }}
          </Query>
        );
      }}
    </ApolloConsumer>
  );
};

export default MessageCount;

脫離React標簽的寫法

由于編程習慣的不同,有些人(比如我),并不是很喜歡(或者說成習慣)把邏輯代碼React標簽混合寫在一起,就如我們從本文檔開始一路看來所有的示例代碼那樣!
個人覺得在一個大型的項目中把Query標簽Mutation標簽以及其他的各種標簽層層嵌套,再加上各種邏輯實現代碼,全部擠在React組件中,真的是一件糟糕的事情。
雖然官方推崇這種寫法(以上絕大部分代碼是官方文檔的示例),他們給出的理由是這樣寫更方便,更簡單!

因人而異吧!

我個人更傾向于把GraphQL和Apollo的邏輯部分React組件分離開,我們可以使用react-apollo庫提供的graphqlcompose方法做到分離。
當然,在react-apollo這個包中并不止這兩個方法,還有其他的方法,請參考React Apollo API

下面展示一段分離開的代碼示例:

import { Avatar, Card, Icon } from 'antd'
import gql from 'graphql-tag'
import * as React from 'react'
import { useEffect, useRef } from 'react'
import { compose, graphql } from 'react-apollo'
import { QueryState, Typename } from 'src/config/clientState'
import { GET_COMPONENTS_LIST, GET_QUERY_STATE } from 'src/graphql'
import { getCorrectQueryState } from 'src/util'
import Pagination from '../pagination'
import './index.scss'

const { Meta } = Card

type Type = {
  id: number,
  name: string
}

type Author = {
  username: string,
  email: string,
  avatar: string
}

type Component = {
  id: number,
  name: string,
  version: string,
  chineseName: string,
  description: string,
  type: Type,
  url: string,
  author: Author,
  previewUrl: string,
  isOwn: boolean,
  isStored: boolean
}

type ListComponent = {
  data: {
    components: Component[],
    compCount: number,
  },
  componentsCollect: ([id, isCollect]: [number, boolean]) => void,
  queryState: QueryState
}

const ListComponent = (props: ListComponent) => {
  const cardList: React.MutableRefObject<HTMLDivElement | null> = useRef(null)
  const { data, componentsCollect, queryState } = props

  // 加載時組件卡片的動畫效果,配合React的ref和key屬性使用
  useEffect(() => {
    if(cardList.current) {
      cardList.current.childNodes.forEach((element: HTMLElement, index: number) => {
        setInterval(() => {
          element.classList.add('card-load-anim')
        }, index * 40)
      })
    }
  })

  return (
    <div className='m-list'>
      <div className='m-cards' ref={cardList} key={Math.random()}>
        {
          !data || !data.components
            ? null
            : data.components.map((o: Components, i: number) => {
              return (
                <Card
                  key={`m-list-btn-${i}`}
                  hoverable={true}
                  cover={<img alt='example' src='https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png' />}
                  actions={
                    [
                      <a
                        className={`${o.isOwn ? 'm-list-btn-text-disabled' : 'm-list-btn-text'}`}
                        key={`m-list-btn-${i}-1`}
                        href='javascript:void(0)'
                        onClick={!o.isOwn ? componentsCollect.bind(null, [o.id, !o.isStored]) : null}
                      >
                        <Icon type='copy' />
                        {o.isStored ? '取消收藏' : '收藏'}
                      </a>,
                      <a
                        className='m-list-btn-text'
                        key='m-list-btn-2'
                        href='javascript:void(0)'
                      >
                        <Icon type='file-search' />
                        文檔
                      </a>
                    ]
                  }
                >
                  <Meta
                    avatar={<Avatar src='https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' />}
                    title={o.name}
                    description={o.description}
                  />
                </Card>
              )
            })
        }
      </div>
      <Pagination totalPages={Math.ceil(data.compCount / queryState.pagination.size)} total={data.compCount} />
    </div>
  )
}

export default compose(
  graphql(
    gql(GET_QUERY_STATE),
    {
      props: ({ data: { queryState } }: any) => ({ queryState })
    }
  ),
  graphql(
    gql(GET_COMPONENTS_LIST),
    {
      options: ({ queryState }: any) => ({
        variables: {
          ...getCorrectQueryState(queryState)
        }
      })
    }
  ),
  graphql(gql`
    mutation ($id: Int!, $isCollect: Boolean!){
      storeComponent(id: $id, isStore: $isCollect)
    }
  `, {
    props: ({ mutate }: any) => ({
      componentsCollect: ([id, isCollect]: [number, boolean]) => {
        debugger
        mutate({
          variables: { id, isCollect },
          optimisticResponse: {
            __typename: Typename.Mutation,
            storeComponent: {
              __typename: Typename.Component,
              id,
              isStored: isCollect
            }
          }
        })
      }
    })
  })
)(ListComponent)

寫在最后:

如果認真學習完這扁文檔,相信你對Apollo技術棧開發React應用已經算是入門了,今后開發時遇到問題,多看一看官方文檔,相信你會很快掌握它。

由于水平有限,這篇文章是我自己一邊翻譯一邊加入自己的理解而寫成的,其中肯定少不了一些不妥或錯誤的地方,歡迎大家指正!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容