0%

什麼是 GraphQL ?

GraphQL 是一種由 Facebook 創建的開源查詢語言。在 GraphQL 於 2015 年開源之前,Facebook 自 2012 年起在其內部使用它作為移動應用程序(mobile application)常見的 REST 架構的替代方案。它允許對特定數據的請求,使客戶端能夠更好的控制發送的信息。使用 RESTful 架構時這更加困難,因為後端定義了每個 URL 上每個資源可用的數據,而前端始終必須請求資源中的所有信息,即使只需要其中的一部分。此問題稱為過度提取(overfetch)。在最壞的情況下,客戶端應用程序必須通過多個網路請求讀取多個資源。這種過度提取,也增加了對瀑佈網路請求(waterfall network requests)的需求。 GraphQL 允許客戶端通過向服務器發出單個請求來決定所需的數據。因此,Facebook 的移動應用程序的網路使用率大幅下降,因為 GraphQL 使數據傳輸更加有效率。

GraphQL 是供 API 使用的查詢語言。它也是滿足資料查詢的運行環境(runtime)。GraphQL 服務不規定使用那種傳輸協定,但通常是透過 HTTP 來提供的。

為了展示 GraphQL query 與它的回應,請看一下 SWAPI,這是星際大戰(Star Wars) API。SWAPI 是與 GraphQL 包在一起的 REST API。我們可以用來傳送 query 及接收資料。

GraphQL query 只會要求它需要的資料。以下是個 GraphQL query 範例,在 query 區塊,我們索取 Princess Leia 的個人資料。由於我們指明索取第五人的資料(PersonID:5),所以得到 Leia Organa 的紀錄。接下來,我們指定取得資料的三個欄位(field,Google 翻譯為 “字段”): name, birthYear, created。result 區塊則是回應: 它按照 query 的外形(shape)將 JSON 資料格式化了。這個回應只含有我們需要的資料。這與 SQL 很相似,所以同樣都稱為 Query Language。

query
1
2
3
4
5
6
7
query {
person(personID:5) {
name
birthYear
created
}
}
result
1
2
3
4
5
6
7
8
9
{
"data": {
"person": {
"name": "Leia Organa",
"birthYear": "19BBY",
"created": "2014-12-10T15:20:09.791000Z"
}
}
}

因為查詢的動作是互動的,所以我們接下來可以修改它,以查詢新的結果。如果我們加入欄位 filmConnection,就可以索取有 Leia 的電影名稱。

query
1
2
3
4
5
6
7
8
9
10
11
12
query {
person(personID:5) {
name
birthYear
created
filmConnection {
films {
title
}
}
}
}
result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"data": {
"person": {
"name": "Leia Organa",
"birthYear": "19BBY",
"created": "2014-12-10T15:20:09.791000Z",
"filmConnection": {
"films": [
{
"title": "A New Hope"
},
{
"title": "The Empire Strikes Back"
},
{
"title": "Return of the Jedi"
},
{
"title": "Revenge of the Sith"
},
{
"title": "The Force Awakens"
}
]
}
}
}
}

這個 query 是嵌套狀的,它被執行時可遍歷相關的物件,如此一來,我們就可以用單一 HTTP 請求來取得兩種類型的資料,而不需要為了查詢多個物件而反覆執行多次。我們不會收到與這些類型有關但不想要取得的資料。使用 GraphQL 時,用戶端可以用一個請求指令取得他們需要的所有資料。

當你對 GraphQL 伺服器執行 query 時,它會用一種型態系統(type system)來驗證 query。每一個 GraphQL 服務都會在 GraphQL schema(綱要)裡定義許多型態。你可以將型態系統視為 API 的資料藍圖,這個藍圖的基礎是你所定義的一系列物件。例如,稍早的人物 query 的基礎是 Person 物件:

Person
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Person {
id: ID!
name: String
birthYear: String
eyeColor: String
gender: String
hairColor: String
height: Int
mass: Float
skinColor: String
homeworld: Planet
species: Species
filmConnection: PersonFilmsConnection
starshipConnection: PersonStarshipConnection
vehicleConnection: PersonVehiclesConnection
created: String
edited: String
}

Person 型態定義了所有欄位及其型態,讓你可以在查詢 Princess Leia 時取得它們。

