學習 GraphQL 筆記(一)Object Type

最近因為公司的需求,可能要導入 GraphQL 到專案裡面,所以去做了些研究開始學習 GraphQL。
過程中實在是有許多地方的概念,很難搞懂(老了學不來了QQ)。在此紀錄自己學習後的理解,希望會幫助到入門的人,也希望有大大提點不正確的地方(拜託鞭我,我抖 M 不抖音)。
這邊先簡介一下 GraphQL,GraphQL 是由 Facebook 開源出來的專案。其特點是所見即所得的存取資料方式。通常在 RESTful 的 API 設計下,一個頁面可能需要多個 API 才能得到全部要呈現的資料,但是 GraphQL 只有一個 API,可以減少 HTTP 的來往次數。內建類型檢查的功能。個人使用至今,覺得缺點就是會增加 Backend 維護的複雜度。
以下我們可以從 GraphQL 官網的 Banner 看到使用的方式,先在 Backend 定義資料的格式,在前端建立想要的資料結構,最後回傳跟 query 一樣的資料結構 — 這就是所說的所見即所得。
GraphQL 範例
這邊必須抱怨一下,我看了許多介紹都沒提到 Resolver(尤其官網)。覺得缺少這塊會讓人學習上很大的缺陷。
接下來會分為三個部分來介紹:
Object Type
Query Schema
Resolver
這篇主要講述 Object Type 的部分,但是還是會包含一些 Query Schema 的東西。以下是範例:
type Character {
name: String!
appearsIn: [Episode]!
oldField: String @deprecated
length(unit: LengthUnit = METER, sort: String): Float
}
extend type Character {
age: String
}
Object Type & Field
Object Type 是 GraphQL 的基本元件,定義 Front End 可以從 Server 得到什麼類型的資料,以及其 Field。以上面為例:
type 是宣告了這是哪種類型的 Object Type
Character 宣告此 Object Type 的名稱,不能以
__為開頭name、appearsIn、oldField、length則是其擁有的欄位,後面接的就是其欄位的資料型態length後面括號內則是參數extend是指在宣告完 Object Type 後,還可以再額外擴充欄位
Arguments
Field 的參數可以有多個,當參數是非必須的時候,則需要給定一個預設值。
birthday(year: Int = 1991): String
Operation Name
GraphQL 內有三個特殊的 Object Type — Query 、 Mutation、 Subscription,其使用上與一般的 Object Type 沒有差別,唯一的特殊點在於它們定義了 Entry Point。以上面 GraphQL 的 Banner 為例。第二個區域的 query 部分 project(name = "GraphQL") 就會需要在 Server 定義:
type Query {
product(name: String): Product
}
裡面宣告查詢的接口名稱 product 和其參數 name: String,最後是回傳的 Object Type Product。
Query:獲取資料
Mutation:更新資料
Subscription:訂閱資料
Scalar
我們可以從上面的範例,看到 Field 後面會接上該 Field 資料的類型。在 GraphQL 裡面稱之為 Scalar,其內建有:
ID:接受 String 和 Int 格式,但最後都會轉成 String
String:UTF-8 字符,由
"包裹Int
Float
Boolean
除了內建的 Scalar 之外,GraphQL 也允許自定義 Scalar。其轉換的方式需要在 Resolver 設定
// Object Type
scalar Time
type Birthday {
date: Time
}
type Query {
birthday: Birthday
}
// resolver
const { GraphQLScalarType } = require("graphql");
const { Kind } = require('graphql/language');
Time: new GraphQLScalarType({
name: 'Time',
description: 'Date custom scalar type',
parseValue: (value) => new Date(value), // 接收
serialize: (value) => new Date(value), // 回傳
parseLiteral: (ast) =>
if (ast.kind === Kind.INT) return parseInt(ast.value, 10);
return null;
});
Enumeration
一種特殊的 Scalar,限制 Field 的值在一個集合內。驗證值只能是集合中的某一個。
enum COLOR {
RED
GRAY
BLACK
WHITE
ORANGE
}
type Cat {
hair: COLOR
}
List & Non-Null
在 GraphQL 中預設 Field 的值可以為 null ,如果想要限定不能為 null 時,則在後面 scalar 加上 ! 。
List 則是讓資料回傳陣列的方式,方法是用 [] 將 Scalar 包裹起來,另外也可搭配 ! 使用。
Example 1:
myField: [String!]
myField: null // 有效
myField: [] // 有效
myField: ['a', 'b'] // 有效
myField: ['a', null, 'b'] // 錯誤
Example 2:
myField: [String]!
myField: null // 錯誤
myField: [] // 有效
myField: ['a', 'b'] // 有效
myField: ['a', null, 'b'] // 有效
最後將介紹 Interface 和 Union,這兩個概念對我來說是比較難懂的,到現在也不確定我的理解正不正確。希望有不同見解的人多多留言交流一下。
Interface & Union 在使用上常常會搭配 Variables 和 Inline Fragment 使用。因為 Interface & Union 可以相連多個 Object Type,但是最後只會回傳一個 Type,所以需要 Variables 來做篩選。Inline Fragment 則是用來獲得特定 Object Type 的資料。
Interface
Interface 是一個抽象類型,包含某些 Field。而 Object Type 必須包含這些 Field 才能實現這個 Interface。看完官網的這段話後,我真的在想說是在說三小。在看過其他的文章和在社群上討論後,個人理解 Interface 主要使用情境和功能在於,當不同的 Object Type 有許多相同或共用的 Field 時,可以使用 Interface 來做更好的管理。
interface Animal {
weight: Float
height: Float
name: String
}
interface Bird {
fly: Boolean
}
// Dog 裡面還是必須要重新宣告 Animal 裡面的 Fields,有點蠢但沒辦法,可以改進的空間
// 除了 Animal 裡面的 Fields,也能有自己額外的 Fields
type Dog implements Animal {
weight: Float
height: Float
name: String
master: String
}
// 一個 Object Type 可以實現複數個 interface
type Eagle implements Animal & Bird {
weight: Float
height: Float
name: String
fly: Boolean
color: String
}
// 最後是回傳 interface
type Query {
search(text: String!): Animal!
}
當在存取資料時,相同 interface 的欄位就可以直接 query 出來,但是額外的部分就需要使用 Inline Fragment 了
query {
search(text: "Dog") {
name
weight
height
// Inline Fragment 的部分,下一篇會介紹
... on Dog {
master
}
}
}
Union
Union 和 Interface 類似,但並不指定 Object Type 之間的任何共同欄位。所以資料都必須使用 Inline Fragment 來取得。適用於從單個 Field 回傳不相交的 Object Type。
union SearchResult = Photo | Person
type Person {
name: String
age: Int
}
type Photo {
height: Int
width: Int
}
type SearchQuery {
firstSearchResult: SearchResult
}
// Query
# 無效查詢
{
firstSearchResult {
name
height
}
}
# 正確查詢
{
firstSearchResult {
... on Person {
name
}
... on Photo {
height
}
}
}
Input Type
這個 Object Type 需要搭配第二篇的 Variables 一起看可能會比較懂。在 Front End 發送請求的時候,有時候會需要傳入較為複雜的變數,而不僅僅只是個 Scalar 的時候,就可以在 Server 用 Input 來定義,可以使用的變數。
input Search {
key: String!
}
# Front End Query
query SearchKey ($search: Search){
search(key: $search)
}
# Variables
{
key: "Popping"
}