安息吧 REST API,GraphQL 長存

首發於衆成翻譯javascript


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

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

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

我喜歡「始終以 WHY 開頭」,因此讓咱們開始吧。github

摘要:爲何咱們須要 GraphQL ?

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

  • 須要進行屢次往返以獲取視圖所需的數據:使用 GraphQL,你能夠隨時經過單次往返服務器獲取視圖所需的全部初始數據。要使用 REST API 實現相同的功能,咱們須要引入難以管理和擴展的非結構化參數和條件。
  • 客戶端依賴於服務端:客戶端使用 GraphQL 做爲請求語言:(1) 消除了服務器對數據形狀或大小進行硬編碼的須要,(2) 將客戶端與服務端分離。這意味着咱們能夠把客戶端與服務端分離開來,單獨進行維護和改進。
  • 糟糕的前端開發體驗:使用 GraphQL,開發人員能夠聲明式地來表達其用戶界面的數據需求。他們聲明他們須要什麼數據,而不是如何獲取它。UI 須要哪些數據,與開發人員在 GraphQL 中聲明該數據的方式之間存在緊密的聯繫。

本文將詳細介紹 GraphQL 如何解決全部這些問題。shell

在咱們開始以前,若是你還不熟悉 GraphQL,能夠從簡單的定義開始。數據庫

什麼是 GraphQL ?

GraphQL 是一門語言。若是咱們將 GraphQL 嵌入某個軟件應用,該應用可以聲明式地將任意必需的數據傳遞給一樣使用 GraphQL 的後端數據服務。json

就像一個小孩能夠很快學會一門新的語言 - 而成年人則相對沒那麼容易學會 - 從頭開始使用 GraphQL 會比引入 GraphQL 到一個成熟的應用中更容易。後端

要讓一個數據服務可以使用 GraphQL,咱們須要實現一個運行時層,並將其暴露給想要與服務端通訊的客戶端。將服務器端的這一層看做簡單的 GraphQL 語言的翻譯器,或者表明數據服務的 GraphQL 代理。GraphQL 不是存儲引擎,因此它並非一個獨立的解決方案。這就是爲何咱們不能僅有一個 GraphQL 的服務器,咱們還須要實現一個翻譯運行時。

這個抽象層能夠用任意語言編寫,它定義了一個通用的基於圖形的 schema 來發布它所表明的數據服務的功能。使用 GraphQL 的客戶端程序能夠經過其功能查詢該 schema。這種方法使得客戶端與服務端解耦,並容許其二者獨立開發和擴展。

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 運行時中定義一個 schema,而且該 schema 能被客戶端所使用。

這個 schema 基本能夠視爲一個功能文檔,其中列出了客戶端能夠請求 GraphQL 層的全部查詢。由於咱們在這裏使用的是節點的圖,因此使用 schema 會帶來一些靈活性。該 schema 大體表示了 GraphQL 層能夠響應的範圍。

還不夠清楚?咱們能夠說 GraphQL 其實根本就是:REST API 的接替者。因此讓我回答一下你最有可能問的問題。

REST API 有什麼問題?

REST API 最大的問題是其多端點的本質。這要求客戶端進行屢次往返以獲取數據。

REST API 一般是端點的集合,其中每一個端點表明一個資源。所以,當客戶端須要獲取多個資源的數據時,須要對 REST API 進行屢次往返,以將其所需的數據放在一塊兒。

在 REST API 中,沒有客戶端請求語言。客戶端沒法控制服務器返回的數據。沒有任何語言能夠這樣作。更確切地說,可用於客戶端的語言很是有限。

例如,READ REST API 端點多是

  • GET /ResouceName ——從該資源獲取全部記錄的列表;
  • GET /ResourceName/ResourceID ——獲取該 ID 標識的單條記錄。

例如,客戶端不能指定爲該資源中的記錄選擇哪些字段。這意味着 REST API 服務將始終返回全部字段,而無論客戶端實際須要哪些。GraphQL 針對這個問題定義的術語是超量獲取不須要的信息。這對客戶端和服務器而言都是網絡和內存資源的浪費。

REST API 的另外一大問題是版本控制。若是你須要支持多個版本,那一般意味着須要新的端點。而在使用和維護這些端點時會致使諸多問題,而且這可能致使服務器上的代碼冗餘。

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

GraphQL如何作到這一點?

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

  • GraphQL schema 是強類型 schema。要建立一個 GraphQL schema,咱們要定義具備類型字段。這些類型能夠是原語的或者自定義的,而且 schema 中的全部其餘類型都須要類型。這種豐富的類型系統帶來豐富的功能,如擁有內省 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 數據可能相似於:

{
  "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 表示它的視圖的方式:

// 容器組件:
<PersonProfile person={data.person} ></PersonProfile>
// PersonProfile 組件:
Name: {person.name}
Birth Year: {person.birthYear}
Planet: {person.planet.name}
Films: {person.films.map(film => film.title)}

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

如今咱們來看看如何使用 RESTful API 請求這些數據。

咱們須要獲取單我的物的信息,而且假定咱們知道該人物的 ID,則 RESTful API 會將該信息暴露爲:

GET - /people/{id}

這個請求將返回給咱們該人物的姓名,出身年份和其餘有關信息。一個設計良好的 RESTful 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

一旦咱們獲取了來自服務器的全部6個響應,咱們即可以將它們組合起來,以知足咱們的視圖所需的數據。

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

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

固然,這只是 RESTful API 對於此數據的一個實現。可能會有更好的實現,能使這個視圖更容易實現。例如,若是 API 服務器實現了資源嵌套,而且代表了人物與電影之間的關係,則咱們能夠經過如下方式讀取電影數據:

GET - /people/{id}/films

然而,一個純粹的 RESTful API 服務器極可能不會像這般實現,而且咱們須要讓咱們的後端工程師爲咱們額外建立這個自定義的端點。這就是擴展 RESTful 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 API 託管在 https://github.com/graphql/swapi-graphql。能夠去嘗試使用它構建咱們的人物數據對象。後續咱們探討的 API 可能會有一些細微的變更,但下面是你可使用這個 API 來查看咱們對視圖數據請求的正式查詢(以Darth Vader爲例):

{
  person(personID: 4) {
    name,
    birthYear,
    homeworld {
      name
    },
    filmConnection {
      films {
        title
      }
    }
  }
}

這個請求定義了一個很是接近視圖的響應結構,記住,咱們是在一次往返中得到的全部這些數據。

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 還將緩存響應以使其可用於相同資源的後續請求。

謝謝閱讀!

相關文章
相關標籤/搜索