21 分鐘學 apollo-client 是一個系列,簡單暴力,包學包會。
搭建 Apollo client 端,整合 redux
使用 apollo-client 來獲取資料
修改本地的 apollo store 資料
提供定製方案
修改本地 store 資料
之前我們已經知道,我們可以在請求結束之後,通過自動執行 fetchMore
的 updateQuery
回撥,修改 apollo store。
那麼,如何在不觸發請求的情況下,主動修改 apollo store 呢?
也許你會說通過 redux 的方式,dispatch 一個 action,由 reducer 來處理,但因為 apollo store 的資料儲存方案,這會相當麻煩。
詳細原理請看這一小節 apollo store 儲存細節
read & write
Apollo 對此,提供了兩組命令式 api read/writeQuery
和 read/writeFragment
詳見其文件:DataProxy
或者這篇中文文件:GraphQL 入門: Apollo Client 儲存API
讀完這兩篇文件,你大概就能掌握修改 apollo store 的技巧。
不過其中還是有不少值得注意的點,下面通過程式碼和註釋來體會:
import React, { PureComponent } from `react`;
import TodoQuery from `./TodoQuery`;
import TodoFragment form `./TodoFragment`;
import { withApollo, graphql } from `react-apollo`;
@graphql(TodoQuery)
@withApollo // 通過 props 讓元件可以訪問 client 例項
class TodoContainer extends PureComponent {
handleUpdateQuery = () => {
const client = this.props.client;
const variables = {
id: 5
};
const data = client.readQuery({
variables,
query: TodoQuery,
});
const nextData = parseNextData(data);
client.writeQuery({
variables,
query: TodoQuery,
data: nextData,
});
}
handleUpdateFragment = () => {
const client = this.props.client;
const data = client.readFragment({
id: `Todo:5`,
fragment: TodoFragment,
fragmentName: `todo`, // fragment 的名字,必填,否則可能會 read 失敗
});
const nextData = parseNextData(data);
client.writeFragment({
id: `Todo:5`,
fragment: TodoFragment,
fragmentName: `todo`,
data: nextData,
});
}
}
不過,還是需要注意,它們和 fetchMore 裡的 updateQuery 一樣,都存在靜默失敗和寫入限制。
如果你發現資料沒有被更新,嘗試看我給出的解讀和解毒方案: 寫入 store 的失敗原因分析和解決方案
你可能還注意到了 read/writeFragment 時,其 id 並不是簡單的 5
,而是 ${__typename}:5
。
這和 apollo store 儲存資料的方式有關,我在 apollo store 儲存細節 詳述了 apollo store 儲存資料的原理。
在此處,你只需要知道,這裡 id 的值應當與你在建立 apollo client 時設定的 dataIdFromObject
有關,如果沒有設定,預設為 ${__typename}:${data.id}
。
最好的方式是呼叫 client.dataIdFromObject
函式計算出 id
const { id, __typename } = data;
const id = client.dataIdFromObject({
id,
__typename,
});
簡化介面
不過你不覺得上面這種寫法相當麻煩嗎?
雖然先 read 再 write 比較原子化,但是考慮到大部分場景下我們只需要 update 就可以了,引數這麼傳來傳去相當麻煩,更不用說會寫太多重複的程式碼。
所以我們們可以寫一個 updateQuery
、updateFragment
函式。
enhancers.js
import client from `./client`;
function updateFragment(config) {
const { id: rawId, typename, fragment, fragmentName, variables, resolver } = config;
// 預設使用 fragmentName 作為 __typename
const __typename = typename || toUpperHeader(fragmentName);
const id = client.dataIdFromObject({
id: rawId,
__typename,
});
const data = client.readFragment({ id, fragment, fragmentName, variables });
const nextData = resolver(data);
client.writeFragment({
id,
fragment,
fragmentName,
variables,
data: nextData,
});
return nextData;
}
function updateQuery(config) {
const { variables, query, resolver } = config;
const data = client.readQuery({ variables, query });
const nextData = resolver(data);
client.writeQuery({
variables,
query,
data: nextData,
});
return nextData;
};
function toUpperHeader(s = ``) {
const [first = ``, ...rest] = s;
return [first.toUpperCase(), ...rest].join(``);
}
如此,我們可以這樣簡化之前的程式碼
import React, { PureComponent } from `react`;
import TodoQuery from `./TodoQuery`;
import TodoFragment form `./TodoFragment`;
import { withApollo, graphql } from `react-apollo`;
import { updateQuery, updateFragment } from `@/apollo/enhancers`;
@graphql(TodoQuery)
@withApollo // 通過 props 讓元件可以訪問 client 例項
class TodoContainer extends PureComponent {
handleUpdateQuery = () => {
return updateQuery({
variables: {
id: 5
},
query: TodoQuery,
resolver: data => parseNextData(data),
});
}
handleUpdateFragment = () => {
return updateFragment({
id: 5,
typename: `Todo`,
fragment: TodoFragment,
fragmentName: `todo`,
resolver: data => parseNextData(data);
});
}
}
其中,resolver
是一個資料處理函式,它接收 read 操作後的 data ,返回的 nextData 將用於 write 操作。
這種簡單的 update 場景其實是更常見的,且你仍然可以在 resolver 中進行 debug,可以說程式碼相當精簡了。
註冊為 client api
在 封裝修改 client 的 api 裡我們提到可以使用 enhancer 的方式為 client 新增介面,如果你懶得每次都 import 這兩個函式,那麼不妨將他們註冊為 client 的例項方法:
enhancers.js
const enhancers = [
updateFragmentFactory,
updateQueryFactory,
];
export default function applyEnhancers(client) {
// 更函式式的寫法是把 enhancers 也作為引數傳進來,但是我需要的 enhancer 比較少,做此精簡
return enhancers.reduce(
(result, enhancer) => enhancer(result),
client
);
}
// --- enhancers ---
function updateFragmentFactory(client) {
return function updateFragment(config) {
// ...
}
return client;
}
function updateQueryFactory(client) {
return function updateQuery(config) {
// ...
};
return client;
}
這樣你就可以直接從 client 例項中訪問這兩個函式了。
提示:在元件中只需
withApollo
即可新增client
到props
中。