21 分鐘學 apollo-client 系列:修改本地 store 資料

tinkgu發表於2017-09-18

21 分鐘學 apollo-client 是一個系列,簡單暴力,包學包會。

搭建 Apollo client 端,整合 redux
使用 apollo-client 來獲取資料
修改本地的 apollo store 資料
提供定製方案

apollo store 儲存細節
寫入 store 的失敗原因分析和解決方案

修改本地 store 資料

之前我們已經知道,我們可以在請求結束之後,通過自動執行 fetchMoreupdateQuery 回撥,修改 apollo store。

那麼,如何在不觸發請求的情況下,主動修改 apollo store 呢?

也許你會說通過 redux 的方式,dispatch 一個 action,由 reducer 來處理,但因為 apollo store 的資料儲存方案,這會相當麻煩。
詳細原理請看這一小節 apollo store 儲存細節

read & write

Apollo 對此,提供了兩組命令式 api read/writeQueryread/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 就可以了,引數這麼傳來傳去相當麻煩,更不用說會寫太多重複的程式碼。
所以我們們可以寫一個 updateQueryupdateFragment 函式。

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 即可新增 clientprops 中。

相關文章