在 Nuxt.js 共用 GraphQL Fragments

最近公司打 API 的方式換成 GraphQL,這邊紀錄一下怎麼在 Nuxt.js 用 GraphQL 的 fragments 特性,來達到重複使用程式碼查詢(query)的效果。

Fragments 是什麼

fragments 是什麼?簡單來說,就是一個可重用的欄位(fields)片段,你可以在裡頭放好幾個查詢欄位,接著就可以拿這個片段放到不同的查詢物件中。

最簡單的範例長這樣:

query posts {
  rightPosts: allPosts {
    ...postFields
  }
  
  leftPosts: allPosts {
    ...postFields
  }
}

fragment postFields on Post {
  id
  title
  heroImage {
    title
    urlMobileSized
  }
}

以 JavaScript 來類比,你可以想像 postFields 就是物件,而 ...postFields 就像是把物件展開(spread)一樣,能將 postFields 內聯到 rightPostsleftPosts 物件中,所以最後 rightPosts 實際上是長這樣:

query posts {
  rightPosts: allPosts {
    id
    title
    heroImage {
      title
      urlMobileSized
    }
  }
}

情境闡述

這是我在專案碰到的情況:我需要在網站的不同頁面去取得最新文章,它們的查詢欄位大致相同,但仍有些欄位不是每個查詢都需要的。

最簡單的做法當然就是每個查詢都寫好自己的欄位。這個方法很快,但缺點就是重複,之後修改共同欄位需要一次改好幾個地方。

另一個方法是使用指令(directives),藉由變數(variables)來動態地更改欄位,比如這樣:

query posts($shouldQueryTitle: Boolean!) {
  allPosts {
    id
    title @include(if: $shouldQueryTitle)
  }
}

shouldQueryTitletrue,就會查詢 title 這個欄位,反之則不會。這個方法可行,但若需要客製化的欄位不少,實作起來會相當麻煩。

fragments 能怎麼解決這個問題呢?很簡單,看共用欄位有哪些,把它們抽取成 fragment,再用 ... 內聯。

這個方法還有一個好處,就是它能內聯嵌套查詢,而非在第一層就整個覆蓋。比方說,下面這塊程式碼,allPosts 有兩個 heroImage 欄位,但裡頭查詢的欄位並不同:

query posts {
  allPosts {
    ...postFields
    heroImage {      urlTabletSized
    }
  }
}

fragment postFields on Post {
  id
  title
  heroImage {    title
    urlMobileSized
  }
}

最後,heroImage 實際上是長這樣:

heroImage {
  title
  urlMobileSized
  urlTabletSized
}

這可以讓我們在享受共用欄位的同時,還能客製化出不同的查詢內容。很方便吧!

在多個檔案間共用 Fragments

如果你是在 Nuxt 中使用 GraphQL,可以參考 @nuxtjs/apollo 這個套件,它裡頭包著 vue-apollo

要創建一個查詢,可以利用 gql 標籤直接寫在 apollo 物件裡,也可以另開一個 .graphql.gql。我偏好後者,因為比較好管理。

回到 fragments。若是用檔案管理的方法,fragments 一般來說會放在使用它的檔案中。但如果我要在多個檔案間共用一個 fragment,那該怎麼做?

具體情境是這樣的:我有好幾個不同的查詢物件,它們都會去查詢 heroImageogImage,而這兩個欄位也都是物件,會去查詢 tiny、mobile、tablet、desktop、original 這五種尺寸的圖片。每次查詢都要寫這 5 * 2 個欄位,實在是有點麻煩——有沒有辦法把它們抽取成一個 fragment 呢?

在單一檔案中很簡單,就直接放在下面:

query latestPosts {
  latestPosts: allPosts {
    id
    title
    heroImage {
      ...urls
    }
    ogImage {
      ...urls
    }
  }
}

fragment urls on Image {
  urlTinySized
  urlMobileSized
  urlTabletSized
  urlDesktopSized
  urlOriginal
}

但若要在多個檔案間共用,可以把這個 fragment 移入一個獨立的檔案。假設檔案結構長這樣:

apollo
├─fragments
   └─image-urls.gql <-- `urls` fragment is here!
└─queries
   ├─editor-choices.gql
   ├─collaborations.gql
   └─posts.gql

要怎麼把 image-urls.gql 導入到查詢檔案中使用呢?你可能會想說直接 import,但電腦會噴錯,因為 import 是 JS 語法,GraphQL 無法辨識。

那該怎麼辦?經過我一番搜索,其實你可以這麼做:

#import '../fragments/image-urls.gql'
query latestPosts {
  latestPosts: allPosts {
    id
    title
    heroImage {
      ...urls
    }
    ogImage {
      ...urls
    }
  }
}

只要在 import 前加上 # 變成註解,就能成功把 fragments 導進來囉(我猜這應該是 graphql-loader 的功勞,但詳細怎麼做的就不清楚了)。