- 原文地址:REST APIs are REST-in-Peace APIs. Long Live GraphQL
- 原文做者:Samer Buna
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:sigoden
- 校對者:jasonxia23、shawnchenxmu
在使用多年的 REST API 後,當我第一次接觸到 GraphQL 並瞭解到它試圖解決的問題時,我沒法抗拒給本文取了這樣一個標題。html
固然,過去,這可能只是本人有趣的嘗試,可是如今,我相信這有趣的預測正在慢慢發生。前端
請不要理解錯了,我並無說 GraphQL 會幹掉 REST 或其它相似的話語,REST 大概永遠不會真正消亡,就像 XML 並不會真正消亡同樣。我只是認爲 GraphQL 與 REST 的關係將會變得像 JSON 與 XML 同樣。node
本文並非百分百支持 GraphQL。須要注意 GraphQL 靈活性所帶來的開銷。好的靈活性經常伴隨着大的開銷。react
我信仰"一切從提問開始",讓咱們開始吧。android
GraphQL 漂亮地解決了以下三個重要問題:ios
本文將就 GraphQL 如何解決這些問題進行詳細闡述。git
在咱們正式開始以前,考慮到你目前可能還不熟悉 GraphQL ,咱們先從簡單定義開始。github
GraphQL 是一門語言。 若是咱們傳授 GraphQL 語言給一款應用,這款應用就可以向支持 GraphQL 的後端數據服務聲明式傳達數據需求。web
就像小孩子很快就能學會一種新語言,而成年人卻很難學會同樣,使用 GraphQL 從頭開始編寫應用比將 GraphQL 添加到一款成熟的應用要容易不少。算法
爲了讓數據服務支持 GraphQL,咱們須要實現一個運行時層並將它暴露給想要與服務通訊的客戶端。能夠將這個添加到服務端的層簡單地看做是一位 GraphQL 語言翻譯員,或表明數據服務並會說 GraphQL 語言的代理。GraphQL 並非一個存儲引擎,因此它不能做爲一個獨立的解決方案。這就是咱們不能有一個純粹的 GraphQL 服務,而須要實現一個翻譯運行時的緣由。
這個層能夠用任何語言編寫,它定義了一個通用的基於圖的模板來發布它所表明的數據服務的功能。支持 GraphQL 的客戶端能夠在功能容許的範圍內使用這種模版進行查詢。這一策略能夠將客戶端與服務端分離,容許二者獨立開發和擴展。
一個 GraphQL 請求既能夠是查詢(讀操做),也能夠是修改(寫操做)。無論是何種情形,請求均只是一個帶有特定格式的簡單字符串,GraphQL 服務器能夠對其進行解析、執行、處理。在移動和 Web 應用中最多見的響應格式是 JSON 。
GraphQL 一切爲了數據通訊。你有一個須要須要彼此通訊的客戶端和服務器,客戶端須要告訴服務器它須要什麼數據,服務器須要根據客戶端的需求返回具體的數據,GraphQL 做爲這種通訊的中間人。
屏幕截圖中是個人 Pluralsight 課程 —— 使用 GraphQL 構建可擴展 API 你問,客戶端難道不能直接與服務器通訊嗎?答案是能。
這兒有幾個緣由致使咱們須要在客戶端和服務器間添加一個 GraphQL 層。緣由之一,可能也是最主要的緣由,這樣作更高效。客戶端一般須要從服務器獲取多個資源,而服務器一般只能理解如何對單個資源進行回覆。這就形成客戶端最後須要屢次往返服務器才能集齊須要的數據。
經過 GraphQL,咱們基本上能夠將這種複雜的屢次請求轉移到服務端,讓 GraphQL 層來處理。客戶端向 GraphQL 層發起單個請求,並獲得一個徹底符合客戶端需求的響應。
使用 GraphQL 層還有不少其它好處。例如,另外一個大的好處是與多個服務進行通訊。當您有多個客戶端向多個服務請求數據時,中間的 GraphQL 可讓通訊簡化、標準化。儘管與 REST API 比起來這不算是賣點 —— 由於 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:
/ResouceName
- 從該資源獲取包含全部記錄的列表/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 把自定義端點這一思想發揮到了極致,它讓這個端點可以回覆全部數據問題。
伴隨着單個端點這一律唸的另外一個重要概念是須要一種強大的客戶端請求描述語言與自定義的單個端點進行通訊。缺乏客戶端請求描述語言,單個端點是沒有意義的。它須要一種語言解析自定義請求以及根據自定義請求返回數據。
擁有一門客戶端請求描述語言意味這客戶端可以對請求進行控制。客戶端可以精確表達它們須要什麼,服務端也能精準回覆客戶端須要的。這就解決了超額獲取的問題。
當涉及到版本時,GraphQL 提供了一種有趣的解決方式。版本可以被徹底避免。基本上,咱們只須要在保留老的字段的基礎上添加新字段便可,由於咱們用的是圖,咱們能很靈活的在圖上添加更多節點。所以,咱們能夠在圖上留下舊的 API,並引入新的 API,而不會將其標記爲新版本。API 只是多了更多節點。
這點對於移動端尤其重用,由於咱們沒法充值這些移動端使用的版本。一經安裝,移動端應用可能數年都使用老版本 API 。對於 Web,咱們能夠經過發佈新代碼簡單的控制 API 版本,對於移動端應用,這點很難作到。
尚未徹底相信? 結合實例一對一對比 GraphQL 和 REST 怎麼樣?
咱們假設咱們是開發者,負責構建閃亮全新的用戶界面,用來展現星球大戰影片和角色。
咱們要構建的第一份 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 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 頻道提問。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。