GraphQL 初初探:Relay,把資料接到介面上!
這篇總覺得沒寫完的系列文,是我後來「GraphQL & Relay - 串起前後端世界的橋樑」以及「Overview of GraphQL & Clients」兩份簡報的雛形。歡迎大家直接前往閱讀 XD
在上一篇的 GraphQL 極・簡介中,大概帶過了 GraphQL 的出生背景、想要解決的問題,以及用起來大概是什麼感覺。
但是「前後端資料交換」這件事光憑 GraphQL 好像只解決了一半,除了用 GraphQL 本身解決了後端 API 設計的問題(嗯,其實已經不錯了,例如在 app 發佈了很多版本後,就比較不用煩惱如何維護一堆 API 版本讓舊版 app 不會壞掉)之外,通常我們還會遇到諸如此類的麻煩事:
- 如何將資料分頁?
- 如何避免傳統用「頁」來分頁時,無限捲軸類的 UI 可能會因為在 fetch 第二頁時,server 上出現新的資料導致原本在第一頁的資料被「擠」到第二頁,結果顯示出了重複、不正常的資料?
- 如何快取住分頁後的資料?
- 如何符合效能考量地快取住互相有關聯的資料?
(例如如果 n 篇貼文的作者是同一個人,如何避免在前端快取同一個人的資料 n 次) - 如何在使用者更新資料時,一併更新所有在快取中的內容?
(例如在使用者閱讀訊息後,除了要取消訊息本身的 highlight,也要同時讓上方工具列的未讀訊息提示消失、或是使用者更新 username 後,同時換掉所有介面中的舊 username) - 如何做 optimistic update(使用者更新資料時,例如對貼文按讚,我們先讓 UI 更新假裝操作已經很迅速地完成,然後才在背後默默的向 server 發出更新的 request,萬一 request 失敗再將 UI 回復原狀)?
- 若 server 上的資料更新,如何即時推播到前端,並像第 5 點一樣同時更新所有介面?
不過,這些問題只要祭出基於 GraphQL 的殺手級資料交換框架 —— Relay,都可以獲得解決。
1~3 就是 Relay Cursor Connections 的課題、4 Relay Global Object Identification + Relay Store、5~6 Relay Mutation、7 Relay Subscription。
Facebook 在 2017 的 F8 開發者大會上發佈了 API 大幅改進的 Relay Modern (Relay v1.0),以下的討論會以 Relay Modern 為主。
認識 Relay
筆者認為可以把 Relay 這個名詞理解為兩個部分的統稱:
- 一份 GraphQL Specification,只要後端開出來的 GraphQL API 符合這份規範,我們就可以用 Relay 的方式無痛處理資料交換的問題。
- 一套(目前是 React/React Native)的前端框架,只要接上符合 Relay spec 的 GraphQL backend,上一段所述的問題都能獲得標準化的輕鬆解決方式。
用起來的感覺
在投身於研究 Relay 的細節之前,筆者想先分享一下 Relay 用起來的整體感受,也許會對考慮是否將 Relay 採用於專案有幫助:
- 如 Relay 所述,適合 data-driven app,亦即 app 的主要功能就是閱讀、發佈、更新各種資料,例如社交網站 Facebook、電商、論壇、線上學習、專案管理⋯⋯之類的 app,資料越複雜幫助越大。
- 用起來很像 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。 - 承第二點,有 Garbage Collection 機制,若 Relay Store 拓墣到太肥大,會自動回收掉當下沒有被任何 component subscribe 到的 data。
- 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。
- 承前一點,在 Mutation 中我們也可以輕鬆地用一樣的方式定義 optimistic update 如何更新 Relay Store,差別只在 optimistic update function 裡因為 request 還沒實際發送出去,當然也沒辦法取得 server response,所以就需要自己「仿造」出需要的資料,例如要送出新留言內容的留言。
- 承上,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,這個解法感覺輕鬆直觀許多。
- Relay Subscription 有點像 Relay Mutation,但相較於 Relay Mutation 是由使用者觸發資料更新,Relay Subscription 則是藉由 socket.io 之類的技術由 server 主動推播。Relay Subscription 的 spec 已經大致上確定了,但相關實作還沒有成熟,因此筆者對這塊的研究就相對缺乏了。
- 筆者覺得 GraphQL + Relay 走在正確的路上,而且是身為 web app 開發最強團隊之一的 Facebook 採用並推出的做法,但這條路目前還沒被大家踩寬踩平,如果對 JavaScript 生態圈以及 Relay 內部實作沒有足夠的了解,就容易陷在各種一般的地雷中,面對原生的 error 或不符合預期的行為花掉許多時間。
- 後端若使用 GraphQL Relay API 雖然會變得容易維護,但也產生出了新的後端問題需要解決,例如如何避免在動態查詢下動態解決 n+1 queries、SQL 不容易實作出 cursor based pagination、定義出 mutation、重新設計權限控管方式等。雖然若原本後端架構已經用 form objects、policy objects 整理良好並有相關測試程式,感覺會輕鬆不少。
用起來很像「接了 backend 並會自動更新資料」版的 Redux。目前最乾淨的 data-driven app 做法 。還很新所以容易踩雷。
Relay 各部簡介
那麼,以下就來簡介一下 Relay 的各個部分:
Relay
- Query Renerer
- Fragment Container
- Refetch Container
- Pagination Container
- Mutations
GraphQL Relay
- Relay Object Identification
- Relay Connection
- Relay Mutations
Workarounds
- 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)
前面不小心花太多時間長篇大論了來不及寫完,這裡的白話說明之後再補上 > <
參考資料
- GraphQL Relay Specification:閱讀這篇的先備知識除了 GraphQL 之外還有星際大戰 XDD
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.
- 待補