還在用 Redux,要不要試試 GraphQL 和 Apollo?
前段時間刷 Twitter 的時候看到大 V 紛紛提到 Apollo,預測它將在 2018 年崛起。正巧碰上有使用 GraphQL 的機會,在大概翻了下 Apollo 的文檔之後,我下定決心在新的前端項目里嘗試下拋開已經熟悉的 Redux,完全使用 Apollo 來寫數據層。一個月後的現在,我必須出來好好讚美下這位「太陽神」了。
GraphQL
轉眼已經 2018 年了,GraphQL 已不再是個新鮮的名詞了。15 年短暫的掀起一波討論之後,似乎也沒有聽到多少它的聲音了。然而 Github 在這幾年裡慢慢成熟,Github 也將新版 api 完全用 GraphQL 實現。在這裡我就不展開討論 GraphQL 的本身了,它讓前後端之間的數據獲取變得更加簡單。
Redux
提到前端數據管理,最先想到的就是 Redux,我想很多人都體驗過對 Redux 從陌生到熟悉的各個階段,大致應該是這樣的:
- 開始:Facebook 設計的 Flux 架構,很厲害的樣子,大家都在用那我也用吧
- 半年:數據管理變的清晰些,終於不用在組件里來回混亂的 setState 了
- 一年:我就是個 CRUD 工程師,寫個千篇一律的列表,表單頁用 redux 真是折騰,多些了多少代碼啊
- 一年半:看了 redux-action,redux-promise,dva, mirror ...,根據團隊的業務場景定製了最合適的中間件和插件。代碼又變的簡潔啦
- 兩年:該折騰的都折騰過了。有點累了,但是也離不開了。
為什麼累了呢?因為 Flux 的單向數據流對你來說已經不再新鮮了。大部分時候,store 里存放的都是從後端請求來的數據,對於它們而言,怎麼樣做 dispatch 和 reduce 其實並不是關鍵,反倒是怎麼設計 store 值得考慮。
當 Redux 遇上業務需求
讓我們直接以一個真實的場景作為例子吧:
這是一個很常見的評論列表,拿到需求後我們就開始寫我們的 <Comments />
組件了,在 Redux 的範式下,我們難免要按照這個邏輯來寫:
- 在
Comments
的didMount
里,dispatch
一個獲取數據的action
,在這個fetch action
內發送請求。為了做 loading,我們很可能要再 dispatch 一個 action 去通知 redux 我們發起了一個請求。 - 如果請求順利成功了,我們 dispatch 一個請求數據成功的 action,然後在 reducer 內處理並更新數據。
- 在
Comments
內我們收到了 props 傳來的數據,正式開始渲染
我們大量的工作花費在了如何獲取數據上。而我們面臨的挑戰又是什麼呢?看幾個產品經理們可能會提的需求
- 用戶創建或修改評論,要能立刻在列表中看到更新;
簡單,重新請求一遍整個列表介面就好了!一般而言確實足夠了,不過要求高的產品可能會要求你做」樂觀「更新來讓體驗更好。這也沒什麼問題,加個 reducer
就是。
- 當滑鼠 hover 在用戶頭像上的時候,要彈出用戶的詳細數據(個人簡介,聯繫方式...)
首先你會想,後端大哥能不能把這些欄位都幫我加在評論的介面數據里,他毫不猶豫的拒絕了你,拿出一個 commonUser 的介面讓你自己去調。細一想用戶數據量不小,評論里也有大量的相同用戶,不放在列表裡也確實合理。心一橫,乾脆把前端這裡的數據結構全部 normalize 化,按用戶 id 為 key 用哈希表來存放數據。也就一個下午,你得到了一個非常完美的解決方案。
面對這樣的場景,我們寫了太多的 命令式 代碼,我們一步步的描述了怎麼去獲取評論數據,在得到評論數據後再提取出所有的用戶 id,去重後再次請求獲取所有的用戶數據,等等。我們還需要考慮緩 normalize, 緩存,樂觀更新等等細節上的問題。而這些,恰恰是 redux 幫不了我們的。於是我們會基於 Redux 封裝更強大的庫和框架,但真正 focus 在數據獲取上的好像還真沒看到非常合適的。
Declarative(聲明式) vs Imperative(命令式)
那麼在 Apollo 的世界裡是什麼樣的呢?
import { graphql } from react-apollo;const CommentsQuery = gql` query Comments() { comments { id content creator { id name } } }`;export default graphql(CommentsQuery)(Comments);
我們使用了 graphql
(類比到 redux 中的 connect) 作為高階組件將一條 GraphQL 的查詢語句綁定到了 Comments 組件上,然後你所有的一切就準備就緒了。這麼簡單么?是的,我們不再需要描述怎麼在 didMount 里發送請求,怎麼處理請求來的數據。而是委託 Apollo 幫我們處理這些所有事情,它會稱職的幫我們在需要的時候發送請求獲取數據,然後將 data 映射到 Comments 的 props 中交給我們。
不止於此,當我們做更新操作的時候也會便捷許多。比如修改一條評論。我們定義一個 graphql 的 mutation 操作:
// ...const updateComment = gql` mutation UpdateComment($id: Int!, $content: String!) { UpdateComment(id: $id, content: $content) { id content gmtModified } }`;class Comments extends React.Component { // ... onUpdateComment(id, content) { this.props.updateComment(id, content); } // ...}export default graphql(updateComment)(graphql(CommentsQuery)(Comments));
當我們調用 updateComment 時,你就會神奇的發現,列表中的評論數據自動更新了。這是因為 apollo-client 把數據按照類型自動緩存在了 cache 中,GraphQL 節點返回的任何數據都會自動被用來更新緩存,在 UpdateComment 這個 mutation 中,我們定義了它的返回值,一條類型為 Comment 的新修改評論,並且指定了需要接受的欄位,content
和 gmtModified
。這樣,apollo-client 就會自動通過 id 和類型去更新緩存中的數據,從而重新渲染我們的列表。
再看看剩下的需求,我們需要在滑鼠停留在用戶頭像時展開用戶詳情。這個需求下我們不僅僅需要定義我們需要什麼數據,還會關心「怎麼」獲取數據(在 hover 頭像時發送請求)。Apollo 同樣為我們提供了 「命令式」 的支持。
class UserItem extends React.Component { // ... onHover() { const { client, id } = this.props; client.query({ query: UserQuery, variables: { id } }).then(data => { this.setState({ fullUserInfo: data }); }); }}export default withApollo(UserItem);
幸運的是這裡我們依然不需要自己考慮緩存的問題。得益於 Apollo 全局的數據緩存,當我們查詢過用戶 A 之後,再次查詢相同 id 的數據會直接命中緩存,apollo-client 會直接 resolve 緩存中的數據,並不發送請求。這時候問題來了,假設我就是想要每次都重新查詢呢?
client.query({ query: UserQuery, variables: { id }, fetchPolicy: cache-and-network});
Apollo 給我們提供了很多策略來自定義緩存邏輯,比如默認的 cache-first
(優先使用緩存),這裡的 cache-and-network
(先使用緩存,同時發請求更新),以及 cache-only
和 network-only
。
這些就是 GraphQL 和 Apollo 很吸引我的一些地方。當你開始從 GraphQL 的角度來思考,你更多的關心的是你的業務組件需要什麼數據,而不是怎麼一步步的獲得它。而剩下的大部分業務場景,都可以通過前端的數據類型推導和緩存自動解決掉。當然,篇幅有限,還有很多優雅的地方來不及提及,比如分頁,直接操作緩存達到樂觀更新,輪詢查詢,以及數據訂閱等等。如果有機會的話我們可以繼續深入探討。
REST 和其他本地狀態 ?
看到這裡,你可能會覺得 「GraphQL 很酷,Apollo 也很酷,但是我的後端是 REST,目前是與他們無緣了」。其實不然,從 Apollo Client 的 2.0 版本開始引入了 Apollo Link,理論上來說我們可以通過 GraphQL 從任何類型的數據源獲取數據。
「通過 GraphQL「 意味著我們可以使用書寫 GraphQL 的查詢語句來獲取無論是 rest api 或是 client state 中的數據,這樣 Apollo Client 可以替我們管理應用中所有的數據,包括緩存和數據拼接。
const MIXED_QUERY = gql` query UserInfo() { // graphql endpoint currentUser { id name } // client state browserInfo @client { platform } // rest api messages @rest(route: /user/messages) @type(type: [Message]) { title } }`;
在這樣一個 Query 查詢中,我們使用 GraphQL 的 directive 拼接了來自於 GraphQL,rest,client state 中的數據,將它們抽象在一起維護。與之類似的,我們還可以封裝相應的 mutation 實現。
尾巴
以上大概就是我這段時間使用 Apollo 和 GraphQL 的一些淺淺的實踐。雖然接觸的不深,但我可以感受到 Thinking in GraphQL 為前端帶來的更優雅的解決方式,和 Apollo Client 這樣一個完整的前端數據層解決方案的高效。我相信在 2018 年,它們會迎來更大的增長,甚至有代替 redux 成為通用數據管理方案的可能。
Apollo 相關的社區也比較活躍,在 dev-blog.apollodata.com 上也經常發表一些很有參考價值的文章,有興趣可以隨便看看~
推薦閱讀:
※2.2 webpack
※如何評價 Airbnb 發布的 React Sketch.app 工具?
※解讀GraphQL(一): WHAT & WHY
※React 16 中的異常處理
※React中的"state"和AngularJS中的"scope"有什麼相同點?