當前位置:
首頁 > 最新 > 安息吧 REST API,GraphQL 長存

安息吧 REST API,GraphQL 長存

即使與 REST API 打交道這麼多年,當我第一次了解到 GraphQL 和它試圖解決的問題時,我還是禁不住把本文的標題發在了 Twitter 上。

請別會錯意。我不是在說 GraphQL 會「殺死」 REST 或別的類似的東西。REST 可能永遠不會消失,就像 XML 從沒消失過一樣。我只是認為 GraphQL 之於 REST,正如 JSON 之於 XML 那般。

本篇文章實際上並沒有100%贊成 GraphQL。後文會有一個專門的章節來闡述 GraphQL 的靈活性成本,更高的靈活性意味著更高的成本。

我喜歡「始終以 WHY 開頭」,所以讓我們開始吧。

摘要:為什麼我們需要 GraphQL ?

GraphQL 解決的最重要的3個問題分別是:

需要進行多次往返以獲取視圖所需的數據:使用 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 是此種通信方式的中介。

截圖來源於我的 Pluralsight 課程 - 使用 GraphQL 構建可擴展的 API。

你可能會問,為什麼客戶端不直接與伺服器通信呢? 當然可以。

在客戶端和伺服器之間加入 GraphQL 層的考量有多種原因。其中之一,也許是最受歡迎的原因便是效率。客戶端通常需要向伺服器請求多個資源,而伺服器會用單個資源進行響應。所以客戶端的請求最終會多次往返伺服器,以收集所有需要的數據。

使用 GraphQL,我們基本上可以將這種多個請求的複雜度轉移到伺服器端,並且通過 GraphQL 層處理它。客戶端向 GraphQL 層發起單個請求,並獲得一個完全符合客戶端需求的響應。

引入 GraphQL 層有諸多好處。例如,一大好處便是能與多個服務進行通信。當你有多個客戶端請求多個服務的數據時,中間的 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 中,沒有客戶端請求語言。客戶端無法控制伺服器返回的數據。沒有任何語言可以這樣做。更確切地說,可用於客戶端的語言非常有限。

例如,READREST API 端點可能是

GET - 從該資源獲取所有記錄的列表;

GET - 獲取該 ID 標識的單條記錄。

例如,客戶端不能指定為該資源中的記錄選擇哪些欄位。這意味著 REST API 服務將始終返回所有欄位,而不管客戶端實際需要哪些。GraphQL 針對這個問題定義的術語是超量獲取不需要的信息。這對客戶端和伺服器而言都是網路和內存資源的浪費。

REST API 的另一大問題是版本控制。如果你需要支持多個版本,那通常意味著需要新的端點。而在使用和維護這些端點時會導致諸多問題,並且這可能導致伺服器上的代碼冗餘。

上面提到的 REST API 的問題正是 GraphQL 試圖要解決的問題。它們當然不是 REST API 的所有問題,我也不想討論 REST API 是什麼。我主要討論的是比較流行的基於資源的 HTTP 端點 API。這些 API 中的每一個最終都會變成一個具有常規 REST 端點 + 由於性能原因而制定的自定義特殊端點的組合。這就是為什麼 GraphQL 提供了更好的選擇。

GraphQL如何做到這一點?

GraphQL 背後有很多概念和設計決策,但最重要的可能是:

GraphQL 模式是強類型模式。要創建一個 GraphQL 模式,我們要定義具有類型的欄位。這些類型可以是原語的或者自定義的,並且模式中的所有其他類型都需要類型。這種豐富的類型系統帶來豐富的功能,如擁有內省 API,並能夠為客戶端和伺服器構建強大的工具。

GraphQL 使用圖與數據通信,數據自然是圖。如果需要表示任何數據,右側的結構便是圖。GraphQL 運行時允許我們使用與該數據的自然圖形式匹配的圖 API 來表示我們的數據。

GraphQL 具有表達數據需求的聲明性。GraphQL 為客戶端提供了一種聲明式語言,以便表達它們的數據需求。這種聲明性創造了一個關於使用 GraphQL 語言的內在模型,它接近於我們用英語考慮數據需求的方式,並且它讓使用 GraphQL API 比備選方案(REST API)容易得多。

最後一個概念解釋了為什麼我個人認為 GraphQL 是一個規則顛覆者的原因。

這些都是高層次的概念。讓我們進一步了解一些細節。

為了解決多次往返的問題,GraphQL 讓響應伺服器只是作為一個端點。本質上,GraphQL 將自定義端點的思想運用到極致,即讓整個伺服器成為一個可以回復所有數據請求的自定義端點。

與單一端點概念相關的另一大概念是使用該自定義的單個端點所需的富客戶端請求語言。沒有客戶端請求語言,單個端點是沒有用的。它需要一種語言來處理自定義請求,並響應該自定義請求的數據。

