透過 Colocated Fragments 在 React TypeScript 專案中管理 GraphQL Query

莫力全 Kyle Mo
Starbugs Weekly 星巴哥技術專欄
10 min readApr 21, 2022

最近到新公司後才開始接觸鼎鼎有名的 GraphQL,以前總聽到使用 GraphQL 後可以讓前端開發者在串接資料上更為方便,也可以解決一些 over fetching 或 under fetching 的問題,自己花了一定的時間從 0 開始學習後,的確體會到許多方便開發的點。另外在觀察 codebase 之後,發現自己對於採用 GraphQL 的應用下程式架構的管理蠻有興趣的,今天這篇文章就想簡單介紹一下其中一種管理 GraphQL Queries 的方式 — Colocated Fragments。

GraphQL Query 的組織與管理

熟悉 GraphQL 的前端開發者應該都對以下這種格式的 Query 不陌生,我們可以自己定義要抓取哪些 types 的哪些 fields,讓開發者可以非常彈性的抓取自己想要的資料。

然而隨著專案的成長,codebase 中漸漸會有越來越多因應不同需求的 query,這時候如何有系統的管理這些 queries 就變得非常重要了。一般來說你應該會看到以下兩種管理 query 的方式,就算有些差別也大多是這兩種方法的變形:

  1. 建立一個專門聚集各種 query 的地方(中心化管理): 可能是在 root folder 底下建立一個 graphql/queries的資料夾,裡面包含許多 xxx.ts的檔案,當你想修改或是查看 query 時就知道可以來這邊找。
  2. 將 query 放置在要 consume 那些 data 的 component 檔案裡。

本篇文章將聚焦在介紹第二種方式,並且搭配 Fragments 讓 Query 更好維護與重用。

什麼是 GraphQL Fragments ?

Fragments 是一段 reusable 的 GraphQL query,以 GraphQL 的官方文件為例子:

假設你的 query 有兩個屬性都需要抓取相同的 fields(上圖的 leftComparison 與 rightComparison,其實他們都是要抓取 hero 這個類別的資料),與其在兩個地方都寫上重複的 fields,我們可以撰寫一個 for 特定類別的 Fragment (以上圖為例,為 Character 這個 type 建立 comparisonFields 這個 Fragment),將共用的 fields 寫在裡面,再於要抓取的地方以類似 JS spread syntax (…) 的方式加入 Fragments,GraphQL 實際上就會幫我們展開這段 Fragment 到每一段 query 中,這就跟 JS 的 spread syntax 會幫我們展開物件的 key value pairs 是一樣的意思。

所以如果擅用 Fragment 的話其實可以發揮蠻強大的效果,讓程式碼更可讀更好維護也避免一直重複寫一樣的 query 。不過其實 GraphQL Fragments 搭配 React 的 Component 還能做到更多的事,接下來就來介紹本篇的主角 Colocated Fragments。

(如果還是不太懂 fragments 的讀者建議先去看看官方文件再接著往下閱讀喔!)

Colocated Fragments

首先來看看官方文件的說明:

簡單來說 Colocated Fragments 的概念跟上面提過的 Fragments 其實是一樣的,差別在於它是寫在需要使用這個 Fragment 欄位的 component 旁邊。

我們直接看範例會比較好理解,假設我們今天要實作像是 Medium 首頁這樣的文章列表。

那麼 components 的層級架構應該會類似這樣:

ArticleList
- ArticleCard
- AuthorSection
- .....(還有其他元件,這邊先忽略)

首先 <ArticleList /> 包含整個列表,<ArticleCard /> 則代表單一個文章卡片,<AuthorSection /> 則像上圖一樣負責顯示作者的大頭貼與姓名。(其他的元件我們先忽略不看)

我們從層級比較低的元件 <AuthorSection /> 開始看起:

首先我們建立一個叫做 AuthorSection 的 Fragment (naming convention 為與 component 同名,不過這邊沒有強制),並且指定給 User type。再來是將這段 Fragment 放到 AuthorSection.fragments 這個 property 當中(這裡的命名依然是 convention,並沒有強制一定要放到 fragments 這個屬性裡)。

可以注意到 <AuthorSection /> 這個 component 會吃進一個叫 author 的 prop,這個 author prop 的欄位跟我們在 fragment 裡寫的是一致的。

要特別注意的是,在這個 component 中並沒有發出任何的 GraphQL query,僅僅是將這個 component 會用到的資料寫成 fragment 而已,到時候會由上層元素使用這個 child components 裡定義的 fragment 並負責發出 query,取得資料後再傳遞到這個 child component 裡,就像我們平常熟悉的寫法一樣。

我們接著再往上一個層級看看 <ArticleCard /> 這個元件。

