- 原文地址:The future of state management
- 原文作者:Peggy Rayzis
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:yct21
當一個應用的規模逐漸擴張,其所包含的應用狀態一般也會變得更加複雜。作為開發者,我們可能既要協調從多個遠端伺服器傳送來的資料,也要管理好涉及 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 的資料流架構圖。
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,須要將一個包含 resolvers
、defaults
和 cache
欄位的 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
頻道。歡迎你來和我們一起構建下一代的狀態管理方法!
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。