[譯] REST API 已死,GraphQL 長存

SigodenH發表於2017-08-14

REST API 已死,GraphQL 長存

在使用多年的 REST API 後,當我第一次接觸到 GraphQL 並瞭解到它試圖解決的問題時,我無法抗拒給本文取了這樣一個標題。

https://twitter.com/samerbuna/status/644548922979954688

當然,過去,這可能只是本人有趣的嘗試,但是現在,我相信這有趣的預測正在慢慢發生。

請不要理解錯了,我並沒有說 GraphQL 會幹掉 REST 或其它類似的話語,REST 大概永遠不會真正消亡,就像 XML 並不會真正消亡一樣。我只是認為 GraphQL 與 REST 的關係將會變得像 JSON 與 XML 一樣。

本文並不是百分百支援 GraphQL。需要注意 GraphQL 靈活性所帶來的開銷。好的靈活性常常伴隨著大的開銷。

我信仰"一切從提問開始",讓我們開始吧。

總而言之:為什麼需要 GraphQL?

GraphQL 漂亮地解決了如下三個重要問題:

  • 填充一個檢視需要的資料進行多次往返拉取: 使用 GraphQL,我們總能夠通過一次往返就能從伺服器獲取到用來填充檢視的所有初始化資料。如果使用 REST API,要到達相同的效果,我們需要引入非結構化的引數和條件,使管理和維護變得困難。
  • 客戶端對服務端產生依賴: 使用 GraphQL,客戶端就有了自己的語言:1) 無需服務端對資料的結構和規格進行硬編碼 2) 客戶端與服務端解耦。這意味著我們能讓客戶端與伺服器分離並單獨對它進行維護和升級。
  • 糟糕的前端開發體驗: 使用 GraphQL,前端開發人員使用宣告式語言表達其對填充使用者介面所需要的資料的需求。他們表達他們需要什麼,而不是如何使其可用。這樣就在 UI 需要的資料和開發人員在 GraphQL 中表述的資料之間構建一種緊密的聯絡。

本文將就 GraphQL 如何解決這些問題進行詳細闡述。

在我們正式開始之前,考慮到你目前可能還不熟悉 GraphQL ,我們先從簡單定義開始。

GraphQL 是什麼?

GraphQL 是一門語言。 如果我們傳授 GraphQL 語言給一款應用,這款應用就能夠向支援 GraphQL 的後端資料服務宣告式傳達資料需求。

就像小孩子很快就能學會一種新語言,而成年人卻很難學會一樣,使用 GraphQL 從頭開始編寫應用比將 GraphQL 新增到一款成熟的應用要容易很多。

為了讓資料服務支援 GraphQL,我們需要實現一個執行時層並將它暴露給想要與服務通訊的客戶端。可以將這個新增到服務端的層簡單地看作是一位 GraphQL 語言翻譯員,或代表資料服務並會說 GraphQL 語言的代理。GraphQL 並不是一個儲存引擎,所以它不能作為一個獨立的解決方案。這就是我們不能有一個純粹的 GraphQL 服務,而需要實現一個翻譯執行時的原因。

這個層可以用任何語言編寫,它定義了一個通用的基於圖的模板來發布它所代表的資料服務的功能。支援 GraphQL 的客戶端可以在功能允許的範圍內使用這種模版進行查詢。這一策略可以將客戶端與服務端分離,允許兩者獨立開發和擴充套件。

一個 GraphQL 請求既可以是查詢(讀操作),也可以是修改(寫操作)。不管是何種情形,請求均只是一個帶有特定格式的簡單字串,GraphQL 伺服器可以對其進行解析、執行、處理。在移動和 Web 應用中最常見的響應格式是 JSON

什麼是 GraphQL ?(把我當五歲小孩後再向我解釋版)