GraphQL 通常被稱為宣告式(declarative)資料擷取語言,意思是開發者只要跟據他們需要哪些資料來列出資料需求,而不需要把注意力放在如何取得它。市面上有供各種語言使用的 GraphQL 伺服器程式庫,包括 C#、Clojure、 Elixir、 Erlang、 Go、 Groovy、 Java、 JavaScript、 .NET、 PHP、 Python、 Scala 與 Ruby。

GraphQL 規格

GraphQL 是一種用戶端 / 伺服器通信規格(spec)。規格描述了某種語言的功能與特性。我們受益於語言規格,因為它提供了共通的詞彙與最佳用法,讓語言與使用社群得以依循。

ECMAScript 規格是一種著名的軟體規格。每隔一段時間,一群來自流覽器公司、科技公司與語言社群的代表都會聚在一起,討論應該在 ECMAScript 規格中加入(與移除)哪些東西。GraphQL 也是如此,有一群人會聚在一起討論應納入這種語言(或從中移除)的東西,這份規格是所有 GraphQL 實品的指南。

當規格發表時,GraphQL 的創造者也會分享一個以 JavaScript 寫成的參考成品 graphql.js。你可以將這個參考成品當成藍圖,但它的目的不包括規定你一定要用某種語言來實作服務,它只是個指南。了解這種查詢語言與型態系統之後,你可以用你喜歡的任何一種語言來建立服務。

規格敘述編寫 query 時應使用的語言與語法,也規定一個型態系統及型態系統的執行和驗證引擎。除了以上的規定之外,規格不限制任何東西,GraphQL 未規定該使用哪種語言、如何儲存資料、該支援哪些用戶端。查詢語言有一份指南,但如何實際運用在你的專案完全由你自己決定。

GraphQL 的設計準則

雖然 GraphQL 不限制建構 API 的方式,但它提供了一些構思服務的指南:

  • 階層式

    GraphQL query 是階層式的,query 有一些欄位在其它欄位裡面,且 query 的外型類似它回傳的資料。

  • 以產品為中心

    GraphQL 的目地是提供用戶端需要的資料,以及支援用戶端的語言和 runtime。

  • 強型態

    GraphQL 伺服器採取 GraphQL 型態系統。在 schema 內,每一個資料點都有一種特定的型態,GraphQL 會拿它來驗證資料點。

  • 由用戶端指定的 query

    GraphQL 伺服器提供用戶端可以使用的功能。

  • 自我查詢

    GraphQL 語言可查詢 GraphQL 伺服器的型態系統。

REST 的缺陷

REST 應該是目前應用最普遍的 API 架構。這是一個資源導向的架構,可讓使用者在這個架構上執行諸如 GET、PUT、POST 與 DELETE 等動作來處理網路資源。你可將資源組成的網路當成一種虛擬狀態機,而哪些動作(GET、PUT、POST、DELETE)是這個機制的狀態改變。

在 RESTful 架構中,路由(route)代表資訊。例如,向這些路由請求資訊可得到特定的回應:

1
2
3
/api/food/hot-dog
/api/sport/skiing
/api/city/Lisbon

REST 可讓我們用各種端點來建構資料模型,它提供了複雜網路環境中處理資料的方法。配合 JSON 的應用,REST 變成了廣泛使用的 API,但是隨著行動裝置及網路的發展,REST 在某些狀況下已經顯露了一些疲態了。GraphQL 正是為了緩解這些疲態而造就的。

Overfetch

假如我們要建立一個 app,並讓它使用 REST 版的 SWAPI 提供的資料。我們要先載入 1 號角色 Luke Skywalker 的一些資料,為了取得這項資訊,我們可以向 https://swapi.co/api/people/1/ 發出 GET 請求,得到的回應應該是這筆 JSON 資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"name": "Luke Skywalker",
"height": "172",
"mass": "77",
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "https://swapi.co/api/planets/1/",
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
],
"species": [
"https://swapi.co/api/species/1/"
],
"vehicles": [
"https://swapi.co/api/vehicles/14/",
"https://swapi.co/api/vehicles/30/"
],
"starships": [
"https://swapi.co/api/starships/12/",
"https://swapi.co/api/starships/22/"
],
"created": "2014-12-09T13:50:51.644000Z",
"edited": "2014-12-20T21:17:56.891000Z",
"url": "https://swapi.co/api/people/1/"
}

