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

阿里南京技術專刊發表於2019-03-04

img
https://twitter.com/seldo/status/950794461235130368

題注:如果喜歡我們的文章別忘了點選關注阿里南京技術專刊呦~ 本文轉載自 阿里南京技術專刊-知乎,歡迎大牛小牛投遞阿里南京前端/後端開發等職位,詳見 阿里南京誠邀前端小夥伴加入~

前段時間刷 Twitter 的時候看到大 V 紛紛提到 Apollo,預測它將在 2018 年崛起。正巧碰上有使用 GraphQL 的機會,在大概翻了下 Apollo 的文件之後,我下定決心在新的前端專案裡嘗試下拋開已經熟悉的 Redux,完全使用 Apollo 來寫資料層。一個月後的現在,我必須出來好好讚美下這位“太陽神”了。

GraphQL

轉眼已經 2018 年了,GraphQL 已不再是個新鮮的名詞了。15 年短暫的掀起一波討論之後,似乎也沒有聽到多少它的聲音了。然而 Github 在這幾年裡慢慢成熟,Github 也將新版 api 完全用 GraphQL 實現。在這裡我就不展開討論 GraphQL 的本身了,它讓前後端之間的資料獲取變得更加簡單。

Redux

提到前端資料管理,最先想到的就是 Redux,我想很多人都體驗過對 Redux 從陌生到熟悉的各個階段,大致應該是這樣的:

img

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

為什麼累了呢?因為 Flux 的單向資料流對你來說已經不再新鮮了。大部分時候,store 裡存放的都是從後端請求來的資料,對於它們而言,怎麼樣做 dispatch 和 reduce 其實並不是關鍵,反倒是怎麼設計 store 值得考慮。

當 Redux 遇上業務需求

讓我們直接以一個真實的場景作為例子吧:

img

這是一個很常見的評論列表,拿到需求後我們就開始寫我們的 <Comments /> 元件了,在 Redux 的正規化下,我們難免要按照這個邏輯來寫:

  1. CommentsdidMount 裡,dispatch 一個獲取資料的 action,在這個 fetch action 內傳送請求。為了做 loading,我們很可能要再 dispatch 一個 action 去通知 redux 我們發起了一個請求。
  2. 如果請求順利成功了,我們 dispatch 一個請求資料成功的 action,然後在 reducer 內處理並更新資料。
  3. 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 中交給我們。

img

不止於此,當我們做更新操作的時候也會便捷許多。比如修改一條評論。我們定義一個 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 從任何型別的資料來源獲取資料。

img

“通過 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 上也經常發表一些很有參考價值的文章,有興趣可以隨便看看~

相關文章