GraphQL 一切為了資料通訊。你有一個需要需要彼此通訊的客戶端和伺服器,客戶端需要告訴伺服器它需要什麼資料,伺服器需要根據客戶端的需求返回具體的資料,GraphQL 作為這種通訊的中間人。

[譯] REST API 已死,GraphQL 長存

螢幕截圖中是我的 Pluralsight 課程 —— 使用 GraphQL 構建可擴充套件 API 你問,客戶端難道不能直接與伺服器通訊嗎?答案是能。

這兒有幾個原因導致我們需要在客戶端和伺服器間新增一個 GraphQL 層。原因之一,可能也是最主要的原因,這樣做更高效。客戶端通常需要從伺服器獲取多個資源,而伺服器通常只能理解如何對單個資源進行回覆。這就造成客戶端最後需要多次往返伺服器才能集齊需要的資料。

通過 GraphQL,我們基本上可以將這種複雜的多次請求轉移到服務端,讓 GraphQL 層來處理。客戶端向 GraphQL 層發起單個請求,並得到一個完全符合客戶端需求的響應。

使用 GraphQL 層還有很多其它好處。例如,另一個大的好處是與多個服務進行通訊。當您有多個客戶端向多個服務請求資料時,中間的 GraphQL 可以讓通訊簡化、標準化。儘管與 REST API 比起來這不算是賣點 —— 因為 REST API 也可以很容易地完成同樣的工作 —— 但 GraphQL 執行時提供了一種結構化和標準化的方法。

[譯] REST API 已死,GraphQL 長存

螢幕截圖中是我的 Pluralsight 課程 —— 使用 GraphQL 構建可擴充套件 API 不是讓客戶端直接請求兩個不同的資料服務(如幻燈片所示),而是讓客戶端先與 GraphQL 層通訊。GraphQL 層再分別與兩個不同的資料服務通訊。通過這種方式,GraphQL 解決了客戶端必須與多個不同語言的後端進行通訊的問題,並將單個請求轉換為使用不同語言的多個服務的多個請求。

想象一下,你認識三個人,他們說不同的語言,掌握著不同領域的知識。然後再想象一下,你遇到一個只有結合三個人的知識才能回答的問題。如果你有一個會說這三種語言的翻譯人員,那麼任務就變成將你的問題的答案放在一起,這就很容易了。這就是 GraphQL 執行時要做的。

計算機還沒有聰明到能回答任何問題(至少目前是這樣),所以它們必須遵守某種演算法。這就是為什麼我們需要在 GraphQL 執行時中定義一個模板讓客戶端來使用的原因。

這個模板基本上是一個功能文件,它列出了客戶端能向 GraphQL 層查詢的全部問題。因為模板採用了圖形節點所以在使用上具有一定的靈活性。模板也表明了 GraphQL 層能解答哪些問題,不能解答哪些問題。

還是不理解?讓我用最確切最簡短的話語來描述 GraphQL :一種 REST API 的替代。接下來讓我回答一下你很可能會問的問題。

REST API 有什麼錯?

REST API 最大的問題是其天然傾向多端點。這造成客戶端需要多次往返獲取資料。

REST API 通常由多個端點組成,每個端點代表一種資源。因此,當客戶端需要多個資源時,它需要向 REST API 發起多個請求,才能獲取到所需要的資料。

在 REST API 中,是沒有描述客戶端請求的語言的。客戶端無法控制伺服器返回哪些資料。沒有讓客戶端對返回資料進行控制的語言。更確切的說,客戶端能使用的語言是很有限的。

例如,有如下進行讀取操作的 REST API:

  • GET /ResouceName - 從該資源獲取包含所有記錄的列表
  • GET /ResourceName/ResourceID - 通過 ID 獲取某條特定記錄

