【譯】GraphQL vs. REST

WilsonWu發表於2019-03-04

兩種通過 HTTP 傳送資料的方式:區別在哪裡?

GraphQL 常常被認為是一種全新的 API 方式。你可以通過傳送一次查詢請求便獲得所需要的資料,而不是通過伺服器嚴格定義的請求終端。GraphQL 確實有這樣的變革能力,一個團隊在採用 GraphQL 後能夠使得前端和後端的合作變得比之前更流暢。然而在實際操作中,兩種技術都通過傳送 HTTP 請求獲取結果,而且 GraphQL 使用了 REST 模型中的很多內建元素

那麼從技術層面來講它們的本質到底是什麼?這兩款 API 範例的相似處和區別都有哪些?我在文章最後將會宣告 GraphQL 和 REST 的區別並不是很大,但 GraphQL 其本身的一些小的改變使得為開發和自定義一個 API 帶來了巨大的區別。

那麼言歸正傳,我們會先指出 API 的一些性質,然後我們會討論 GraphQL 和 REST 是如何處理它們的。

資源

REST 的核心理念就是資源。每個資源都由一個 URL 定義,然後通過向指定 URL傳送 GET 請求來獲取資源。目前大部分 API 會得到的一個 JSON 響應。這個請求和響應如下:

GET /books/1

{
  "title": "Black Hole Blues",
  "author": {
    "firstName": "Janna",
    "lastName": "Levin"
  }
  // ... more fields here
}複製程式碼

注意:在以上例項中,有的 REST APIs 會把 “author” 當成獨立資源返回。

在 REST 中需要注意的是,資源的型別和你獲取資源的方法是緊密相關的。當使用以上 REST 資料時,你可能會把它當成是 book 的一個終端。

GraphQL 在這方面就相當不一樣了,因為在 GraphQL 裡這兩個概念是完全分開的。在你的模版裡可能會有 ‘Book’ 和 “author” 兩種型別:

type Book {
  id: ID
  title: String
  published: Date
  price: String
  author: Author
}

type Author {
  id: ID
  firstName: String
  lastName: String
  books: [Book]
}複製程式碼

注意在這裡我們對可獲得的資料型別進行了描述,但這個描述並沒有告訴你每個物件是如何從客戶端獲得的。這就是 REST 和 GraphQL 的核心區別之一 —— 對某一指定資源的描述不一定要和獲取的方式相結合。

如果想要真正得到到某一本書或者其作者的資訊,我們需要在我們現有的模式中創造一個 ‘Query’ 型別:

type Query {
  book(id: ID!): Book
  author(id: ID!): Author
}複製程式碼

現在我們可以傳送一個類似於 REST 的請求,不過這次是使用 GraphQL:

GET /graphql?query={ book(id: "1") { title, author { firstName } } }

{
  "title": "Black Hole Blues",
  "author": {
    "firstName": "Janna",
  }
}複製程式碼

很好,現在我們有成果了!即使雙方都使用 URL 來傳送請求並返回相同的 JSON 結構作為回應,我們還是能馬上看出 GraphQL 和 REST 之間的區別。

首先,我們能看出 GraphQL 查詢的 URL 詳細指出了我們所尋找的資源以及我們所關心的欄位。而且 API 的使用者決定是否需要包括有關 ‘author’ 的資源,而不是由伺服器端的程式碼來決定。

但最重要的是,資源的身份以及 Book 和 Author 的概念和獲取的方式無關。我們實際上可以使用多種不同的請求來獲取同一本書的不同欄位。

總結

我們已經找到了一些相似和不同的地方:

  • 相同: 都擁有資源這個概念,而且都可以指定資源的身份
  • 相同: 都能通過 HTTP GET 和一個 URL 來獲取資訊
  • 相同: 請求的返回值都是 JSON 資料
  • 不同: 在 REST 中,你所訪問的終端就是所需物件的身份,在 GraphQL 中,物件的身份和獲取的方式是獨立存在的
  • 不同: 在 REST 中,資源的形式和大小是由伺服器所決定的。在 GraphQL 中,伺服器宣告哪些資源可以獲得,而客戶端會對其所需資源作出請求。

好吧,如果你之前使用過 GraphQL 和/或 REST的話這些看上去很基礎。如果你之前沒用過 GraphQL,你可以使用 Launchpad 來試試這個例項 。這是一個用於在瀏覽器中創造和探索 GraphQL 例項的工具。

URL 路徑 vs GraphQL 模版

一款無法正確預測結果的 API 是沒有實際用途的。當你使用一款 API 的時候,大部分情況下會把它當做程式的某一部分去使用它,這款程式會知道可以呼叫什麼 API,以及 API 的結果是什麼。這樣程式才能運用好 API 返回的結果。

