iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 7
2
Modern Web

Think in GraphQL系列 第 7

GraphQL 入門: Arguments, Aliases, Fragment 讓 Query 更好用 (進階 Query)

header

今天的內容可以用這張圖來表示:

Pasted image

可以看到內容涵括 Operation Name, Aliases, Fragment, Arguments, Variables 5 個 query 技巧,今天不只教 Client 端的 Query 也教你如何實作 Server 的 Schema 與 Resolver。

在開始前依然做個觀念釐清, Operation Name, Aliases, Fragment 三個純為 query 技巧 ; 而 Arguments 與 Variables 兩個 query 技巧則需要 Schema 設計的參與

不過因為 Schema 若是太簡單也沒有用到 進階 query 的需要,因此我先從較複雜的 Arguments & Variables 開始講起:

Arguments 多個願望一次滿足

在 RESTful API,你需要透過 query parameter 或是 URL segments 傳遞參數來獲取不同的資料,並且常常需要很多支 API 才能獲得想要的資料,更別提一堆傳入的參數要如何管理。

在 GraphQL 為了能夠優雅地達到「一次解決」的特性,允許每一個 field 都可以傳入參數,即使是 Scalar field

Arguments 範例

先看看在 query 中如何使用 Arguments ,我將會使用新增的 Query field user (功能為得到特定 user) 做舉例:

Arguments 範例 - Client Side

  • GraphQL Query
query {
  # 傳入 Argument "Fong" (Argument for Object Type)
  user(name: "Fong") {
    id
    name
    # 傳入 Argument METRE (Argument for Scalar Type),
    # 此 field 回傳 FLOAT type
    height(unit: FOOT)
    # 傳入 Argument POUND (Argument for Scalar Type),
    # 此 field 回傳 FLOAT type
    weight(unit: POUND)
  }
}
  • Response
{
  "data": {
    "user": {
      "id": 1,
      "name": "Fong",
      "height": 5.741469816272966,
      "weight": 154.3235835294143
    }
  }
}

也可以測試看看如果 height, weight 不帶參數會有什麼結果,可以到之前的範例(傳送門)練習看,或是直接點開圖

query 是不是很簡單 ? 那就讓我們來講解如何實現吧:

Server Side

  • GraphQL Schema
# Enum Type 為一種特殊的 Scalar Type ,使用時只能出現裡面有定義到的值且不需要加引號
# 進入 JavaSript 中使用時,會轉為 String 格式
"""
高度單位
"""
enum HeightUnit {
  "公尺"
  METRE
  "公分"
  CENTIMETRE
  "英尺 (1 英尺 = 30.48 公分)"
  FOOT
}

"""
重量單位
"""
enum WeightUnit {
  "公斤"
  KILOGRAM
  "公克"
  GRAM
  "磅 (1 磅 = 0.45359237 公斤)"
  POUND
}

type User {
  ...
  "身高 (預設為 CENTIMETRE)"
  height(unit: HeightUnit = CENTIMETRE): Float
  "體重 (預設為 KILOGRAM)""
  weight(unit: WeightUnit = KILOGRAM): Float
}

type Query {
  ...
  "取得特定 user (name 為必填)"
  user(name: String!): User
}
  • GraphQL Resolver
const resolverMap = {
  Query: {
    ...
    // 對應到 Schema 的 Query.user
    user: (root, args, context) => {
      // 取出參數。因為 name 為 non-null 故一定會有值。
      const { name } = args;
      return users.find(user => user.name === name);
    }
  },
  User: {
    ...,
    // 對應到 Schema 的 User.height
    height: (parent, args) => {
      const { unit } = args;
      // 可注意到 Enum type 進到 javascript 就變成了 String 格式
      // 另外支援 default 值 CENTIMETRE
      if (!unit || unit === "CENTIMETRE") return parent.height;
      else if (unit === "METRE") return parent.height / 100;
      else if (unit === "FOOT") return parent.height / 30.48;
      throw new Error(`Height unit "${unit}" not supported.`);
    },
    // 對應到 Schema 的 User.weight
    weight: (parent, args, context) => {
      const { unit } = args;
      // 支援 default 值 KILOGRAM
      if (!unit || unit === "KILOGRAM") return parent.weight;
      else if (unit === "GRAM") return parent.weight * 100;
      else if (unit === "POUND") return parent.weight / 0.45359237;
      throw new Error(`Weight unit "${unit}" not supported.`);
    }
  },
}

看完 code 後,我們可以觀察到:

  1. 在 Schema 中定義 arugment 時,可以用 ! 來決定參數為必填 (Non-Null) 或選填 (Nullable)
  2. 不管是 Object Type 或 Scalar Type 都能使用 Argument
  3. 若參數涉及某些特定字串,那事先定義 Enum Type 會是一個好選擇來避免 magic number 氾濫
  4. 如果想要傳入的 Argument 可以如 Object 的型態的話,需要另外定義 Input Object Type ,這在明天的 Mutation 會有更詳細的講解
  5. 這邊解說一下,為什麼 user: (root, args, context) 使用 root 而非 parent,其實兩者的意思差不多,只是因為 Query 已經是最外層的 field ,所以他的 parent 就是 root ,而這個 root 的值是可以透過初始化 Apollo Server 時指定進去的 (預設為 {})。

Variables 掌握所有變化

隨著 Schema 越來越龐大, query 也會越來越複雜,一筆 query 可能會需要十幾個參數輸入,讓 query 十分難以管理,於是讓我們來介紹 Variables 功能!

Variables 範例

  • GraphQL Query
# 因為 user 的 argument name 為必填 `!`,所以在參數宣告列上也要加上 `!`
query ($name: String!) {
  user(name: $name) {
    id
    name
  },
}