例如,客戶端是不能夠指定從該資源的記錄中選擇哪些欄位的。資訊僅存在於提供 REST API 的服務中,該服務將始終返回所有欄位,而不管客戶端需要什麼。借用 GraphQL 術語描述這個問題:超額獲取(over-fetching) 沒用的資訊。這浪費了伺服器和客戶端的網路記憶體資源 * REST API 的另一個大問題就是版本控制了。如果你需要支援多版本,那你就需要為此建立多個新的端點。這會導致這些端點很難使用和維護,此外,還造成服務端出現很多冗餘程式碼。

上面列出的一些 REST API 帶來的問題都是 GraphQL 試圖解決的。這並不是 REST API 帶來的全部問題,我也不打算說明 REST API 是什麼不是什麼。我只是在談論一種最流行的基於資源的 HTTP 終點 API。這些 API 最終都會變成一種具有常規 REST 特性的端點和出於效能原因定製的特殊端點的組合。

GraphQL 如何實現其魔力?

在 GraphQL 背後有很多的概念和設計策略,這兒列舉了一些最重要的:

  • GraphQL 模板是強型別的。要建立一套 GraphQL 模板,我們需要定義了一些帶有型別欄位。這些型別可以是原始資料型別也可以是自定義的,在模板中一切均需要型別。豐富的型別系統帶來了豐富的特性,如 API 自證,這讓我們能夠為客戶端和服務端建立強大的工具。
  • GraphQL 以圖的形式組織資料,資料自然形成圖。如果你需要一個結構描述資料,圖是一種不錯的選擇。GraphQL 執行時讓我們能夠使用與該資料的自然圖結構匹配的圖 API 來表示我們的資料。 -GraphQL 具有表達資料需求宣告性質。GraphQL 讓客戶端能夠以一種宣告性的語言描述其對資料的需求。這種宣告性帶來了一種圍繞著 GraphQL 語言使用的心智模型,該模型與我們用自然語言思考資料需求的方式接近,讓我們使用 GraphQL 時比使用其它方式更容易。

最後一個概念是我為什麼認為 GraphQL 是遊戲規則改變者的原因。

這些全是抽象概念。讓我們深入到細節中。

為了解決多次往返請求的問題,GraphQL 讓響應伺服器變成一個端點。本質上,GraphQL 把自定義端點這一思想發揮到了極致,它讓這個端點能夠回覆所有資料問題。

伴隨著單個端點這一概念的另一個重要概念是需要一種強大的客戶端請求描述語言與自定義的單個端點進行通訊。缺少客戶端請求描述語言,單個端點是沒有意義的。它需要一種語言解析自定義請求以及根據自定義請求返回資料。

擁有一門客戶端請求描述語言意味這客戶端能夠對請求進行控制。客戶端能夠精確表達它們需要什麼,服務端也能精準回覆客戶端需要的。這就解決了超額獲取的問題。

當涉及到版本時,GraphQL 提供了一種有趣的解決方式。版本能夠被完全避免。基本上,我們只需要在保留老的欄位的基礎上新增新欄位即可,因為我們用的是圖,我們能很靈活的在圖上新增更多節點。因此,我們可以在圖上留下舊的 API,並引入新的 API,而不會將其標記為新版本。API 只是多了更多節點。

這點對於移動端尤為重用,因為我們無法充值這些移動端使用的版本。一經安裝,移動端應用可能數年都使用老版本 API 。對於 Web,我們可以通過釋出新程式碼簡單的控制 API 版本,對於移動端應用,這點很難做到。

還沒有完全相信? 結合例項一對一對比 GraphQL 和 REST 怎麼樣?

REST 風格 API vs GraphQL API —— 案例

我們假設我們是開發者,負責構建閃亮全新的使用者介面,用來展示星球大戰影片和角色。

我們要構建的第一份 UI 很簡單:一個顯示單個星球大戰角色的資訊檢視。例如,達斯·維德以及電影中出場的其他角色。這個檢視需要顯示角色的姓名、出生年份、母星名、以及出場的所有影片中出現的頭銜。