這個 component 也不是負責發出 query 的地方,它做的事跟剛剛的 <AuthorSection /> 很像 — 在 component 旁定義 fragments。

不同的是它使用了他的子元件 <AuthorSection /> 所定義的 fragment 來組合成自己的 fragment。

最底下的 ${AuthorSection.fragments.author} 其實就是呼應到 <AuthorSection /> 中的這段

等於把 gql parse 後的 fragment query 載入到 <ArticleCard /> 裡。

…AuthorSection 則是將該 Fragment 展開到 ArticleCard 的 fragment 中(由小 fragment 組合成大的 fragment 的概念),到這裡 ArticleCard 的 fragment 也完成了。

最後我們來看最上層且真實負責發出 query 的元件 <ArticleList />。

這個 component 是實際上透過 useQuery 發出 network request 抓取資料再把資料往下傳的 component,可以看到 ARTICLE_LIST_QUERY 這個 gql 語法也是寫在 component 旁邊,是的,不只 fragment,query 也可以使用這種 colocated 的方式來管理,而這個 query 跟剛剛一樣使用了下層子元件的 fragment 來組合出 fields,因此它實際上包含了 ArticleCard 與 AuthorSection 的 fragment fields,如此一來就做到在實際要發出 query 的 parent component 旁建立出 query, 同時也讓子元件需要的資料是依附在它們自己身邊的。

graphql-code-generator

上面的例子都是使用單純的 JavaScript,然而現今的前端專案 TypeScript 有慢慢變成標配的趨勢。使用 TypeScript 時,我們會想要為從 API 拿回來的資料做上型別註記,難不成我們需要自己觀察 API 回傳資料的格式再一一寫成 interface 或者 type 嗎…?要這麼做當然可以,只不過這個過程非常累,況且當 API 回傳的格式更改了,你還要回過頭去更改那些型別註記,實在是缺乏彈性也不好維護。通常我們會搭配 graphql-code-generator 這樣的工具去根據專案中撰寫的 GraphQL queries 自動生成 type definitions,這樣就可以直接拿來使用,當 API 格式有變動時也只要重新呼叫一次 codegen 產生新的 type 定義就好,程式碼基本上不需要做任何變動。

例如說我們可以在設定檔告訴 graphql-code-generator 去名為 graphql 的資料夾底下去掃描我們寫過的 query,最後就會產生 type 定義檔在 generated/graphql.tsx 這個檔案讓我們做使用。

不過我沒事說這個幹嘛?跟 Colocated Fragments 有什麼關聯嗎?

上面的例子其實就是中心化管理 GraphQL query 的方式,我會提出來是因為就算使用 colocated fragments,我們依然可以靠 code generator 為我們建立型別,甚至 fragment 本身也可以建立出型別讓 component 使用。

因為 fragment 或是 query 散落在各個 component,所以我們需要對 graphql-code-generator 的設定檔做一些更改。

讓 graphql-code-generator 知道要去 src folder 底下的 ts/tsx 檔案尋找 query 並生出對應的型別。

值得一提的是它也會為 fragment 建立一個型別,因為剛剛我們在撰寫 component 的 props 的時候早就把 fragment 與 props 的屬性設為一致,所以可以直接使用 fragment 的 type。

當然 graphql-code-generator 還有很多強大的 plugins 可以使用,例如可以自動生成 custom react hooks 等等功能,有興趣的讀者就自行研究囉!

小結 — Why Colocated Fragments ?

其實 Colocated Fragments 好不好沒有一個絕對的答案,還是要看專案的需求來決定。至少以我自己來說還蠻喜歡這樣的開發架構的,因為我覺得它有達到「高內聚,低耦合」的特性,讓一個 React component 的邏輯、樣式、渲染與資料都可以寫在同一個地方,維護起來會比較方便。再者不同 component 間的依賴性也會減低,假設今天上圖的 AuthorSection component 需要多一個資料欄位 birthdate,我們只需要在該 component 的 fragment 多加一個 birthdate 的 field 就好,不需要去更改其他 component 的程式碼,因為上層的資料也是由下層的 fragment 組合而成的。

當然再重申一次架構這種事我認為只有最適合而沒有最好的解法,今天這篇文的目的只是想讓大家認識 Colocated Fragments 這種管理 GraphQL queries 的架構,如果有其他想法也歡迎提出啦!

--

--

莫力全 Kyle Mo
Starbugs Weekly 星巴哥技術專欄

什麼都想學的雜食性軟體工程師 🇹🇼 (https://github.com/kylemocode) 合作與聯繫 📪 oldmo860617@gmail.com IG 技術自媒體:@kylemo.webdev.life