所以一款 API 最重要的一個特點就是去描述它到底能得到什麼。你在讀 API 文件的時候恰恰就是為了瞭解這些。現在通過使用 GraphQL 的內部描述特點或者使用類似 Swagger 這種適用於 REST 模板系統的工具,我們可以採用程式設計的方式來獲取這方面的資訊。

目前的 REST API 通常被形容為一連串的端點:

GET /books/:id
GET /authors/:id
GET /books/:id/comments
POST /books/:id/comments複製程式碼

所以你可以將此 API 的“形態”描述為線性 —— 因為你可以接觸一連串的資訊。當你想要獲取或者儲存資訊的時候,最先想到的問題就是“我應該使用哪一個終端”?

而在 GraphQL 中,就像我們之前提到的,你並不是使用一系列 URL 來驗證 API 可以獲得有哪些資訊,而是使用 GraphQL 的模板:

type Query {
  book(id: ID!): Book
  author(id: ID!): Author
}

type Mutation {
  addComment(input: AddCommentInput): Comment
}

type Book { ... }
type Author { ... }
type Comment { ... }
input AddCommentInput { ... }複製程式碼

將它和 REST 中請求相同資料集的請求路徑做對比時,有幾點有趣的地方。首先,在區分讀取和寫入時,GraphQL 使用的是 Mutation 和 Query 這兩種不同的初始型別,而不是通過對同一 URL 傳送兩種不同的 HTTP 術語。在 GraphQL 文件中,你可以使用關鍵字來選擇你所傳送的操作:

query { ... }
mutation { ... }複製程式碼

如果想要了解更多有關查詢語言的細節,請閱讀我之前寫的文章, “對 GraphQL 查詢的分析”。

你可以看出 Query 型別中的欄位和我們之前所寫的 REST 路徑正好重合。這是因為此型別是我們資料的切入點,所以這在 GraphQL 中是和終端 URL 幾乎相同的一個概念。

你從 GraphQL API 中獲取最初資源的方式和使用 REST 的方法類似 —— 都是通過傳遞一個名字和一些引數 —— 但最大的不同之處是在這之後你會做什麼。你可以用 GraphQL 傳送一個複雜的請求並通過與模板之間的關係來獲取額外的資料。但在 REST 中,你需要通過傳送多個請求來使用相關資料去構造最初的回應,或者在 URL 中包含特殊引數來修改響應的結果。

結論

在 REST 中,可獲得資料的空間是由一系列線性的終端來描述的,而在 GraphQL 中是通過使用有關聯的模板:

  • 相同: REST API 中的一列終端和 GraphQL API 中的 Query 和 Mutation 類的欄位很像,都是資料的切入點。
  • 相同: 兩種 API 都可以區分資料的讀取和寫入。
  • 不同: 在 GraphQL 中,你可以使用由模板定義的關係,通過傳送一次請求從初始點一直走到相關資料。然而在 REST 中,你必須要使用多個終端來獲取相關資源。
  • 不同: 在 GraphQL 中,除了在每個請求的根源處所能獲取的型別都是 Query 類外,Query 的欄位和其他類的欄位沒有本質區別。比方說,你可以在 Query 的每個欄位裡放一個引數。而在 REST 中,巢狀的URL裡沒有第一類這個概念。
  • 不同: 在 REST 中,你通過將 HTTP 術語 GET 改為 POST 來指定寫入,但在 GraphQL 裡需要改變請求裡的關鍵字

由於第一個相似點,很多人把 GraphQL 的 Query 類中的欄位當作“終端”或者“請求”。雖然這的確是一個合理的比較,但這種理解可能會誤導別人認為 Query 類和其他類的工作方式不同,這種理解是錯誤的。

路徑處理器 vs Resolvers

當你呼叫一款 API 的時候到底發生了什麼?通常情況下 API 會在伺服器端收到請求後執行一段程式碼。這類程式碼可能會進行計算,也可能是從資料庫中載入資料,甚至會使用另一款 API 或做其他事。重要的是你不需要了解它在內部到底做了了什麼。不過 REST 和 GraphQL 這兩款 API 都具備非常標準化的內部執行方式,通過比較它們內部的執行區別,我們可以找出這兩款 API 基礎層面的不同點。

在接下來的對比中我會使用 JavaScript,因為這是我最熟悉的語言。不過你當然可以用其他語言去實現 REST 或者 GraphQL。我會省略設定伺服器的步驟,因為這不是重點。

來看看這個用 experss 寫的 hello world 例子,express 是 Node 裡很火的 API庫 之一。

app.get(`/hello`, function (req, res) {
  res.send(`Hello World!`)
})複製程式碼