聽起來很簡單,我們實際上已經需要處理三種不同的資源:人物、星球和電影。資源之間的關係很簡單,任何人都很容易就猜出這裡的資料組成。

此 UI 的 JSON 資料可能類似於:

{
  "data": {
    "person": {
      "name": "Darth Vader",
      "birthYear": "41.9BBY",
      "planet": {
        "name": "Tatooine"
      },
      "films": [
        { "title": "A New Hope" },
        { "title": "The Empire Strikes Back" },
        { "title": "Return of the Jedi" },
        { "title": "Revenge of the Sith" }
      ]
    }
  }
}
複製程式碼

假設資料服務按照上面的結構返回資料給我們。我們有一種可行的方式即使用 React.js 來展現檢視:

// The Container Component:
<PersonProfile person={data.person} ></PersonProfile>

// The PersonProfile Component:
Name: {person.name}
Birth Year: {person.birthYear}
Planet: {person.planet.name}
Films: {person.films.map(film => film.title)}
複製程式碼

這是一個簡單例子,此外我們關於星球大戰的經驗也能幫我們一點忙,我們可以很清楚的明白 UI 和資料之間的關係。與我們想象一致,UI 是使用了 JSON 資料物件中的全部的鍵。

讓我們來看看如何通過 REST 風格 API 獲取這些資料。

我們需要單個角色的資訊,假設我們知道這個角色的 ID,REST 風格的 API 傾向於這樣輸出這些資訊:

GET - /people/{id}
複製程式碼

這個請求將會返回角色的姓名、出生年份以及一些其它資訊給我們。一個規範的 REST 風格 API 將會返回給我們角色星球的 ID 以及該角色出現過的所有影片的 ID 組成的陣列。

這個請求以 JSON 格式返回的響應類似於:

{
  "name": "Darth Vader",
  "birthYear": "41.9BBY",
  "planetId": 1
  "filmIds": [1, 2, 3, 6],
  *** 其它資訊我們不需要 ***
}
複製程式碼

然後為了獲取星球名稱,我們發起請求:

GET - /planets/1
複製程式碼

接著為了獲取影片中的頭銜,我們發起請求:

GET - /films/1
GET - /films/2
GET - /films/3
GET - /films/6
複製程式碼

當從伺服器接受到所有的六個資料後,我們才能將其組合並生成滿足檢視需要的資料。

除了有需要六次往返才能獲取到滿足一個簡單 UI 需求的資料這一事實外,這種方式並無不可。我們闡明瞭如何獲取資料,以及如何處理資料使其滿足檢視需要。

如果你想確認我說的你可以自己動手嘗試。有一個部署在 swapi.co/ 上的 REST API 服務提供了星球大戰的資料,點進去,在裡面嘗試構造角色資料。資料的鍵名可能不同,但 API 端點是一致的。你同樣需要進行六次 API 呼叫。同樣,你不得不超額獲取檢視不需要的資訊。

當然,這只是 REST API 的一個實現方式,可能有更好的實現讓生成檢視更簡單。例如,如果 API 服務支援資源巢狀並能理解角色和影片之間的關係,我們能夠通過這種方式獲取影片資料:

GET - /people/{id}/films
複製程式碼

然而,一個純粹的 REST API 服務很難實現這點。我們需要讓後端工程師為我們建立自定義端點。這造成 REST API 規模不斷增長這一事實 —— 為了滿足不斷增長的客戶端的需要,我們不斷新增自定義端點。管理這些自定義端點很難。

讓我們來看一看 GraphQL 策略。GraphQL 在服務端擁抱自定義端點思想並把它發展到極致。服務將只是一個端點,通道變得沒有意義。如果我們使用 HTTP 實現,HTTP 方法將失去意義。假設我們有一個單一的 GraphQL 端點,它的 HTTP 地址是 /graphql

因為我們希望一次往返獲取需要的資料,所以我們需要明明白白告訴伺服器我們需要哪些資料。我們通過 GraphQL 進行查詢:

