本文檔諸多例子來源於官方文檔,但也在編寫過程中添加了我對這套技術棧的一些理解,如果你更喜歡看官方文檔,請移步官網/官方文檔
爲什麼要使用apollo?
沒有redux繁瑣的action、reducer、dispatch……,讓全局管理store變得簡單、直白!使用
redux
管理狀態,重心是放在如何去拿數據上;而apollo
把重心放在需要什麼數據上。理解這一點非常重要!
好了,廢話不多說,我們立即開始!
準備工作
創建React項目
- 本篇文章的隨堂練習可以使用
create-react-app
快速創建一個React應用,不熟悉create-react-app的小夥伴可以先行了解。 - 也可以在codesandbox上在線搭建React項目。
npm i create-react-app -g
create-react-app react-apollo-client-demo --typescript
cd react-apollo-client-demo
npm start
搭建graphql服務
- 服務端可以在github上forkgraphpack項目,然後在codesandbox裏面導入該項目即可零配置搭建一個在線的graphql服務。
- 你也可以在本地搭建自己的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-boost
和apollo-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.readQuery
和cache.writeQuery
,它們允許您使用GraphQL讀取和寫入緩存。- 另外還有其他的方法,例如
cache.readFragment
,cache.writeFragment
和cache.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實例。
直接寫入緩存不需要GraphQLmutate
或resolvers
函數。因此我們在上面的代碼中沒有看見使用它們,直接在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
: 使用__typename
和id
從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
並傳入項目的__typename
和id
來獲取緩存鍵。
一旦我們讀取了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
指令標記GraphQL
的toggleTodo
字段來指示這是一個本地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將包含在自己的包中,使用addResolvers
和 setResolvers
即可辦到(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