這個龐大的回應,遠比 app 需要的資料還要多。我們只需要 name、mass 與 height 的資訊:

1
2
3
4
5
{
"name": "Luke Skywalker",
"height": 172,
"mass": 77
}

這是明顯的 overfetch (過度擷取)案例 – 取得許多不需要的資料。用戶端只需要三個資料點,卻得到包含 16 個鍵的物件,而且是透過網路傳送不需要的資料。

在 GraphQL 中,這個請求又是如何?

query
1
2
3
4
5
6
7
query {
person(personID:1) {
name
height
mass
}
}
result
1
2
3
4
5
6
7
8
9
{
"data": {
"person": {
"name": "Luke Skywalker",
"height": 172,
"mass": 77
}
}
}

我們索取指定外形的資料,並收到那種外形的資料,不多也不少。這是比較宣告式的作法,因為不會收到無關的額外資料,所以可以更快速的得到回傳。

Underfetch

當我們想在我們的 Star Wars app 中加入另一個功能,除了 name、height 與 mass 之外,也要顯示有 Luke Skywalker 這位角色的電影名稱。當我們向 https://swapi.co/api/people/1/ 請求資料後,仍然需要發出其它的請求來取得更多資料,這代表我們 underfetch (擷取不足)

為了取得每部電影的名稱,我們必須向 films 陣列內的每一個路由索取資料:

1
2
3
4
5
6
7
"films": [
"https://swapi.co/api/films/2/",
"https://swapi.co/api/films/6/",
"https://swapi.co/api/films/3/",
"https://swapi.co/api/films/1/",
"https://swapi.co/api/films/7/"
]

我們要發出一個請求來索取 Luke Skywalker (https://swapi.co/api/people/1/),接著還要發出五個請求來取得每一部電影才能得到這一筆資料。索取每部電影時,我們也會得到一個大型的物件,但我們只想要一個值 title

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"title": "The Empire Strikes Back",
"episode_id": 5,
"opening_crawl": "It is a dark time for the\r\nRebellion. Although the Death\r\nStar has been destroyed,\r\nImperial troops have driven the\r\nRebel forces from their hidden\r\nbase and pursued them across\r\nthe galaxy.\r\n\r\nEvading the dreaded Imperial\r\nStarfleet, a group of freedom\r\nfighters led by Luke Skywalker\r\nhas established a new secret\r\nbase on the remote ice world\r\nof Hoth.\r\n\r\nThe evil lord Darth Vader,\r\nobsessed with finding young\r\nSkywalker, has dispatched\r\nthousands of remote probes into\r\nthe far reaches of space....",
"director": "Irvin Kershner",
"producer": "Gary Kurtz, Rick McCallum",
"release_date": "1980-05-17",
"characters": [
"https://swapi.co/api/people/1/",
"https://swapi.co/api/people/2/",
"https://swapi.co/api/people/3/",
"https://swapi.co/api/people/4/",
"https://swapi.co/api/people/5/",
"https://swapi.co/api/people/10/",
"https://swapi.co/api/people/13/",
"https://swapi.co/api/people/14/",
"https://swapi.co/api/people/18/",
"https://swapi.co/api/people/20/",
"https://swapi.co/api/people/21/",
"https://swapi.co/api/people/22/",
"https://swapi.co/api/people/23/",
"https://swapi.co/api/people/24/",
"https://swapi.co/api/people/25/",
"https://swapi.co/api/people/26/"
],
"planets": [
//... 長串的路由
],
"starships": [
//... 長串的路由
],
"vehicles": [
//... 長串的路由
],
"species": [
//... 長串的路由
],
"created": "2014-12-12T11:26:24.656000Z",
"edited": "2017-04-19T10:57:29.544256Z",
"url": "https://swapi.co/api/films/2/"
}

如果我們想要列出電影的每個角色,就要發出更多的請求,在這個例子中,我們要接觸 16 個路由,並產生 16 次的用戶端往返。每一個 HTTP 請求都會用到用戶端的資源,並過度攫取資料,造成更緩慢的使用者體驗,特別是使用慢速網路或裝置的使用者可能會完全無法看到內容。

GraphQL 處理擷取不足的方法是定義一個嵌套式的 query,接著一次請求所有的資料。