擁有客戶端請求語言意味著客戶端將處於控制之中。它們可以明確地請求它們需要什麼,伺服器將會正確應答它們請求的內容。這解決了超量獲取的問題。

對於版本控制,GraphQL 的做法很有趣。我們可以完全避免版本控制。本質上,我們可以添加新的欄位,而不需要刪除舊的欄位,因為我們有一個圖,並且我們可以通過添加更多的節點來靈活地擴展圖。因此,我們可以在圖上留下舊的 API,並引入新的 API,而不會將其標記為新版本。API 只會增長,而不會有版本。

這對於移動客戶端尤其重要,因為我們無法控制它們正在使用的 API 版本。一旦安裝,移動應用可能會持續使用同一個舊版 API 很多年。對於 Web,則很容易控制 API 的版本,因為我們只需推送新的代碼即可。然而對於移動應用,這很難做到。

還不完全信服?要不我們用實際的例子來對 GraphQL 和 REST 做個一對一的比較?

RESTful APIs vs GraphQL APIs?—?示例

假設我們是負責構建展示「星球大戰」電影和角色的嶄新用戶界面的開發者。

我們負責構建的第一個 UI 很簡單:顯示單個星球大戰人物的信息。例如,達斯·維德(Darth Vader),以及該角色參演的所有電影。這個視圖需要顯示人物的姓名,出生年份,星球名稱以及所有他們參演的電影的名稱。

就是這麼簡單,我們只要處理3種不同的資源:人物,星球和電影。這些資源之間的關係也很簡單,任何人都能猜到這裡的數據形狀。人物對象從屬於一個星球對象,並且具有一個或多個電影對象。

這個 UI 的 JSON 數據可能類似於:

假設某個數據服務給我們提供了該數據的確切結構,這有一種使用 React.js 表示它的視圖的方式:

這是一個很簡單的例子,雖然我們對星球大戰的觀影經驗可能有所幫助,但 UI 和數據之間的關係其實是非常清晰的。UI 使用了我們假想的 JSON 數據對象中的所有「鍵」。

現在我們來看看如何使用 RESTful API 請求這些數據。

我們需要獲取單個人物的信息,並且假定我們知道該人物的 ID,則 RESTful API 會將該信息暴露為:

這個請求將返回給我們該人物的姓名,出身年份和其他有關信息。一個設計良好的 RESTful API 還會返回給我們該人物的星球 ID 和參演的所有電影 ID 的數組。

這個請求的 JSON 響應可能是這樣的:

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

然後為了獲取電影名,我們發出請求:

一旦我們獲取了來自伺服器的所有6個響應,我們便可以將它們組合起來,以滿足我們的視圖所需的數據。

除了我們必須做6次往返以滿足一個簡單的用戶界面的簡單數據需求的事實,我們獲取數據的方法是命令式的。我們給出了如何獲取數據以及如何處理它以使其準備好渲染視圖的說明。

如果你不明白我的意思,你可以自己動手嘗試一下。星球大戰數據有一個 RESTful API,目前由 http://swapi.co/ 託管。可以去嘗試使用它構建我們的人物數據對象。數據的鍵可能有所不同,但是 API 端點是一樣的。你需要執行6次 API 調用。此外,你將不得不超量獲取視圖不需要的信息。

當然,這只是 RESTful API 對於此數據的一個實現。可能會有更好的實現,能使這個視圖更容易實現。例如,如果 API 伺服器實現了資源嵌套,並且表明了人物與電影之間的關係,則我們可以通過以下方式讀取電影數據:

然而,一個純粹的 RESTful API 伺服器很可能不會像這般實現,並且我們需要讓我們的後端工程師為我們額外創建這個自定義的端點。這就是擴展 RESTful API 的現實——我們不得不添加自定義端點,以有效滿足不斷增長的客戶端需求。然而管理像這樣的自定義端點是很困難的一件事。

現在來看看 GraphQL 的實現方式。伺服器端的 GraphQL 包含了自定義端點的思想,並將其運用到極致。伺服器將只是單個端點,而通道不再重要。如果我們通過 HTTP 執行此操作,那麼 HTTP 方法肯定也不重要。假設我們有單個 GraphQL 端點通過 HTTP 暴露在 。

由於我們希望在單次往返中請求我們所需的數據,所以我們需要一種表達我們對伺服器端完整數據需求的方式。我們使用 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 API 託管在 https://github.com/graphql/swapi-graphql。可以去嘗試使用它構建我們的人物數據對象。後續我們探討的 API 可能會有一些細微的變動,但下面是你可以使用這個 API 來查看我們對視圖數據請求的正式查詢(以Darth Vader為例):

這個請求定義了一個非常接近視圖的響應結構,記住,我們是在一次往返中獲得的所有這些數據。

