<b>本文講的是[譯] REST API 已死,GraphQL 長存,</b>
<b></b>
在使用多年的 REST API 後,當我第一次接觸到 GraphQL 并了解到它試圖解決的問題時,我無法抗拒給本文取了這樣一個标題。
https://twitter.com/samerbuna/status/644548922979954688
當然,過去,這可能隻是本人有趣的嘗試,但是現在,我相信這有趣的預測正在慢慢發生。
請不要了解錯了,我并沒有說 GraphQL 會幹掉 REST 或其它類似的話語,REST 大概永遠不會真正消亡,就像 XML 并不會真正消亡一樣。我隻是認為 GraphQL 與 REST 的關系将會變得像 JSON 與 XML 一樣。
本文并不是百分百支援 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 伺服器可以對其進行解析、執行、處理。在移動和 Web 應用中最常見的響應格式是 JSON 。
GraphQL 一切為了資料通信。你有一個需要需要彼此通信的用戶端和伺服器,用戶端需要告訴伺服器它需要什麼資料,伺服器需要根據用戶端的需求傳回具體的資料,GraphQL 作為這種通信的中間人。
螢幕截圖中是我的 Pluralsight 課程 —— 使用 GraphQL 建構可擴充 API
你問,用戶端難道不能直接與伺服器通信嗎?答案是能。
這兒有幾個原因導緻我們需要在用戶端和伺服器間添加一個 GraphQL 層。原因之一,可能也是最主要的原因,這樣做更高效。用戶端通常需要從伺服器擷取多個資源,而伺服器通常隻能了解如何對單個資源進行回複。這就造成用戶端最後需要多次往返伺服器才能集齊需要的資料。
通過 GraphQL,我們基本上可以将這種複雜的多次請求轉移到服務端,讓 GraphQL 層來處理。用戶端向 GraphQL 層發起單個請求,并得到一個完全符合用戶端需求的響應。
使用 GraphQL 層還有很多其它好處。例如,另一個大的好處是與多個服務進行通信。當您有多個用戶端向多個服務請求資料時,中間的 GraphQL 可以讓通信簡化、标準化。盡管與 REST API 比起來這不算是賣點 —— 因為 REST API 也可以很容易地完成同樣的工作 —— 但 GraphQL 運作時提供了一種結構化和标準化的方法。
不是讓用戶端直接請求兩個不同的資料服務(如幻燈片所示),而是讓用戶端先與 GraphQL 層通信。GraphQL 層再分别與兩個不同的資料服務通信。通過這種方式,GraphQL 解決了用戶端必須與多個不同語言的後端進行通信的問題,并将單個請求轉換為使用不同語言的多個服務的多個請求。
想象一下,你認識三個人,他們說不同的語言,掌握着不同領域的知識。然後再想象一下,你遇到一個隻有結合三個人的知識才能回答的問題。如果你有一個會說這三種語言的翻譯人員,那麼任務就變成将你的問題的答案放在一起,這就很容易了。這就是 GraphQL 運作時要做的。
計算機還沒有聰明到能回答任何問題(至少目前是這樣),是以它們必須遵守某種算法。這就是為什麼我們需要在 GraphQL 運作時中定義一個模闆讓用戶端來使用的原因。
這個模闆基本上是一個功能文檔,它列出了用戶端能向 GraphQL 層查詢的全部問題。因為模闆采用了圖形節點是以在使用上具有一定的靈活性。模闆也表明了 GraphQL 層能解答哪些問題,不能解答哪些問題。
還是不了解?讓我用最确切最簡短的話語來描述 GraphQL :一種 REST API 的替代。接下來讓我回答一下你很可能會問的問題。
REST API 最大的問題是其天然傾向多端點。這造成用戶端需要多次往返擷取資料。
REST API 通常由多個端點組成,每個端點代表一種資源。是以,當用戶端需要多個資源時,它需要向 REST API 發起多個請求,才能擷取到所需要的資料。
在 REST API 中,是沒有描述用戶端請求的語言的。用戶端無法控制伺服器傳回哪些資料。沒有讓用戶端對傳回資料進行控制的語言。更确切的說,用戶端能使用的語言是很有限的。
例如,有如下進行讀取操作的 REST API:
GET <code>/ResouceName</code> - 從該資源擷取包含所有記錄的清單
GET <code>/ResourceName/ResourceID</code> - 通過 ID 擷取某條特定記錄
例如,用戶端是不能夠指定從該資源的記錄中選擇哪些字段的。資訊僅存在于提供 REST API 的服務中,該服務将始終傳回所有字段,而不管用戶端需要什麼。借用 GraphQL 術語描述這個問題:超額擷取(over-fetching) 沒用的資訊。這浪費了伺服器和用戶端的網絡記憶體資源
*
REST API 的另一個大問題就是版本控制了。如果你需要支援多版本,那你就需要為此建立多個新的端點。這會導緻這些端點很難使用和維護,此外,還造成服務端出現很多備援代碼。
上面列出的一些 REST API 帶來的問題都是 GraphQL 試圖解決的。這并不是 REST API 帶來的全部問題,我也不打算說明 REST API 是什麼不是什麼。我隻是在談論一種最流行的基于資源的 HTTP 終點 API。這些 API 最終都會變成一種具有正常 REST 特性的端點和出于性能原因定制的特殊端點的組合。
在 GraphQL 背後有很多的概念和設計政策,這兒列舉了一些最重要的:
GraphQL 模闆是強類型的。要建立一套 GraphQL 模闆,我們需要定義了一些帶有類型的字段。這些類型可以是原始資料類型也可以是自定義的,在模闆中一切均需要類型。豐富的類型系統帶來了豐富的特性,如 API 自證,這讓我們能夠為用戶端和服務端建立強大的工具。
GraphQL 以圖的形式組織資料,資料自然形成圖。如果你需要一個結構描述資料,圖是一種不錯的選擇。GraphQL 運作時讓我們能夠使用與該資料的自然圖結構比對的圖 API 來表示我們的資料。
-GraphQL 具有表達資料需求聲明性質。GraphQL 讓用戶端能夠以一種聲明性的語言描述其對資料的需求。這種聲明性帶來了一種圍繞着 GraphQL 語言使用的心智模型,該模型與我們用自然語言思考資料需求的方式接近,讓我們使用 GraphQL 時比使用其它方式更容易。
最後一個概念是我為什麼認為 GraphQL 是遊戲規則改變者的原因。
這些全是抽象概念。讓我們深入到細節中。
為了解決多次往返請求的問題,GraphQL 讓響應伺服器變成一個端點。本質上,GraphQL 把自定義端點這一思想發揮到了極緻,它讓這個端點能夠回複所有資料問題。
伴随着單個端點這一概念的另一個重要概念是需要一種強大的用戶端請求描述語言與自定義的單個端點進行通信。缺少用戶端請求描述語言,單個端點是沒有意義的。它需要一種語言解析自定義請求以及根據自定義請求傳回資料。
擁有一門用戶端請求描述語言意味這用戶端能夠對請求進行控制。用戶端能夠精确表達它們需要什麼,服務端也能精準回複用戶端需要的。這就解決了超額擷取的問題。
當涉及到版本時,GraphQL 提供了一種有趣的解決方式。版本能夠被完全避免。基本上,我們隻需要在保留老的字段的基礎上添加新字段即可,因為我們用的是圖,我們能很靈活的在圖上添加更多節點。是以,我們可以在圖上留下舊的 API,并引入新的 API,而不會将其标記為新版本。API 隻是多了更多節點。
這點對于移動端尤為重用,因為我們無法充值這些移動端使用的版本。一經安裝,移動端應用可能數年都使用老版本 API 。對于 Web,我們可以通過釋出新代碼簡單的控制 API 版本,對于移動端應用,這點很難做到。
還沒有完全相信? 結合執行個體一對一對比 GraphQL 和 REST 怎麼樣?
我們假設我們是開發者,負責建構閃亮全新的使用者界面,用來展示星球大戰影片和角色。
我們要建構的第一份 UI 很簡單:一個顯示單個星球大戰角色的資訊視圖。例如,達斯·維德以及電影中出場的其他角色。這個視圖需要顯示角色的姓名、出生年份、母星名、以及出場的所有影片中出現的頭銜。
聽起來很簡單,我們實際上已經需要處理三種不同的資源:人物、星球和電影。資源之間的關系很簡單,任何人都很容易就猜出這裡的資料組成。
此 UI 的 JSON 資料可能類似于:
假設資料服務按照上面的結構傳回資料給我們。我們有一種可行的方式即使用 React.js 來展現視圖:
這是一個簡單例子,此外我們關于星球大戰的經驗也能幫我們一點忙,我們可以很清楚的明白 UI 和資料之間的關系。與我們想象一緻,UI 是使用了 JSON 資料對象中的全部的鍵。
讓我們來看看如何通過 REST 風格 API 擷取這些資料。
我們需要單個角色的資訊,假設我們知道這個角色的 ID,REST 風格的 API 傾向于這樣輸出這些資訊:
這個請求将會傳回角色的姓名、出生年份以及一些其它資訊給我們。一個規範的 REST 風格 API 将會傳回給我們角色星球的 ID 以及該角色出現過的所有影片的 ID 組成的數組。
這個請求以 JSON 格式傳回的響應類似于:
然後為了擷取星球名稱,我們發起請求:
接着為了擷取影片中的頭銜,我們發起請求:
當從伺服器接受到所有的六個資料後,我們才能将其組合并生成滿足視圖需要的資料。
除了有需要六次往返才能擷取到滿足一個簡單 UI 需求的資料這一事實外,這種方式并無不可。我們闡明了如何擷取資料,以及如何處理資料使其滿足視圖需要。
當然,這隻是 REST API 的一個實作方式,可能有更好的實作讓生成視圖更簡單。例如,如果 API 服務支援資源嵌套并能了解角色和影片之間的關系,我們能夠通過這種方式擷取影片資料:
然而,一個純粹的 REST API 服務很難實作這點。我們需要讓後端工程師為我們建立自定義端點。這造成 REST API 規模不斷增長這一事實 —— 為了滿足不斷增長的用戶端的需要,我們不斷添加自定義端點。管理這些自定義端點很難。
讓我們來看一看 GraphQL 政策。GraphQL 在服務端擁抱自定義端點思想并把它發展到極緻。服務将隻是一個端點,通道變得沒有意義。如果我們使用 HTTP 實作,HTTP 方法将失去意義。假設我們有一個單一的 GraphQL 端點,它的 HTTP 位址是 <code>/graphql</code>
因為我們希望一次往返擷取需要的資料,是以我們需要明明白白告訴伺服器我們需要哪些資料。我們通過 GraphQL 進行查詢:
GraphQL 查詢隻是字元串,但它将包含我們需要的全部資料。這就是聲明的強大之處。
英語中,我們這樣闡述資料需求:我們需要角色名、出生年份、星球名和在所有出現過的影片中的頭銜。通過 GraphQL,我們進行如下轉換:
再細讀一次英語表述的需求并與 GraphQL 查詢進行對比。它們不能再更接近了。現在,将 GraphQL 查詢與我們最開始用到的原始 JSON 資料進行對比。GraphQL 查詢完全與 JSON 資料結構相對應,不過排除所有是值的部分。如果我們仿照問題與答案關系來考慮這中情況,那問題就是沒有具體答案的答案原語。
如果答案是:
離太陽最近的星球是水星。
一種好的提問方式是保留原話隻去掉提問部分:
哪個星球裡太陽最近?
這種關系同樣适用于 GraphQL 查詢。拿着 JSON 格式的響應資料,移除所有是答案的部分(作為值的對象),最後你得到了一個非常适合代表關于 JSON 響應問題的 GraphQL 查詢。
現在,将 GraphQL 查詢和與我們展示資料的聲明性 React UI 對比。所有出現在 GraphQL 查詢中的資料都出現在了 UI 中。所有出現在 UI 中的資料都出現在了 GraphQL 查詢中。
這就是 GraphQL 強大的心智模型。UI 知曉它所需要的确切資料,提取需要的資料也很容易。編寫 GraphQL 查詢變成一個從 UI 中提取作為變量這一簡單的工作。
将模型進行反轉,它仍然很強大。如果我們知道了 GraphQL 查詢,我們同樣知道如何在 UI 中使用相應資料。我們不需要分析響應資料就能使用它,也不需要的這些 API 的文檔。這一切都是内建的。
這個請求傳回的我們的響應資料結構十分接近視圖用到的,記住,這些資料是我們通過一次往返獲得的。
完美的解決方案是不存在的。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,我們就能夠隻緩存這些記錄而不用緩存整個響應了。
對于 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 的文章。
<b>原文釋出時間為:2017年8月14日</b>
<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>