GET or POST - /graphql?query={...}
複製程式碼

GraphQL 查詢只是字串,但它將包含我們需要的全部資料。這就是宣告的強大之處。

英語中,我們這樣闡述資料需求:我們需要角色名、出生年份、星球名和在所有出現過的影片中的頭銜。通過 GraphQL,我們進行如下轉換:

{
  person(ID: ...) {
    name,
    birthYear,
    planet {
      name
    },
    films {
      title
    }
  }
}
複製程式碼

再細讀一次英語表述的需求並與 GraphQL 查詢進行對比。它們不能再更接近了。現在,將 GraphQL 查詢與我們最開始用到的原始 JSON 資料進行對比。GraphQL 查詢完全與 JSON 資料結構相對應,不過排除所有是值的部分。如果我們仿照問題與答案關係來考慮這中情況,那問題就是沒有具體答案的答案原語。

如果答案是:

離太陽最近的星球是水星。

一種好的提問方式是保留原話只去掉提問部分:

哪個星球裡太陽最近?

這種關係同樣適用於 GraphQL 查詢。拿著 JSON 格式的響應資料,移除所有是答案的部分(作為值的物件),最後你得到了一個非常適合代表關於 JSON 響應問題的 GraphQL 查詢。

現在,將 GraphQL 查詢和與我們展示資料的宣告性 React UI 對比。所有出現在 GraphQL 查詢中的資料都出現在了 UI 中。所有出現在 UI 中的資料都出現在了 GraphQL 查詢中。

這就是 GraphQL 強大的心智模型。UI 知曉它所需要的確切資料,提取需要的資料也很容易。編寫 GraphQL 查詢變成一個從 UI 中提取作為變數這一簡單的工作。

將模型進行反轉,它仍然很強大。如果我們知道了 GraphQL 查詢,我們同樣知道如何在 UI 中使用相應資料。我們不需要分析響應資料就能使用它,也不需要的這些 API 的文件。這一切都是內建的。

獲取星球大戰資料的 GraphQL 託管在 github.com/graphql/swa…。點選進去並嘗試構造角色資料。只有一點點不同,我們之後會談論,以下是可以從這個 API 中獲取檢視所需要資料的正式查詢(使用達斯·維德舉例)

{
  person(personID: 4) {
    name,
    birthYear,
    homeworld {
      name
    },
    filmConnection {
      films {
        title
      }
    }
  }
}
複製程式碼

這個請求返回的我們的響應資料結構十分接近檢視用到的,記住,這些資料是我們通過一次往返獲得的。

GraphQL 靈活性帶來的開銷

完美的解決方案是不存在的。GraphQL 帶來了靈活性,也帶來了一些明確的問題和考量。

GraphQL更容易的造成一個安全隱患是資源耗盡型攻擊(拒絕服務攻擊)。GraphQL 伺服器可能會受到伴隨著極其複雜的查詢的攻擊,造成伺服器資源耗盡。很容易就能構造一個深度巢狀關係鏈(使用者 -> 好友 -> 好友的好友。) 或者多次通過欄位別名請求同一欄位的查詢。資源耗盡型攻擊並沒有限定 GraphQL,但是在使用 GraphQL 時,我們要特別小心。

這兒有一些緩解措施我們可以用上。我們可以進行一些高階查詢的開銷分析,對單個使用者請求的資料量做某種限制。我們也可以實現一種機制對需要很長時間處理的請求進行超時處理。此外,考慮到 GraphQL 就只是一個處理層,我們能在 GraphQL 之下的更底層進行速率限制。

如果我們嘗試保護的 GraphQL API 端點並不是公開的,僅供我們私有的客戶端(web、移動)內部訪問,我們能夠使用白名單策略並預先稽核伺服器能夠處理的查詢。客戶端僅能通過唯一查詢標識碼向伺服器發起稽核過的查詢。Facebook 似乎就採用了這種策略。

