GraphQL 初初探:Relay,把資料接到介面上!

Pokai Chang
9 min readJul 29, 2017

--

這篇總覺得沒寫完的系列文,是我後來「GraphQL & Relay - 串起前後端世界的橋樑」以及「Overview of GraphQL & Clients」兩份簡報的雛形。歡迎大家直接前往閱讀 XD

在上一篇的 GraphQL 極・簡介中,大概帶過了 GraphQL 的出生背景、想要解決的問題,以及用起來大概是什麼感覺。

但是「前後端資料交換」這件事光憑 GraphQL 好像只解決了一半,除了用 GraphQL 本身解決了後端 API 設計的問題(嗯,其實已經不錯了,例如在 app 發佈了很多版本後,就比較不用煩惱如何維護一堆 API 版本讓舊版 app 不會壞掉)之外,通常我們還會遇到諸如此類的麻煩事:

  1. 如何將資料分頁?
  2. 如何避免傳統用「頁」來分頁時,無限捲軸類的 UI 可能會因為在 fetch 第二頁時,server 上出現新的資料導致原本在第一頁的資料被「擠」到第二頁,結果顯示出了重複、不正常的資料?
  3. 如何快取住分頁後的資料?
  4. 如何符合效能考量地快取住互相有關聯的資料?
    (例如如果 n 篇貼文的作者是同一個人,如何避免在前端快取同一個人的資料 n 次)
  5. 如何在使用者更新資料時,一併更新所有在快取中的內容?
    (例如在使用者閱讀訊息後,除了要取消訊息本身的 highlight,也要同時讓上方工具列的未讀訊息提示消失、或是使用者更新 username 後,同時換掉所有介面中的舊 username)
  6. 如何做 optimistic update(使用者更新資料時,例如對貼文按讚,我們先讓 UI 更新假裝操作已經很迅速地完成,然後才在背後默默的向 server 發出更新的 request,萬一 request 失敗再將 UI 回復原狀)?
  7. 若 server 上的資料更新,如何即時推播到前端,並像第 5 點一樣同時更新所有介面?

不過,這些問題只要祭出基於 GraphQL 的殺手級資料交換框架 —— Relay,都可以獲得解決。

1~3 就是 Relay Cursor Connections 的課題、4 Relay Global Object Identification + Relay Store、5~6 Relay Mutation、7 Relay Subscription。

對 Relay 的認識越多,會對 Relay 這個名字以及 logo 會覺得越貼切呢。除了 Relay 之外,相似的框架還有 GraphQL 社群自產自銷的 Apollo Client,和 Relay 相比,後者主要的差異除了把許多工作都移交給前端負責解決之外,也支援除了 React 之外的其他前端或 app 框架,不過筆者就還沒研究了。

Facebook 在 2017 的 F8 開發者大會上發佈了 API 大幅改進的 Relay Modern (Relay v1.0),以下的討論會以 Relay Modern 為主。

認識 Relay

筆者認為可以把 Relay 這個名詞理解為兩個部分的統稱:

  1. 一份 GraphQL Specification,只要後端開出來的 GraphQL API 符合這份規範,我們就可以用 Relay 的方式無痛處理資料交換的問題。
  2. 一套(目前是 React/React Native)的前端框架,只要接上符合 Relay spec 的 GraphQL backend,上一段所述的問題都能獲得標準化的輕鬆解決方式。

用起來的感覺

