[譯] 狀態管理的未來:在 Apollo Client 中使用 apollo-link-state 管理本地資料

LeviDing發表於2018-02-01

當一個應用的規模逐漸擴張,其所包含的應用狀態一般也會變得更加複雜。作為開發者,我們可能既要協調從多個遠端伺服器傳送來的資料,也要管理好涉及 UI 互動的本地資料。我們需要以一種合適的方法儲存這些資料,讓應用中的元件可以簡潔地獲取這些資料。

許多開發者告訴過我們,使用 Apollo Client 可以很好地管理遠端資料,這部分資料一般會佔到總資料量的 80% 左右。那麼剩下的 20% 的本地資料(例如全域性標誌、裝置 API 返回的結果等)應該怎樣處理呢?

過去,Apollo 的使用者通常會使用一個單獨的 Redux/Mobx store 來管理這部分本地的資料。在 Apollo Client 1.0 時期,這是一個可行的方案。但當 Apollo Client 進入 2.0 版本,不再依賴於 Redux,如何去同步本地和遠端的資料,變得比原來更加棘手。我們收到了許多使用者的反饋,希望能有一種方案,可以將完整的應用狀態封裝在 Apollo Client 中,從而實現單一的資料來源 (single source of truth)

解決問題的基礎

我們知道這個問題需要解決,現在讓我們思考一下,如何正確地在 Apollo Client 中管理狀態?首先,讓我們回顧一下我們喜歡 Redux 的地方,比如它的開發工具,以及將元件與應用狀態繫結的 connect 函式。我們同時還要考慮使用 Redux 的痛點,例如繁瑣的樣板程式碼,又比如在使用 Redux 的過程中,有許多核心的需求,包括非同步的 action creator,或者是狀態快取的實現,再或者是積極介面策略的採用,往往都需要我們親自去實現。

要實現一個理想的狀態管理方案,我們應當對 Redux 取長棄短。此外,GraphQL 有能力將對多個資料來源的請求整合在單次查詢中,在此我們將充分利用這個特性。

[譯] 狀態管理的未來:在 Apollo Client 中使用 apollo-link-state 管理本地資料

以上是 Apollo Client 的資料流架構圖。

GraphQL:一旦學會,隨處可用

關於 GraphQL 有一個常見的誤區:GraphQL 的實施依賴於伺服器端某種特定的實現。事實上,GraphQL 具有很強的靈活性。GraphQL 並不在乎請求是要傳送給一個 gRPC 伺服器,或是 REST 端點,又或是客戶端快取。GraphQL 是一門針對資料的通用語言,與資料的來源毫無關聯。

而這也就是為何 GraphQL 中的 query 與 mutation 可以完美地描述應用狀態的狀況。我們可以使用 GraphQL mutation 來表述應用狀態的變化過程,而不是去傳送某個 action。在查詢應用狀態時,GraphQL query 也能以一種宣告式的方式描述出元件所需要的資料。

GraphQL 最大的一個優勢在於,當給 GraphQL 語句中的欄位加上合適的 GraphQL 指令後,單條 query 就可以從多個資料來源中獲取資料,無論本地還是遠端。讓我們來看看具體的方法。

Apollo Client 中的狀態管理

Apollo Link 是 Apollo 的模組化網路棧,可以用於在某個 GraphQL 請求的生命週期的任意階段插入鉤子程式碼。Apollo Link 使得在 Apollo Client 中管理本地的資料成為可能,從一個 GraphQL 伺服器中獲取資料,可以使用 HttpLink,而從 Apollo 的快取中請求資料,則需要使用一個新的 link: apollo-link-state

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink } from 'apollo-link';
import { withClientState } from 'apollo-link-state';
import { HttpLink } from 'apollo-link-http';

import { defaults, resolvers } from './resolvers/todos';

const cache = new InMemoryCache();

const stateLink = withClientState({ resolvers, cache, defaults });

const client = new ApolloClient({
  cache,
  link: ApolloLink.from([stateLink, new HttpLink()]),
});
複製程式碼

以上程式碼是使用 apollo-link-state 初始化 Apollo Client。

要初始化一個 state link,須要將一個包含 resolversdefaultscache 欄位的 object 作為引數,呼叫 Apollo Link 中的 withClientState 函式。然後將這個 state link 加入 Apollo Client 的 link 鏈中。該 state link 應該放在 HttpLink 之前,這樣本地的 query 和 mutation 會在發向伺服器前被攔截。

Defaults

前文的 defaults 欄位是一個用於表示狀態初始值的 object,當 state link 剛建立時,這個預設值會被寫入 Apollo Client 的快取。儘管不是必需的引數,不過預熱快取是一個很重要的步驟,傳入的 default 使得元件不會因為查詢不到資料而出錯。

export const defaults = {
  visibilityFilter: 'SHOW_ALL',
  todos: [],
};
複製程式碼

以上程式碼的 defaults 代表了 Apollo cache 的初始值。

Resolvers

在使用 Apollo Client 管理應用狀態後,Apollo cache 成為了應用的單一資料來源,包括了本地和遠端的資料。那麼我們應當如何查詢和更新快取中的資料呢?這便是 Resolver 發揮作用的地方了。如果你以前在伺服器端使用過 graphql-tools,那麼你會發現兩者的 resolver 的型別簽名是一樣的。

