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

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

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

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

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

準備工作

創建React項目

  1. 本篇文章的隨堂練習可以使用create-react-app快速創建一個React應用,不熟悉create-react-app的小夥伴可以先行了解。
  2. 也可以在codesandbox上在線搭建React項目。
npm i create-react-app -g

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

cd react-apollo-client-demo

npm start

搭建graphql服務

  1. 服務端可以在github上forkgraphpack項目,然後在codesandbox裏面導入該項目即可零配置搭建一個在線的graphql服務。
  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等。
  • react-apollo:react的圖層集成(用react的方式來使用apollo)
  • graphql:解析GraphQL查詢

創建一個apollo客戶端

import ApolloClient from 'apollo-boost'
import config from './config'


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

需要注意一點的是apollo-boostapollo-client都提供了ApolloClient,但是兩者需要的參數有一點差別。
簡單來說,apollo-boost這個包相當於一個大集合,把我們需要的常用的功能都集成在裏面,它包括apollo-client。具體見各自API:

  • apollo-boost導出的ApolloClient對象(詳細API):集成官方核心功能的一個大集合對象
  • apollo-client導出的ApolloClient對象(詳細API):默認爲App所在的同一主機上的graphql端點,要自定義uri還需引入apollo-link-http包。

編寫graphql查詢語句

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

爲了演示graphql查詢,我們暫且使用普通的請求看一下:

import { gql } from 'apollo-boost'

// ...

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.FC = () => {
  // ...
  
  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請求。

獲取數據(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>
);

恭喜,您剛剛創建了第一個Query組件!🎉

可以看到,在Query組件內,有一個匿名函數,這個匿名函數有三個參數:loading,error,data。
分別代表組件的加載狀態、組件的加載錯誤提示、以及組件加載到的數據。

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

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

更多props詳見Query API

更新數據(POST)——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(突變,對應REST API的POST)需要一個字符串類型的參數:type。它將mutation包裝在gql中,並將其傳遞給Mutation組件prop
  • Mutation組件內需要一個匿名函數作爲子函數(也稱爲render prop函數),同Query組件,但參數有差異。
  • render prop函數的第一個參數是Mutation組件內部定義的mutate函數。爲了提高代碼可讀性,這裏取名爲addTodo

也可以直接用“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客戶端更新項目列表的查詢,
以獲取我們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

    • 如果指定樂觀響應,則會更新兩次更新函數:一次是樂觀結果,另一次是實際結果。
    • 您可以使用您的變異結果來使用cache.writeQuery更新緩存。

對於update函數,當你在其內部調用cache.writeQuery時,update會在Apollo內部調用廣播查詢(broadcastQueries),廣播查詢會觸發緩存內部和mutation相關的內容自動使用graphql進行查詢並更新UI。
因此當執行mutation後,我們不必手動去執行相關組件的查詢,Apollo Client在內部已經做好了所有工作,這區別於redux在dispatch後所做的一切處理數據的工作。

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

由於我們需要更新顯示TODOS列表的查詢,因此首先使用cache.readQuery從緩存中讀取數據。
然後,我們將mutation後得到的新todo與現有todo列表合併起來,並使用cache.writeQuery將查詢寫回緩存。
既然我們已經指定了一個update函數,那麼一旦新的todo從服務器返回,我們的用戶界面就會用它進行響應性更新。

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字段一起存在)。

如果未指定id_id,或者未指定__typename,則InMemoryCache將按照查詢到對象的層級關係依次回退到根查詢爲止,
例如ROOT_QUERY.allPeople.0將作爲這個對象的鍵被存儲到cache根下。(在展平的數據結構中,所有對象都在cache根下)

即使我們不打算在我們的UI中使用mutation返回的結果,我們仍然需要返回更新的ID和屬性,以便我們的UI進行自動更新。
這裏,我們不需要指定update函數,因爲TODOS查詢將使用緩存中更新的TODO數據自動重建查詢結果。

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

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

如果想要自定義唯一標識符,即不用默認的id來生成cache鍵,可以使用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數據存儲的理解又上升了一個臺階,把所有查詢回來的對象一一拆分,通過唯一標識符的形式把一個深層級的對象展平,直觀展現在cache的根存儲中。
  • 當學習了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是同樣的用法),
    例如將一個item添加到一個列表中或切換布爾值。

直接寫入緩存

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實例,
所以我們可以直接從render prop中拿到client實例。

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

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

@client 指令

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

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服務端定義的字段(對象)編寫graphql語句即可;
當加上@client指令後,Apollo Client不會向服務端發送請求,反而在自己內部尋找graphql內定義的字段(對象),但本地並不是graphql服務器,自然沒有服務端那些已經定義好的字段(對象)可供使用。
這時,我們就需要自己定義可以訪問這些字段(對象)的方式,這個方式便是在解析器(resolvers)中定義一個解析函數(resolver),以供graphql查詢或突變在使用了@client指令時調用,
這樣就建立了graphql查詢或突變與Apollo Client的聯繫,通過這個函數可以解析有@client指令控制的查詢或突變,因此這個函數被命名爲解析函數,意指從本地解析函數中尋找graphql字段的值。

但是這個解析函數(resolver)定義在哪裏呢?
其實這個裝着自定義解析函數的對象才叫resolvers(解析器),它在ApolloClient的構造函數中,也就是說我們實例化ApolloClient時,需要傳遞resolvers給它。

解析器示例:

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

obj {object}: 包含父字段上resolver函數返回的結果的對象,或者爲DOM樹最頂層的查詢或突變的ROOT_QUERY對象
args {object}: 包含傳遞到字段中的所有參數的對象。例如,如果使用updateNetworkStatus(isConnected:true)調用一個mutation,則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](https://www.apollographql.com/docs/react/essentials/local-state/#managing-the-cache)
  • 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 mutation):

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 mutation,它將我們想要切換的item的id作爲唯一的參數。我們通過使用@client指令標記GraphQLtoggleTodo字段來指示這是一個本地mutation。
這將告訴Apollo Client調用我們的本地mutation解析器裏面的toggleTodo解析函數來解析該字段。然後,我們創建一個Mutation組件,就像我們操作遠程mutation一樣。
最後,將GraphQL變異傳遞給組件,並在render prop函數的UI中觸發它。

查詢本地狀態

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

我們來看一個例子:

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爲初始值準備緩存。
初始狀態的結構應與您計劃在APP中查詢它的方式相匹配。

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

重置本地狀態/緩存

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 }));

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

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

使用@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 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庫提供的graphql方法和compose做到分離。
還有其他的輔助方法,請參考React Apollo API

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章