REST將會過時,而GraphQL則會長存

作者|Samer Buna

譯者|張衛濱

編輯|覃雲

來源:前端之巔

在實現 API 時,REST 是常用的方式,但是 RESTful API 有一些固有的不足,在本文中,作者介紹了 GraphQL 的基本原理和設計思想,以及如何藉助其靈活性來解決 RESTful API 面臨的問題,並且探討了靈活性所帶來的成本 。

在處理過多年的 REST API 之後,當我第一次學習到 GraphQL 以及它試圖要解決的問題時,我禁不住發了一條推文,這條推文的內容恰好就是本文的標題。

REST將會過時,而GraphQL則會長存

當然,那個時候,我只是抱着好奇的心態進行了嘗試,但現在,我相信當時的戲言卻正在變成現實。

請不要誤解,我並不是說 GraphQL 將會“殺死”REST 或類似的斷定。REST 可能永遠都不會消亡,就像 XML 永遠也不會滅亡一樣。我只是認爲 GraphQL 之於 REST,就像 JSON 之於 XML 那樣。

本文不會 100% 地鼓吹 GraphQL,這裏面會有一個很重要的章節討論 GraphQL 靈活性的代價。巨大的靈活性會帶來巨大的成本。

爲何要使用 GraphQL?

GraphQL 能夠非常漂亮地解決三個重要的問題:

  • 爲了得到視圖所需的數據,需要進行多輪的網絡調用:藉助 GraphQL,要獲取所有的初始化數據,我們僅需一次到服務器的網絡調用。要在 REST API 中達到相同的目的,我們需要引入非結構化的參數和條件,這是很難管理和擴展的。
  • 客戶端對服務端的依賴:藉助 GraphQL,客戶端會使用一種請求語言,該語言:1)消除了服務器端硬編碼數據形式或數量大小的必要性;2)將客戶端與服務端解耦。這意味着我們能夠獨立於服務器端維護和改善客戶端。
  • 糟糕的前端開發體驗:藉助 GraphQL,開發人員只需使用一種聲明式的語言表達用戶的界面數據需求即可。他們所描述的是需要什麼數據,而不是如何得到這些數據。在 GraphQL 中,UI 所需的數據以及開發人員描述數據的方式之間存在緊密的聯繫。

本文將會詳細闡述 GraphQL 是如何解決這些問題的。

在開始之前,有些人可能還不熟悉 GraphQL,所以我們先給出一個簡單的定義。

什麼是 GraphQL?

GraphQL 就是關於數據通信的。我們有客戶端和服務器端,它們之間都需要進行對話。客戶端需要告訴服務器端它需要什麼數據,而服務器端要以實際的數據滿足客戶端的需求,GraphQL 就位於這種通信之間。

REST將會過時,而GraphQL則會長存

你可能會問,我們爲什麼不能讓客戶端和服務器端直接通信呢?當然可以。

有多個原因促使我們在客戶端和服務器端之間放置一個 GraphQL 層。其中有個原因,可能也是最常見的,那就是效率。客戶端通常需要跟服務端要求多個資源,而服務端通常只能理解如何響應單個資源。所以,客戶端需要發起多輪請求,這樣才能收集到它需要的所有數據。

藉助 GraphQL,我們可以將這種多請求的複雜性轉移到服務端,讓 GraphQL 層來對其進行處理。客戶端對 GraphQL 層發起一個請求並且會得到一個響應,該響應中精確包含了客戶端所需的內容。

使用 GraphQL 還會有很多收益,比如,另外一個收益就是與多個服務進行通信的時候。如果你有多個客戶端要從多個服務請求數據的時候,位於中間的 GraphQL 層能夠簡化和標準化這種通信。儘管這並不是針對 REST API 的(因爲它也能很容易地實現),但是 GraphQL 運行時提供了一個結構化和標準化的方式來實現這一點。

REST將會過時,而GraphQL則會長存