我們首先建立了一個能夠返回hello world字串符的/hello 終端。通過這個例子中我們可以得知使用 REST API 來寫伺服器時一個 HTTP 請求的生命週期:

  1. 伺服器接收請求並解析 HTTP 術語 (這個例子中術語為 ‘GET’)和其 URL
  2. API 庫將術語和路徑相結合並在伺服器程式碼中找到與之相匹配的函式
  3. 函式執行並返回結果
  4. API 庫將結果序列化與響應程式碼和資料頭相結合,最終傳送給客戶端

GraphQL 的工作方式極為相似,對於同一個 hello world 的例子來說兩者幾乎相同:

const resolvers = {
  Query: {
    hello: () => {
      return `Hello world!`;
    },
  },
};複製程式碼

就像你所看到的,我們將函式和一個類別中的欄位相呼應,為指定的 URL 提供一個處理函式。在這個例子中,‘hello’ 是 ‘Query’ 中的一個欄位。在 GraphQL 中,這種對欄位進行操作的函式被稱為 resolver

我們需要用 Query 來傳送請求:

query {
  hello
}複製程式碼

當伺服器接收到 GraphQL 的請求會執行以下步驟:

  1. 伺服器接收請求並開始解析 GraphQL 的請求
  2. 此 Query 的每個欄位會被仔細分析來找出有哪些 resolver 函式會被使用
  3. 函式執行並返回結果
  4. GraphQL 庫和伺服器將返回結果和回應相結合,最終得到和 Query 形態相匹配的結果

所以你最終得到的結果為:

{ "hello": "Hello, world!" }複製程式碼

但這裡有個小技巧,我們實際上可以連續訪問欄位兩次!

query {
  hello
  secondHello: hello
}複製程式碼

在這個例子中出現了相同的生命週期,但由於我們使用化名對同一個欄位傳送了兩次請求,hello 的 resolver 實際上被使用了兩次。這個例子很牽強,但重點是我們可以對同一請求中對多個欄位進行操作,而且在一個 query 中我們也可以對單個欄位進行多次使用。

為了進行補充,以下是一個巢狀在一起的 resolvers 例子:

{
  Query: {
    author: (root, { id }) => find(authors, { id: id }),
  },
  Author: {
    posts: (author) => filter(posts, { authorId: author.id }),
  },
}複製程式碼

這些 resolvers 可以用來對 query 進行補充:

query {
  author(id: 1) {
    firstName
    posts {
      title
    }
  }
}複製程式碼

所以即使這些 resolvers 是平級的,由於它們可以和多種型別相結合,你可以在巢狀的 query 裡將這些 resolvers 連在一起使用。如果想了解 GraphQL 是如何執行工作的,請閱讀以下文章“詳解 Graph QL”

來看看如何使用完整的例子配合不同的請求來進行測試!

圖解:對資源進行獲取的 REST 多次請求 vs GraphQL 的一次請求

結論

最終我們可以得知,REST 和 GraphQL API 都可以在網路中通過不同方式使用函式。如果你對如何搭建 REST API 很熟悉,那麼使用 GraphQL API 應該不會很不一樣。不過 GraphQL 有很大的優勢,因為你可以使用它去執行多個相關函式,而且全程不需要多次請求往返。

  • 相同: REST的終端和 GraphQL 的欄位都會在伺服器端執行函式
  • 相同: 兩者本質上都需要依靠框架和庫來使用和處理網路模板。
  • 不同: 在 REST 中,每次請求通常只使用一個路徑處理函式。在 GraphQL 中,同一 Query 可以使用多個 resolver 來使用多個資源創造巢狀在一起的回應。
  • 不同: 在 REST 中,你可以自己創造每個回應的形式。在 GraphQL 中,回應的模式通過 GraphQL 的執行庫來與請求的形式相匹配。

總而言之,你可以將 GraphQL 當成是可以在一次請求裡執行多個終端的系統,就像是重複使用的 REST。


這些意味著什麼?

我們無法在此文章中對所有細節做出詮釋,比如物件識別、超媒體以及快取。我以後可能會再討論這些問題,但我想讓你明白的是,通過了解 API 的基本知識點可得知,REST 和 GraphQL 工作時所使用的基礎觀念是十分相似的。

我覺得兩者之間的區別反而成為了 GraphQL 的優勢。特別是給予使用者構建多個 resolver 函式的功能非常炫酷,而且也可以傳送一個複雜的請求來一次性得到多種資源,整個過程是可預測的。這個特點避免了 API 的使用者為了構建某個回應形式而去使用多個終端,同時也避免了處理額外不需要的資料。

然而,GraphQL 目前還沒有 REST 那麼多的工具和擴充套件。比方說,你無法對 GraphQL 的結果使用 HTTP 的快取方式。但目前社群方面正在努力打造更好的工具和框架,而且你可以使用類似 Apollo clientRelay 這類快取工具。

如果有更多有關對比 REST 和 GraphQL 的想法,請積極留言!


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章