當使用 GraphQL 時,我們還需要考慮到認證和授權。我們是在 GraphQL 解析請求之前,之後還是之間處理它們呢?

為了回答這個問題,需要將 GraphQL 想象成你一種位於你的後端資料請求邏輯頂層的 DSL(領域限定語言)。它只是一個能夠被我們放在客戶端與實際資料服務(多個)之間的處理層。

將認證和授權當成另一個處理層。GraphQL 與認證和授權邏輯的具體實現關係不大。它的意義不在這兒。但是如果我們把這些層放在 GraphQL 之後,我們就可以在 GraphQL 層使用訪問令牌連通客戶端與執行邏輯。這和我們在 REST 風格 API 處理認證和授權類似。

另一件因為 GraphQL 而變得更具挑戰性的任務是客戶端資料快取。REST 風格的 API 因其類似目錄更容易進行快取處理。REST API 通過訪問路徑獲取資料,我們能夠使用訪問路徑作快取鍵。

對於 GraphQL,我們能夠採用類似的策略使用查詢欄位作為響應資料的快取鍵。但是這種方式有限制,效率低下,還容易造成資料一致性方面的問題。原因是多個 GraphQL 查詢的結果很容易重疊,而這種快取策略並沒有考慮到這種重疊。

這個問題有一個很好的解決方案。一個圖的查詢意味這一個圖的快取。如果我們將一個 GraphQL 查詢的響應資料正則化為一個平鋪的記錄集合,為每個記錄設定一個全域性唯一 ID,我們就能夠只快取這些記錄而不用快取整個響應了。

這種處理並不容易。這樣導致一些記錄指向另一些記錄,導致我們可能得管理一個環形圖,導致在寫入和讀取快取時我們需要進行遍歷,導致我們需要編寫一個層來處理快取邏輯。但是,這種方法總體上比基於響應的快取更高效。Relay.js 就是一個採用這種快取策略並在內部進行自動管理的框架。

對於 GraphQL 我們最需要關心的問題可能是被普遍稱作 N+1 SQL 查詢的問題了。GraphQL 的欄位查詢被設計成獨立的函式,從資料庫獲取這些欄位可能造成每個欄位都需要一個資料庫查詢。

簡單 REST 風格 API 端點的邏輯,易分析,易檢測,可以優化 SQL 查詢語句來解決 N+1 問題。而 GraphQL 需要動態處理欄位,這點不容易做到。幸運的是 Facebook 正在研發一個處理類似問題的可能的解決方案:DataLoader。

如名字暗示,DataLoader 是一款能讓我們從資料庫讀取資料並讓資料能被 GraphQL 處理函式使用的工具。我們使用 DataLoader,而不是直接通過 SQL 查詢從資料庫獲取資料,將 DataLoader 作為代理以減少我們實際需要傳送給資料庫的 SQL 查詢。

DataLoader 使用批處理和快取的組合來實現。如果同一個客戶端請求會造成多次請求資料庫,DataLoader 會整合這些問題並從資料庫批量拉取請求資料。DataLoader 會同時快取這些資料,當有後續請求需要同樣資源時可以直接從快取獲取到。


謝謝你閱讀本文。如果你覺得本文有用,點選下面的連線。關注我以獲取更多的關於 Node.js 和 JavaScript 的文章。

我在 Pluralsight and Lynda 上建立了線上課程。我最近的課程包含 Advanced React.js](www.pluralsight.com/courses/rea…), Advanced Node.js, and Learning Full-stack JavaScript

我還在做讓 JavaScript、Node.js、React.js 和 GraphQL 初學者進階到更高階別的線上線下培訓。如果您正在尋找教練,請與我聯絡。如果你您對本文以及我寫的其它文章有疑問,可以在 這個slack賬戶(你可以邀請自己) 找到我並在 #questions 頻道提問。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章