加上 Variables 區域 (json 格式):

{
  "name": "Fong"
}
  • Reponse
{
  "data": {
    "user": {
      "id": 1,
      "name": "Fong",
    }
  }
}

可以發現,加入 Variables 可以減少 query 管理參數的複雜度,可以僅藉由 variables 的變化來決定 query 的結果。
不過要注意,使用 variable 的話一定要先宣告,且使用時要前面要加上 $


接下介紹的純為 Client Side 的 Query 技巧,不涉及 Server Schema 的程式。

Operation Name 表達想法

在前面的 Query 的例子中,我們都只使用簡寫的格式: query {} 來索取資料,但其實完整的寫法應該是:

query OperationName {
  ...
}

或是 Mutation (明天會再介紹)

mutation OperationName {
  ...
}

加上 Operation Name 有幾點好處:

  1. 增加可讀性、表達性
  2. 若一次執行多筆 operation, Query Name 有助於區分各個 operation
  3. 有名字才好找問題!當你在 debug 或是效能追蹤時,就會發現名字的妙用!
  4. 對 client 端來說, Operaion Name 讓管理 query 更易讀、方便

Operation Name 範例

  • GraphQL Query
query MyBasicInfo {
  me {
    id
    name
    age
  }
}
  • Response
{
  "data": {
    "me": {
      "id": 1,
      "name": "Fong",
      "age": 23
    }
  }
}

可看到回傳的資料並無不同,但相信我,在做效能與錯誤追蹤時相當有用 !
經驗談: 因為我們有使用 Apollo Engine 來紀錄分析前端送來的每個 query,若沒加上 Operation 就只會得到一堆 query, 這樣有記錄跟沒記錄一樣!

Aliases 撞名不尷尬

講完 Operation 外層的 Operation Name,Operation 內層的 query 也可以加上命名 (別名)!

假如你今天需要在一筆 Query 中抓兩筆特定 user 的資料,因為同筆 Query 內不能有相同的 field Name 不然回傳的 json 會搞混。這時 Aliases 就可以協助撞名的困擾:

Aliases 範例

  • GraphQL Query
query UserData($name1: String!, $name2: String!, $name3: String!) {
  user1: user(name: $name1) {
    id
    name
  },
  user2: user(name: $name2) {
    id
    name
  },
  user3: user(name: $name3) {
    id
    name
  },
}

加上 variables

{
  "name1": "Fong",
  "name2": "Kevin",
  "name3": "Mary"
}
  • Server Response
{
  "data": {
    "user1": {
      "id": "1",
      "name": "Fong"
    },
    "user2": {
      "id": "2",
      "name": "Kevin"
    },
    "user3": {
      "id": "3",
      "name": "Mary"
    }
  }
}

可以注意到 data 裡面的 field 名稱會變成 aliases 的名稱。

Fragments 幫你 DRY

有沒已經開始覺得上一支 query 已經有點複雜且充斥著 duplicate code 了呢?此時 Fragments 就可以幫助你 reuse 重複的 Code 並增加可讀性。

Fragment 範例

  • GraphQL Query
query {
  user1: user(name: "Kevin") {
    ...userData
  }
  user2: user(name: "Mary") {
    ...userData
  }
}

fragment userData on User {
  id
  name
}
  • Response
{
  "data": {
    "user1": {
      "id": "2",
      "name": "Kevin"
    },
    "user2": {
      "id": "3",
      "name": "Mary"
    }
  }
}

Fragment 在前端是非常好用的一樣工具,大大減少 duplicate code 以外也讓 query 管理更加方便 !
不過要注意的是, query 的 Fragment 與 schema 的 type 不同, Fragment 只存在於當下的 query , 千萬不要以為使用過一次有定義過下次就可以不定義就直接用,會吃上 Error 的 !


今天的內容不難但東西有點多,希望大家可以花時間消化一下,在這邊提供幾個題目給大家練練。
若懶得打開自己的 project 也可以到第三天的 GraphQL Playground Demo 直接練習 (傳送門)

  1. 建立一個名為 HelloWorld 的 query 來 me 的年齡、體重 (unit 為 KILOGRAM)
    答:點開圖
  2. 取得所有使用者的身高 (unit 為 METRE)
    答:點開圖
  3. 使用 variables 取得 Kevin 與 Mary 的年齡
    答:點開圖

明天就會開始教 GraphQL 的三大支柱之一的 Mutation ,請敬請期待!


上一篇
GraphQL 入門: Schema 與 Resolver 進階功能! (Array, Non-Null, Field Resolver)
下一篇
GraphQL 入門: 初次使用 Mutation
系列文
Think in GraphQL30

1 則留言

1
tacodrem
iT邦新手 5 級 ‧ 2019-04-08 14:16:45

給個小建議~
這篇文章中的"Variables 範例"段落裡
"加上 Variables 區域"
這個步驟....大概額外花了快半小時在搞懂這句話在說什麼吧@@"
估狗了很久, 才終於看到有另一篇文章的截圖說明所謂的"Variables區域"是指哪裡...
偏偏這段落的這一步沒有附上結果截圖, 對於初學讀者來說就是個突然插入的新名詞
丈二金剛莫不著頭緒...Orz|||

可能之前在解說的時候有帶過, 但到了這邊已經完全沒印象了
起了local後的環境像這樣
https://ithelp.ithome.com.tw/upload/images/20190408/20109947xIcT6N3M6C.png

默默縮在左下角, 很難注意到這東西XD

也或許是小弟駑鈍, 算是個案, 只是想留個言讓之後看到的人可以避免一樣的情況
還是很感謝樓主寫了這系列的文章, 造福世人!!
繼續往下學習!!

我要留言

立即登入留言