還在用 Redux,要不要試試 GraphQL 和 Apollo?

還在用 Redux,要不要試試 GraphQL & Apollo?

clipboard.png

前段時間刷 Twitter 的時候看到大 V 紛紛提到 Apollo,預測它將在 2018 年崛起。正巧碰上有使用 GraphQL 的機會,在大概翻了下 Apollo 的文檔以後,我下定決心在新的前端項目裏嘗試下拋開已經熟悉的 Redux,徹底使用 Apollo 來寫數據層。一個月後的如今,我必須出來好好讚美下這位「太陽神」了。javascript

GraphQL

轉眼已經 2018 年了,GraphQL 已再也不是個新鮮的名詞了。15 年短暫的掀起一波討論以後,彷佛也沒有聽到多少它的聲音了。然而 Github 在這幾年裏慢慢成熟,Github 也將新版 api 徹底用 GraphQL 實現。在這裏我就不展開討論 GraphQL 的自己了,它讓先後端之間的數據獲取變得更加簡單。前端

Redux

提到前端數據管理,最早想到的就是 Redux,我想不少人都體驗過對 Redux 從陌生到熟悉的各個階段,大體應該是這樣的:java

clipboard.png

  • 開始:Facebook 設計的 Flux 架構,很厲害的樣子,你們都在用那我也用吧
  • 半年:數據管理變的清晰些,終於不用在組件裏來回混亂的 setState 了
  • 一年:我就是個 CRUD 工程師,寫個千篇一概的列表,表單頁用 redux 真是折騰,多些了多少代碼啊
  • 一年半:看了 redux-action,redux-promise,dva, mirror ...,根據團隊的業務場景定製了最合適的中間件和插件。代碼又變的簡潔啦
  • 兩年:該折騰的都折騰過了。有點累了,可是也離不開了。

爲何累了呢?由於 Flux 的單向數據流對你來講已經再也不新鮮了。大部分時候,store 裏存放的都是從後端請求來的數據,對於它們而言,怎麼樣作 dispatch 和 reduce 其實並非關鍵,反卻是怎麼設計 store 值得考慮。react

當 Redux 趕上業務需求

讓咱們直接以一個真實的場景做爲例子吧:es6

clipboard.png

這是一個很常見的評論列表,拿到需求後咱們就開始寫咱們的 <Comments /> 組件了,在 Redux 的範式下,咱們不免要按照這個邏輯來寫:redux

  1. CommentsdidMount 裏,dispatch 一個獲取數據的 action,在這個 fetch action 內發送請求。爲了作 loading,咱們極可能要再 dispatch 一個 action 去通知 redux 咱們發起了一個請求。
  2. 若是請求順利成功了,咱們 dispatch 一個請求數據成功的 action,而後在 reducer 內處理並更新數據。
  3. Comments 內咱們收到了 props 傳來的數據,正式開始渲染

咱們大量的工做花費在瞭如何獲取數據上。而咱們面臨的挑戰又是什麼呢?看幾個產品經理們可能會提的需求後端

  1. 用戶建立或修改評論,要能馬上在列表中看到更新;

簡單,從新請求一遍整個列表接口就行了!通常而言確實足夠了,不過要求高的產品可能會要求你作」樂觀「更新來讓體驗更好。這也沒什麼問題,加個 reducer 就是。api

  1. 當鼠標 hover 在用戶頭像上的時候,要彈出用戶的詳細數據(我的簡介,聯繫方式...)

首先你會想,後端大哥能不能把這些字段都幫我加在評論的接口數據裏,他堅決果斷的拒絕了你,拿出一個 commonUser 的接口讓你本身去調。細一想用戶數據量不小,評論裏也有大量的相同用戶,不放在列表裏也確實合理。心一橫,乾脆把前端這裏的數據結構所有 normalize 化,按用戶 id 爲 key 用哈希表來存放數據。也就一個下午,你獲得了一個很是完美的解決方案。promise

面對這樣的場景,咱們寫了太多的 命令式 代碼,咱們一步步的描述了怎麼去獲取評論數據,在獲得評論數據後再提取出全部的用戶 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 中交給咱們。

clipboard.png

不止於此,當咱們作更新操做的時候也會便捷許多。好比修改一條評論。咱們定義一個 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 的新修改評論,而且指定了須要接受的字段,contentgmtModified。這樣,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-onlynetwork-only

這些就是 GraphQL 和 Apollo 很吸引個人一些地方。當你開始從 GraphQL 的角度來思考,你更多的關心的是你的業務組件須要什麼數據,而不是怎麼一步步的得到它。而剩下的大部分業務場景,均可以經過前端的數據類型推導和緩存自動解決掉。固然,篇幅有限,還有不少優雅的地方來不及說起,好比分頁,直接操做緩存達到樂觀更新,輪詢查詢,以及數據訂閱等等。若是有機會的話咱們能夠繼續深刻探討。

REST 和其餘本地狀態 ?

看到這裏,你可能會以爲 「GraphQL 很酷,Apollo 也很酷,可是個人後端是 REST,目前是與他們無緣了」。其實否則,從 Apollo Client 的 2.0 版本開始引入了 Apollo Link,理論上來講咱們能夠經過 GraphQL 從任何類型的數據源獲取數據。

clipboard.png

「經過 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 上也常常發表一些頗有參考價值的文章,有興趣能夠隨便看看~