歡迎進入我的博客閱覽此文章。
本文檔諸多例子來源于官方文檔,但也在編寫過程中添加了我對這套技術棧的一些理解,如果你更喜歡看官方文檔,請移步官網/官方文檔
為什么要使用apollo?
沒有redux繁瑣的action、reducer、dispatch……讓全局管理store變得簡單、直白!使用
redux
管理狀態,重心是放在如何去拿數據上;而apollo
把重心放在需要什么數據上。理解這一點非常重要!
好了,廢話不多說,我們立即開始!
準備工作
創建React項目
- 你可以使用
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
- 也可以在codesandbox上在線搭建React項目。方便快捷!
搭建GraphQL服務
- 你可以在github上fork graphpack項目,然后使用
github賬號
登錄codesandbox
并導入該項目,即可零配置搭建一個在線的GraphQL服務。
本文檔在編寫時在codesandbox
上搭建了一個服務,可供參考:https://kdvmr.sse.codesandbox.io/ - 也可以在本地搭建自己的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-boost
和apollo-client
都提供了ApolloClient
,但是兩者需要的參數有一點差別。具體見各自API:
-
apollo-boost
導出的Apollo Client對象(詳細API):集成官方核心功能的一個大集合對象 -
apollo-client
導出的Apollo Client對象(詳細API):默認為App所在的同一主機
上的GraphQL端點
,
要自定義uri
還需引入apollo-link-http
包。如果你使用的是apollo v1.x
,可直接從apollo-client
包內導出createNetworkInterface
方法,用法請見(1.x遷移至2.x指南)[https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-http#upgrading-from-apollo-fetch--apollo-client]
我們看一下使用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組件內,有一個匿名函數
,這個匿名函數有一些參數,最常用的有: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
更新數據——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.readQuery
和cache.writeQuery
,它們允許您使用GraphQL讀取和寫入緩存。- 另外還有其他的方法,例如
cache.readFragment
,cache.writeFragment
和cache.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實例對象client
的client.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
字段一起存在)。imageimage如果未指定
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
字段的定義),
在query
或mutation
操作后,我們幾乎不用手動處理數據,就能實現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組件
中輕松地從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
文檔即可;
當加上@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的APIcontext.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突變):
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
指令標記GraphQL
的toggleTodo
字段來指示這是一個本地突變。
這將告訴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文檔的currentAuthorId
和postCount
字段之后都有@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將包含在自己的包中,這樣也有助于減少入口文件的大小,使用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
這個包中并不止這兩個方法,還有其他的方法,請參考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應用已經算是入門了,今后開發時遇到問題,多看一看官方文檔,相信你會很快掌握它。
由于水平有限,這篇文章是我自己一邊翻譯一邊加入自己的理解而寫成的,其中肯定少不了一些不妥或錯誤的地方,歡迎大家指正!