query
1
2
3
4
5
6
7
8
9
10
11
12
query {
person(personID:1) {
name
height
mass
filmConnection {
films {
title
}
}
}
}
result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"data": {
"person": {
"name": "Luke Skywalker",
"height": 172,
"mass": 77,
"filmConnection": {
"films": [
{
"title": "A New Hope"
},
{
"title": "The Empire Strikes Back"
},
{
"title": "Return of the Jedi"
},
{
"title": "Revenge of the Sith"
},
{
"title": "The Force Awakens"
}
]
}
}
}
}

我們只用一個請求來取得想要的資料。而且一如既往,query 的外形符合所收到資料的外形。

管理 REST 端點

經常有人抱怨 REST API 的另外一個地方是它缺乏彈性。當用戶需求改變時,你通常要建立新的端點,而且新端點可能會開始倍增。套句網路名言:

You get a route!You get a route!Every!Body!Gets!Gets!A!Route!

使用 SWAPI REST API 時,我們必須對許多路由發出請求,大型的 app 通常會使用自訂的端點來盡量減少 HTTP 請求,你可能會開始看到 /api/character-with-movie-title 這類的端點。設置新端點通常意味著前端與後端團隊必須進一步擬定計畫與進行更多的溝通,開發速度或許會因而減緩。

使用 GraphQL 時,典型的架構只有一個端點。單一端點可扮演閘道的角色並協調多個資料來源,但就算只有一個端點,我們仍然可以更輕鬆的組織資料。

在討論 REST 的缺陷時,有一個需要特別注意的地方是許多機構都會同時使用 GraphQL 與 REST。設置 GraphQL 端點來從 REST 端點擷取資料是完全有效的 GraphQL 使用方式。在開始逐漸使用 GraphQL 是一種很好的做法。

現時世界的 GraphQL

許多公司都用 GraphQL 來改善它們的 app、網站與 API,GitHub 是早期就大量採用 GraphQL 的公司之一,它的 REST API 經歷三次變動,在公用 API 第 4 版時開始使用 GraphQL,GitHub 在官網提到,他們發現 “能夠準確定義你需要的資料(而且只有你需要的)是它比 REST API v3 端點還要好的地方”。

其它的公司,例如 The New York Times、IBM、Twitter 與 Yelp 也信任 GraphQL,這些團隊的開發者也經常在會議上宣揚 GraphQL 得好處。

GraphQL 用戶端

GraphQL 只是一種規格。它不在乎與它一起使用的究竟是何種瀏覽器、React、Angular、Vue 或者是純 JavaScript。GraphQL 對一些特定的主題有一些意見,但除此之外,如何設計架構由你自行決定。這導致一些工具出現,讓你在規格之外的領域有一些選擇。

GraphQL 用戶端旨在加速開發團隊的工作流程,並改善 app 的效率與性能。它們可處理諸如網路請求、資料快取,以及將資料注入使用者介面等工作。市面上有許多 GraphQL 用戶端選項,但這個領域的領導者是 RelayApollo

Relay 是 Facebook 製作的用戶端,它是與 React 和 React Native 一起運作的。Relay 是 React 元件與 GraphQL 伺服器回傳的資料之間的連接組織。Facebook、GitHub、Twitch 以及許多其它公司都使用 Relay。

Apollo Client 是社群團體 Meteor Development Group 為了建立更全面的 GraphQL 週邊工具而開發的。Apollo Client 支援所有主要的前端開發平台,例如,Vue ApolloApollo Angular,不限定任何特定的框架。Apollo 開發了一些協助建構 GraphQL 服務(Apollo Server)和改善後端服務效能的開發工具,以及監控 GraphQL API 性能的工具組。Airbnb、CNBC、 The New York Times 與 Ticketmaster 等公司都在產品中使用 Apollo Client。

GraphQL 已經是個相當穩定的標準了,也已經是個龐大且正在持續成長的生態系統。它的概念則來自於圖論(graph theory),要了解關於圖論的點(vertice)與邊(edge)的其他知識,可參考 Vaidehi Joshi 的部落格文章 A Gentle Introduction To Graph Theory

接下來將會是漫長的學習,我們將從 GraphQL 的查詢語言開始,最後會實作 GraphQL API 伺服器。我們將會使用 JavaScript。