GraphQL 靈活性的代價

完美的解決方案實際並不存在。由於 GraphQL 過於靈活,將會帶來一些明確的問題和擔憂。

GraphQL 易導致的一個重要威脅是資源耗盡攻擊(亦稱為拒絕服務攻擊)。GraphQL 伺服器可能會受到超複雜查詢的攻擊,這將耗盡伺服器的所有資源。查詢深度嵌套關係(用戶 -> 朋友 -> 朋友...),或者使用欄位別名多次查詢相同的欄位非常容易。資源耗盡攻擊並不是特定於 GraphQL 的場景,但是在使用 GraphQL 時,我們必須格外小心。

我們可以在這裡做一些緩和措施。比如,我們可以提前對查詢進行成本分析,並對可以使用的數據量實施某種限制。我們也可以設置超時時間來終結需要過長時間解析的請求。此外,由於 GraphQL 只是一個解析層,我們可以在 GraphQL 下的更底層處理速率限制。

如果我們試圖保護的 GraphQL API 端點並不公開,而是為了供我們自己的客戶端(網路或移動設備)內部使用,那麼我們可以使用白名單方法和預先批准伺服器可以執行的查詢。客戶端可以要求伺服器只執行使用查詢唯一標識符預先批准的查詢。據說 Facebook 採用的就是這種方法。

認證和授權是在使用 GraphQL 時需要考慮的其他問題。我們是在 GraphQL 解析過程之前,之後還是之間處理它們?

為了解答這個問題,你可以將 GraphQL 視為在你自己的後端數據獲取邏輯之上的 DSL(領域特定語言)。我們只需把它當作可以在客戶端和我們的實際數據服務(或多個服務)之間放置的一個中間層。

然後將認證和授權視為另一層。GraphQL 在實際的身份驗證或授權邏輯的實現中並無用處,因為它的意義並不在於此。但是,如果我們想將這些層放置於 GraphQL 之後,我們可以使用 GraphQL 來傳遞客戶端和強邏輯之間的訪問令牌。這與我們通過 RESTful API 進行認證和授權的方式非常相似。

GraphQL 另一項更具挑戰性的任務是客戶端的數據緩存。RESTful API 由於其字典性質而更容易緩存。特定地址標識特定數據。我們可以使用地址本身作為緩存鍵。

使用 GraphQL,我們可以採取類似的基本方式,將查詢文本用作緩存其響應的鍵。但是這種方式有著諸多限制,而且不是很有效率,並且可能導致數據一致性的問題。多個 GraphQL 查詢的結果很容易重疊,而這種基礎的緩存方式無法解決重疊的問題。

對於這個問題有一個很巧妙的解決方案,那就是使用圖查詢表示圖緩存。如果我們將 GraphQL 查詢響應範式化為一個扁平的記錄集合,給每條記錄一個全局唯一的 ID,那麼我們就可以緩存這些記錄,而不是緩存完整的響應。

然而這不是一個簡單的過程。記錄將會相互引用,我們將在其中管理循環圖。操作和讀取緩存需要遍歷查詢。儘管我們需要編寫一個中間層來處理這些緩存邏輯,但是這種方式總體上比基於響應的緩存更有效率。Relay.js 便是一個採用這種緩存策略並在內部實現自動管理的框架。

對於 GraphQL,或許我們應該關心的最重要的問題是通常被稱為 N+1 SQL 查詢的問題。GraphQL 查詢欄位被設計為獨立的功能,並且使用資料庫中的數據解析這些欄位可能會導致對已解析欄位產生新的資料庫請求。

對於簡單的 RESTful API 端點邏輯,可以通過增強結構化的 SQL 查詢來分析,檢測和解決 N+1 問題。對於 GraphQL 動態解析的欄位,就沒那麼簡單了。好在 Facebook 開創了一個可行的解決方案:DataLoader。

顧名思義,DataLoader 是一個可用於從資料庫讀取數據並使其可用於 GraphQL 解析函數的工具程序。我們可以使用 DataLoader 而不是直接使用 SQL 查詢從資料庫中讀取數據,而 DataLoader 將作為我們的代理,以減少我們發送到資料庫的實際 SQL 查詢。

DataLoader 的原理是使用批處理和緩存的組合。如果相同的客戶端請求導致需要向資料庫請求多個數據,則可以使用 DataLoader 來合併這些請求,並從資料庫批量載入其響應。DataLoader 還將緩存響應以使其可用於相同資源的後續請求。

謝謝閱讀!


點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 京程一燈 的精彩文章:

Webpack的精彩世界
SQL 教程:如何編寫更佳的查詢
這幾個控制台API能幫你調試Web應用
Node.js創造者,Ryan Dahl專訪
澄清對AMP的十個誤解

TAG:京程一燈 |