客戶端不會與兩個不同的數據服務直接交互,我們現在可以讓客戶端與 GraphQL 層進行通信。然後,GraphQL 層會與兩個不同的數據服務進行通信。這樣的話,GraphQL 首先能夠將客戶端進行隔離,這樣它們就沒有必要使用多種語言進行通信了,同時,GraphQL 還會將一個請求轉換爲針對不同服務的多個請求,這些不同的服務可能會使用不同的語言編寫。

讓我們假設有三個不同的人,他們使用不同的語言並且具備不同類型的知識。假設你有一個問題,該問題需要組合這三個人的知識才能給出答案。如果你有一個能夠說這三門語言的翻譯器,那麼爲你的問題給出答案就會變得很容易。這其實就是 GraphQL 運行時所做的事情。

計算機還沒有足夠智能來回答任意的問題(至少目前還不可以),所以它們必須要在某些地方遵循一定的算法。這也是我們需要在 GraphQL 運行時上定義模式的原因,客戶端會使用該模式。

基本上來講,模式就是一個能力文檔,它包含了客戶端可以請求 GraphQL 層的所有問題的列表。在如何使用模式方面有一定的靈活性,因爲我們在這裏所討論的是一個節點圖。模式主要體現的是 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 服務本身之中,不管客戶端實際需要哪些字段,REST API 服務始終都會返回所有的字段。GraphQL 對該問題的描述術語是過度加載(over-fetching)不需要的信息。不管是對於客戶端還是對於服務器端,這都是網絡和內存資源的一種浪費。

REST API 的另外一個大問題是版本化。如果你需要支持多版本的話,通常意味着要有多個端點。在使用和維護這些端點的時候,這通常會導致更多的問題,而這也可能是服務端出現代碼重複的原因所在。

上文所述的 REST API 的問題恰好是 GraphQL 所要致力解決的。上面所述的這些肯定不是 REST API 的所有問題,我也不想過多討論 REST API 是什麼,不是什麼。我主要講的是基於資源的 HTTP 端點 API。這些 API 最終都會變成常規 REST 端點和自定義專門端點的混合品,其中自定義的專門端點大多都是因爲性能的原因而製作的。在這種情況下,GraphQL 能夠提供好得多的方案。

GraphQL 的魔力是如何實現的?

在 GraphQL 背後有着很多理念和設計決策,但是最爲重要的包括:

  • GraphQL 模式是強類型的模式。要創建 GraphQL 模式,我們需要按照類型來定義字段。這些類型可以是原始類型,也可以是自定義類型,模式中的任何內容都需要一個類型。這種豐富的類型系統允許實現豐富特性,比如具備內省功能的 API,以及爲客戶端和服務端構建強大的工具;
  • GraphQL 將數據以 Graph 的形式來進行表示,而數據很自然的表現形式就是圖。如果想要表示任意的數據,那正確的結構就是圖。GraphQL 運行時允許我們以圖 API 的方式來表示數據,該 API 能夠匹配數據的自然圖形形狀。
  • GraphQL 具有一個聲明式的特質來表示數據需求。GraphQL 爲客戶端提供了一種聲明式的語言,允許它們描述其數據需求。這種聲明式的特質圍繞 GraphQL 語言創建了心智模型,這與我們使用英語思考數據需求的方式非常接近,從而使得 GraphQL API 要比其他替代方案容易得多。

其中,正是由於最後一項理念,我個人認爲 GraphQL 將是一個遊戲規則的改變者。

這些都是高層級的理念,接下來讓我們看一些細節。

爲了解決多輪網絡調用的問題,GraphQL 將響應服務器變成了只有一個端點。從根本上來講,GraphQL 將自定義端點的思想發揮到了極致,將整個服務器變成了一個自定義的端點,使其能夠應對所有的數據請求。

與這個單端點概念相關的另一個重要理念是富客戶端請求語言(rich client request language),這是使用自定義端點所需的。如果沒有客戶端請求語言的話,單端點是沒有什麼用處的。它需要有一種語言來處理自定義的請求併爲該請求響應數據。

具備客戶端請求語言就意味着客戶端將會是可控的。客戶端能夠確切地請求它們想要的內容,服務器端則能夠確切地給出客戶端想要的東西。這解決了過度加載的問題。