在投身於研究 Relay 的細節之前,筆者想先分享一下 Relay 用起來的整體感受,也許會對考慮是否將 Relay 採用於專案有幫助:

  1. 如 Relay 所述,適合 data-driven app,亦即 app 的主要功能就是閱讀、發佈、更新各種資料,例如社交網站 Facebook、電商、論壇、線上學習、專案管理⋯⋯之類的 app,資料越複雜幫助越大。
  2. 用起來很像 Redux,如果熟悉 Redux 大概就會有感覺,各個 Component 會向 store subscribe 資料,並在 store 中的資料更新後自動 re-render。不過 Relay Store 是「接了 backend 並會自動更新資料」版的 Redux,Container 中用相當於 mapStateToProps 的角色定義 data requirement,因為前端 Relay 也會被給一份 GraphQL Schema,只是差在沒有實際的資料,相當於沒有肉的骨架,Relay Store 在收到 Container subscribe 的 data requirement 之後,若發現 store 中沒有實際可以滿足該 requirement 的資料,會再送 GraphQL query 跟 server 要缺少的肉放進 store。
  3. 承第二點,有 Garbage Collection 機制,若 Relay Store 拓墣到太肥大,會自動回收掉當下沒有被任何 component subscribe 到的 data。
  4. Mutation 的機制中有點像 Redux 的 Reducer 的作法,在 mutation commit 下去並收到 server response 的時候,我們可以定義一個 function 來依照 response 來更新 store,例如把某個 node 更新,或在 connection 裡安插新的 edge/node,這些更動也會立即體現在 subscribe 了該資料的 component 上。雖然受限於 Relay Store 的資料結構設計,目前我們還無法用比較乾淨的 functional 寫法來設計這個 function,而需要用 OOP 的方式來操作更新 Relay Store。此外 Mutation 也讓我們可以用類似 form object 的模式來設計處理資料更新的 input。
  5. 承前一點,在 Mutation 中我們也可以輕鬆地用一樣的方式定義 optimistic update 如何更新 Relay Store,差別只在 optimistic update function 裡因為 request 還沒實際發送出去,當然也沒辦法取得 server response,所以就需要自己「仿造」出需要的資料,例如要送出新留言內容的留言。
  6. 承上,Relay 在處理「optimistic update function」跟「真・update function」的手法是不一樣的,「optimistic update function」並不會像「真・update function」真的把 Relay Store 內的資料更新掉,而是使用「optimistic update function」來基於 Relay Store 創造出一層「optimistic update layer」並把這層 layer 覆蓋在 Relay Store 上。這時 Component 們受到這層 layer 濾鏡的影響,收到的 props 和 render 出的內容就會「像是 data 已經被更新後一樣」。在獲得 server 的 response ,「真・update function」執行之後這層 layer 就會被抽掉,於是使用者就會看到真實的 update 結果,或是因為 request 失敗而重新看到原本未更新的資料。相較於以往設計 optimistic update 功能時要花費許多腦力思考如何 update 與 undo data,這個解法感覺輕鬆直觀許多。
  7. Relay Subscription 有點像 Relay Mutation,但相較於 Relay Mutation 是由使用者觸發資料更新,Relay Subscription 則是藉由 socket.io 之類的技術由 server 主動推播。Relay Subscription 的 spec 已經大致上確定了,但相關實作還沒有成熟,因此筆者對這塊的研究就相對缺乏了。
  8. 筆者覺得 GraphQL + Relay 走在正確的路上,而且是身為 web app 開發最強團隊之一的 Facebook 採用並推出的做法,但這條路目前還沒被大家踩寬踩平,如果對 JavaScript 生態圈以及 Relay 內部實作沒有足夠的了解,就容易陷在各種一般的地雷中,面對原生的 error 或不符合預期的行為花掉許多時間。
  9. 後端若使用 GraphQL Relay API 雖然會變得容易維護,但也產生出了新的後端問題需要解決,例如如何避免在動態查詢下動態解決 n+1 queries、SQL 不容易實作出 cursor based pagination、定義出 mutation、重新設計權限控管方式等。雖然若原本後端架構已經用 form objects、policy objects 整理良好並有相關測試程式,感覺會輕鬆不少。

用起來很像「接了 backend 並會自動更新資料」版的 Redux。目前最乾淨的 data-driven app 做法 。還很新所以容易踩雷。

Redux 的作者 Dan Abramov 曾經發推文說 Relay 可能會取代很多 Redux 的應用。

Relay 各部簡介

那麼,以下就來簡介一下 Relay 的各個部分:

Relay

  1. Query Renerer
  2. Fragment Container
  3. Refetch Container
  4. Pagination Container
  5. Mutations

GraphQL Relay

  1. Relay Object Identification
  2. Relay Connection
  3. Relay Mutations

Workarounds

  1. Re-exposing the root query object as a query field to avoid Relay problem as dealing with connections at query root (https://github.com/facebook/relay/issues/112)

前面不小心花太多時間長篇大論了來不及寫完,這裡的白話說明之後再補上 > <

參考資料

It is also assumed that the reader is already familiar with Star Wars; if not, the 1977 version of Star Wars is a good place to start, though the 1997 Special Edition will serve for the purposes of this document.

  • 待補

--

--

Pokai Chang

All-round developer. Tech stuff I studied & talked about: http://bit.ly/2DdA4wo . (Git, ReasonML, GraphQL, NixOS & stuff).