fieldName: (obj, args, context, info) => result;
複製程式碼

如果你沒見過以上這段型別簽名,不要緊張,只需記住重要的兩點:query 或者 mutation 的變數通過 args 引數傳遞給 resolver;Apollo cache 會作為 context 引數的一部分傳遞給 resolver。

export const defaults = { // same as before }

export const resolvers = {
  Mutation: {
    visibilityFilter: (_, { filter }, { cache }) => {
      cache.writeData({ data: { visibilityFilter: filter } });
      return null;
    },
    addTodo: (_, { text }, { cache }) => {
      const query = gql`
        query GetTodos {
          todos @client {
            id
            text
            completed
          }
        }
      `;
      const previous = cache.readQuery({ query });
      const newTodo = {
        id: nextTodoId++,
        text,
        completed: false,
        __typename: 'TodoItem',
      };
      const data = {
        todos: previous.todos.concat([newTodo]),
      };
      cache.writeData({ data });
      return newTodo;
    },
  }
}
複製程式碼

以上的 Resolver 函式是查詢和更新 Apollo cache 的方法。

若要在 Apollo cache 的根上寫入資料,可以呼叫 cache.writeData 方法並傳入相應的資料。有時候我們需要寫入的資料依賴於 Apollo cache 中原有的資料,例如上面的 addTodo 方法。在這種情況下,可以在寫入之前先用 cache.readQuery 查詢一遍資料。若要給一個已經存在的 object 寫一個 fragment,可以傳入一個可選引數 id,這個引數是相應 object 的 cache 索引。上文我們使用了 InMemoryCache,因此索引的形式應當是 __typename:id

apollo-link-state 支援非同步的 resolver 方法,可以用於執行一些非同步的副作用過程,比如訪問一些裝置的 API。然而,我們不建議在 resolver 中對 REST 端點發請求。正確的方法是使用 [apollo-link-rest](https://github.com/apollographql/apollo-link-rest),這個包裡包含有 @rest 指令。

@client 指令

當應用的 UI 觸發了一個 mutation 之後,Apollo 的網路棧需要知道要更新的資料存在於客戶端還是伺服器端。apollo-link-state 使用 @client 指令來標記只需存在於客戶端本地的欄位,然後,apollo-link-state 會在這些欄位上呼叫相應的 resolver 方法。

const SET_VISIBILITY = gql`
  mutation SetFilter($filter: String!) {
    visibilityFilter(filter: $filter) @client
  }
`;

const setVisibilityFilter = graphql(SET_VISIBILITY, {
  props: ({ mutate, ownProps }) => ({
    onClick: () => mutate({ variables: { filter: ownProps.filter } }),
  }),
});
複製程式碼

以上這段程式碼通過 @client 指令將資料修改限制在本地。

Query 的形式和 mutation 類似。如果在 query 中使用了非同步的查詢,Apollo Client 會為你追蹤資料載入和出錯的狀態。如果使用的是 React,可以在元件的 this.props.data 中找到相應的資料,裡面還會有很多輔助方法,例如重發請求、分頁以及輪詢等功能。

GraphQL 的一個很讓人激動的功能是在單個 query 中向多個資料來源請求資料。在下面的例子中,我們在同一條 query 內查詢了 GraphQL 伺服器中儲存的 user 資料以及 Apollo cache 中的 visibilityFilter 資料。

const GET_USERS_ACTIVE_TODOS = gql`
  {
    visibilityFilter @client
    user(id: 1) {
      name
      address
    }
  }
`;

const withActiveState = graphql(GET_USERS_ACTIVE_TODOS, {
  props: ({ ownProps, data }) => ({
    active: ownProps.filter === data.visibilityFilter,
    data,
  }),
});
複製程式碼

以上程式碼使用 @client 指令查詢 Apollo cache。

在我們 最新的文件頁中,可以找到更多的例子,以及一些將 apollo-link-state 整合在應用中的小貼士。

1.0 版本前的路線圖

儘管 apollo-link-state 的開發已足夠穩定,可以投入實際應用的開發了,但仍有一些特性我們希望能儘快實現:

  • 客戶端資料模式:當前,我們還不支援對客戶端資料模式結構的型別校驗,這是因為,如果要將用於執行時構建和校驗資料模式的 graphql-js 模組放入依賴中,會顯著增大網站資原始檔的大小。為了避免這點,我們希望能將資料模式的構建轉移到專案的構建階段,從而達到對型別校驗的支援,並也可以用到 GraphiQL 中的各種很酷的功能。
  • 輔助元件:我們的目標是讓 Apollo 的狀態管理儘可能地與應用無縫連線。我們會寫一些 React 元件,使得某些常見需求的實現不再繁瑣,譬如在程式碼層面上允許直接將程式中的變數作為引數傳遞給某個 mutation 當中,然後在內部直接以 mutation 的方式實現。

如果你對上述問題感興趣,可以在 GitHub 上加入我們的開發和討論,或者進入 Apollo Slack 的 #local-state 頻道。歡迎你來和我們一起構建下一代的狀態管理方法!


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章