在版本化方面,GraphQL 有一種非常有趣的做法,能夠徹底避免版本化的問題。從根本上來講,我們可以添加新的字段,而不必移除舊的字段,因爲我們有一個圖,從而可以通過添加節點來靈活地擴展這個圖。所以,我們可以爲舊 API 繼續保留其路徑,並引入新的 API,而不必將其標記爲新版本。API 只是不斷增長而已。

這對於移動端尤爲重要,因爲我們無法控制它們使用哪個版本的 API。一旦安裝之後,移動應用可能會多年一直使用相同版本的舊 API。在 Web 端,我們能夠很容易地控制 API 的版本,我們只需推送並使用新的代碼即可。對移動應用來說,這樣做就有些困難了。

還不完全相信嗎?我們通過一個具體的例子來對 GraphQL 和 REST 進行一對一的比較如何?

RESTful API 與 GraphQL API 的樣例

假設我們是開發人員,負責構建一個嶄新的用戶界面,展現《星球大戰》電影及其角色。

我們要構建的第一個 UI 界面很簡單:顯示每個《星球大戰》人物信息的視圖。例如,Darth Vader 以及他在哪些電影中出現過。這個視圖將會展現人物的姓名、出生年份、星球名稱以及他們所出現的電影的名字。

聽起來非常簡單,但實際上我們在處理三種不同類型的資源:人物(Person)、星球(Planet)以及電影(Film)。這些資源之間的關係很簡單,任何人都可以猜測數據的形狀。每個 Person 對象屬於一個 Planet 對象,同時每個 Person 對象有一個或多個 Film 對象。

這個 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:

// The PersonProfile Component:

Name: {person.name}

Birth Year: {person.birthYear}

Planet: {person.planet.name}

Films: {person.films.map(film => film.title)}

這是一個非常簡單的樣例,《星球大戰》的體驗可能會爲我們帶來一些幫助,UI 和數據的關係是非常清晰的。UI 用到了 JSON 數據對象中所有的“key”。

現在,我們看一下如何使用 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

從服務器端得到這六個響應之後,我們就可以就可以將它們組合起來以滿足視圖的數據需求。

爲了滿足一個簡單 UI 的數據需求,我們發起了六輪請求,除此之外,我們在這裏的方式是命令式的。我們需要給出如何獲取數據以及如何處理數據使其滿足視圖需求的指令。

如果你想要明白我的真實含義的話,那麼你可以自行嘗試一下。在 http://swapi.co/ 站點上,《星球大戰》的數據目前有一個 RESTful API。你可以去那裏嘗試構建我們的人員數據對象。所使用的 key 可能會略有差異,但是 API 端點是相同的。你需要六次 API 調用,除此之外,在這個過程中還會過度加載視圖並不需要的信息。

當然,這僅僅是該數據的一種 RESTful API 實現方式而已。我們可能還會有更好的實現方式,讓視圖編寫起來更加容易。例如,如果 API 服務器的實現能夠嵌套資源並理解人員和電影之間的關聯關係,那麼我們通過該 API 來讀取電影數據:

GET - /people/{id}/films

但是,純粹的 RESTful API 可能並不會實現這些,我們需要要求後端工程師爲我們創建這個自定義的端點。這就是 RESTful API 進行擴展的現實:我們只能添加自定義端點來有效滿足不斷增長的客戶端需求。管理這樣的自定義端點是非常困難的。

現在,我們再來看一下 GraphQL 的方式。GraphQL 在服務端擁抱了自定義端點的理念,並將其發揮到了極致。服務器只有一個端點,至於通道則無關緊要。如果你通過 HTTP 來實現的話,HTTP 方法當前也是無關緊要的。我們假設有一個通過 HTTP 暴露的 GraphQL 端點,其地址爲/graphql。

因爲想要通過一輪請求就將數據獲取到,所以需要有一種方式來向服務器表達完整的數據需求。我們通過一個 GraphQL 查詢來實現這一點:

GET or POST - /graphql?query={...}

GraphQL 查詢只是一個字符串,但是它需要包含我們所需數據的所有片段。此時,聲明式的方式就能發揮作用了。

