這篇假設你已經知道基本的 API 串接概念(fetch / axios)與 RESTful API 是什麼。 如果還不熟,建議先看 [JS] API 串接。
什麼是 GraphQL?
GraphQL 是 Facebook(現 Meta)在 2012 年內部開始使用、2015 年正式開源的一種 API 查詢語言,同時也是一套執行這些查詢的 runtime。
聽起來很抽象,直接跟 RESTful API 比比看。
假設今天我要做一個頁面,需要顯示某個使用者的名字、他發過的文章標題、以及每篇文章的留言數。 用 RESTful API 的話,可能要打三支 API:
GET /users/1
GET /users/1/posts
GET /posts/:id/comments (每篇文章各一次)
這個問題有個名字叫做 over-fetching 與 under-fetching:
- over-fetching:每支 API 回來的資料裡,有一大堆我根本用不到的欄位,全都白白傳輸了。
- under-fetching:一次請求拿到的資料不夠,必須再發第二、第三次請求。
GraphQL 的解法是:讓 client 自己決定要什麼資料。 不管多複雜的需求,只需要打一次請求,並且精確地描述你要的欄位結構:
query {
user(id: 1) {
name
posts {
title
comments {
count
}
}
}
}
Server 就只會回傳你要求的那些欄位,不多也不少。
GraphQL 不是 RESTful API 的競爭對手,它是另一種設計 API 的思路。 對於資料結構複雜、前端有多種不同需求的場景(例如同一份資料要同時供 web 跟 mobile 使用,但需要的欄位不一樣),GraphQL 有明顯優勢。 但對於資料結構簡單的小型專案,RESTful 通常還是更直覺、更快速的選擇。
GraphQL 的核心概念
Schema 與 Type
GraphQL 的 server 端會定義一份 Schema,描述整個 API 有哪些資料、每種資料長什麼形狀(型別)。 可以把它想成是整個 API 的說明書,客戶端只能查 Schema 裡有定義的東西。
Schema 裡會定義 Type,例如:
type Character {
id: ID!
name: String!
status: String
species: String
origin: Location
}
type Location {
name: String
}
! 代表這個欄位不可為 null(必定有值)。
Type 可以互相嵌套,就像上面的 Character 裡面包了 Location。
Query
Query 是用來讀取資料的,對應 RESTful 的 GET。
語法上先宣告 query,然後描述你想要的資料結構:
query {
character(id: 1) {
name
status
origin {
name
}
}
}
Mutation
Mutation 是用來寫入或修改資料的,對應 RESTful 的 POST / PUT / DELETE。
語法跟 Query 很像,只是把關鍵字換成 mutation:
mutation {
createCharacter(name: "Jeremy", species: "Human") {
id
name
}
}
Variables
Query 跟 Mutation 裡的動態值,不應該直接硬寫在查詢字串裡,而是透過 variables 傳入。 這樣查詢語法可以重複使用,安全性也更好(避免 injection 問題):
query GetCharacter($id: ID!) {
character(id: $id) {
name
status
}
}
然後把變數值另外帶過去:
{
"id": "1"
}
Fragment
當多個 Query 需要取相同的欄位組合時,可以用 Fragment 把那組欄位抽出來重複使用,避免複製貼上。
例如角色的基本資訊可能在好幾個 Query 裡都會用到:
fragment CharacterBasic on Character {
name
status
species
origin {
name
}
}
定義好之後,在 Query 裡用 ...FragmentName 展開:
query GetCharacter($id: ID!) {
character(id: $id) {
...CharacterBasic
}
}
query GetCharacters {
characters {
results {
...CharacterBasic
}
}
}
Fragment 在前端框架裡特別實用:每個元件可以定義自己需要的欄位 Fragment,由父元件的 Query 把所有 Fragment 組合起來一次請求,欄位需求就跟著元件走,不需要集中管理。
範例 API 與前置作業
這篇使用 Rick and Morty API 作為練習對象。 這是一個完全公開、不需要任何 API key 的 GraphQL endpoint,非常適合拿來練手。
endpoint:https://rickandmortyapi.com/graphql
建議在動手寫 code 之前,先到 Rick and Morty GraphQL Playground 用瀏覽器試打幾個 query,感受一下 GraphQL 的查詢方式。
HTML 基本架構:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GraphQL Demo</title>
</head>
<body>
<input id="char-id" type="number" placeholder="輸入角色 ID(1~826)" value="1" />
<button id="search-btn">查詢</button>
<pre id="result"></pre>
<script src="index.js"></script>
</body>
</html>
用 fetch 打 GraphQL
這是最重要的一個認知:GraphQL 的底層就是一個普通的 HTTP POST 請求。
不管用什麼工具,最終都是往 endpoint 送一個 JSON,裡面帶著 query 字串跟 variables。
所以原生的 fetch 完全夠用:
const endpoint = 'https://rickandmortyapi.com/graphql'
const btn = document.getElementById('search-btn')
const input = document.getElementById('char-id')
const result = document.getElementById('result')
btn.addEventListener('click', fetchCharacter)
async function fetchCharacter() {
const id = input.value
const query = `
query GetCharacter($id: ID!) {
character(id: $id) {
name
status
species
origin {
name
}
}
}
`
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables: { id },
}),
})
const { data, errors } = await response.json()
if (errors) {
result.textContent = JSON.stringify(errors, null, 2)
return
}
result.textContent = JSON.stringify(data.character, null, 2)
} catch (error) {
console.error(error)
}
}
注意兩個 GraphQL 特有的地方:
- 永遠是 POST,不管是查詢還是寫入。
- HTTP status 幾乎永遠是 200,GraphQL 把錯誤資訊包在 response body 的
errors欄位裡回傳,而不是用 HTTP 狀態碼表示失敗。所以記得要檢查errors,光看response.ok是不夠的。
GraphQL 的這個設計跟 RESTful 很不一樣,很多人第一次踩坑就是在這裡——看到 200 OK 以為成功了,結果 data 是 null,錯誤全在 errors 裡面。
延伸:Apollo Client
GraphQL 生態系裡功能最完整的 client 框架是 Apollo Client,提供自動快取、與 React / Vue 深度整合的宣告式查詢、以及 WebSocket Subscription 支援。但 bundle size 相對大,小型專案通常只需要 fetch 就夠用。