[譯] 狀態管理的將來:在 Apollo Client 中使用 apollo-link-state 管理本地數據

當一個應用的規模逐漸擴張,其所包含的應用狀態通常也會變得更加複雜。做爲開發者,咱們可能既要協調從多個遠端服務器發送來的數據,也要管理好涉及 UI 交互的本地數據。咱們須要以一種合適的方法存儲這些數據,讓應用中的組件能夠簡潔地獲取這些數據。html

許多開發者告訴過咱們,使用 Apollo Client 能夠很好地管理遠端數據,這部分數據通常會佔到總數據量的 80% 左右。那麼剩下的 20% 的本地數據(例如全局標誌、設備 API 返回的結果等)應該怎樣處理呢?前端

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

解決問題的基礎

咱們知道這個問題須要解決,如今讓咱們思考一下,如何正確地在 Apollo Client 中管理狀態?首先,讓咱們回顧一下咱們喜歡 Redux 的地方,好比它的開發工具,以及將組件與應用狀態綁定的 connect 函數。咱們同時還要考慮使用 Redux 的痛點,例如繁瑣的樣板代碼,又好比在使用 Redux 的過程當中,有許多核心的需求,包括異步的 action creator,或者是狀態緩存的實現,再或者是積極界面策略的採用,每每都須要咱們親自去實現。ios

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

以上是 Apollo Client 的數據流架構圖。github

GraphQL:一旦學會,隨處可用

關於 GraphQL 有一個常見的誤區:GraphQL 的實施依賴於服務器端某種特定的實現。事實上,GraphQL 具備很強的靈活性。GraphQL 並不在意請求是要發送給一個 gRPC 服務器,或是 REST 端點,又或是客戶端緩存。GraphQL 是一門針對數據的通用語言,與數據的來源毫無關聯。後端

而這也就是爲什麼 GraphQL 中的 query 與 mutation 能夠完美地描述應用狀態的情況。咱們可使用 GraphQL mutation 來表述應用狀態的變化過程,而不是去發送某個 action。在查詢應用狀態時,GraphQL query 也能以一種聲明式的方式描述出組件所須要的數據。緩存

GraphQL 最大的一個優點在於,當給 GraphQL 語句中的字段加上合適的 GraphQL 指令後,單條 query 就能夠從多個數據源中獲取數據,不管本地仍是遠端。讓咱們來看看具體的方法。bash

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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索