在中文中,會這樣描述我們的數據需求:我們需要一個人員的姓名、出生年份、星球的名字以及所有相關電影的名稱。在 GraphQL 中,這會翻譯爲:

{

person(ID: ...) {

name,

birthYear,

planet {

name

},

films {

title

}

}

}

再次閱讀一下使用中文表達的需求,然後將其與 GraphQL 查詢進行對比。你會發現,它們非常接近。現在,對比一下這個 GraphQL 查詢和我們開始時所見到的原始 JSON 數據。GraphQL 查詢與 JSON 數據的格式完全相同,唯一的差異在於”值(value)“部分。如果我們將其想象爲問題和答案的關係,所提出的問題就是將答案語句刨除了答案值。

如果答案語句是:

距離太陽最近的行星是水星。

對該問題進行表述時,一種非常好的方式就是將相同語句的答案部分刨除掉:

距離太陽最近的行星是什麼?

同樣的關係可以用到 GraphQL 查詢中。以 JSON 響應爲例,我們將其中的”答案“部分(也就是 JSON 中的值)移除掉,最終就能得到一個 GraphQL 查詢,它能夠非常恰當地表述 JSON 響應所對應的問題。

現在,對比一下 GraphQL 查詢和我們爲數據所定義的聲明式 React UI。GraphQL 查詢中的所有內容都用到了 UI 之中,而 UI 中用到的所有內容也都出現在了 GraphQL 查詢中。

這是 GraphQL 非常強大的思想模型。UI 知道它所需要的確切數據,抽取需求相對是非常容易的。生成 GraphQL 是一項非常簡單的任務,只需將 UI 所需的數據直接抽取爲變量即可。

如果我們將這個模型反過來,它依然非常強大。有一個 GraphQL 查詢之後,我們就能知道如何在 UI 中使用它的響應,這是因爲查詢與響應有着相同的”結構“。我們不需要探查響應就能知道如何使用它,我們甚至不需要任何關於該 API 的文檔。它都是內置的。

https://github.com/graphql/swapi-graphql 站點將《星球大戰》的數據託管爲 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 端點不是公開的,也就是隻用於客戶端(Web 或移動)的內部使用,那麼可以使用白名單的方式,服務器只能執行預選得到許可的查詢。客戶端可以使用一個唯一的查詢標識符,請求服務器執行預先許可的查詢。Facebook 似乎採用了這種方式。

在使用 GraphQL 時,另外一個需要考慮的地方就是認證和授權。在 GraphQL 查詢解析之前、之後或解析的過程中,我們需要在這方面進行處理嗎?

要回答這個問題,我們可以將 GraphQL 視爲你自己的後端數據獲取邏輯之上的一個 DSL(領域特定語言)。它只是一個分層,我們可以將其放到客戶端和實際的數據服務(或多個服務)之間。

我們將認證和授權視爲另一個分層。GraphQL 並不會爲實際的認證和授權邏輯提供幫助。它也不是做這個的。但是如果你想要將這些分層放到 GraphQL 之後的話,我們可以使用 GraphQL 在客戶端和限制邏輯之間傳輸訪問 token。這非常類似於在 RESTful API 認證和授權時,我們所採取的做法。

在客戶端數據緩存方面,GraphQL 也面臨着更多的挑戰。RESTful API 由於其字典(dictionary)的特性,因此更容易進行緩存。對應的地址給出數據,因此我們可以使用這個地址本身作爲緩存的 key。

在使用 GraphQL 的時候,我們可以採用類似的基本方法,使用查詢的文本作爲 key 來緩存它的響應。但是,這種方式是有一定限制的,效率不高,並且會導致數據一致性方面的問題。多個 GraphQL 查詢的結果很容易出現重疊,這種基本的緩存機制不能解決重疊的問題。

但是,在這方面有一個很好的解決方案。圖查詢(Graph Query)意味着圖緩存。如果我們將 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 還會對答案進行緩存,後續的問題如果請求相同的資源的話,就可以使用緩存了。

原文鏈接

https://medium.freecodecamp.org/rest-apis-are-rest-in-peace-apis-long-live-graphql-d412e559d8e4

相关文章