【譯】對比GraphQL與REST——兩種HTTP API的差異

原文標題:GraphQL vs. REST Two ways to send data over HTTP: What’s the difference?
原文地址:https://dev-blog.apollodata.com/graphql-vs-rest-5d425123e34b
作者:Sashko Stubailo
翻譯:Fanny

GraphQL目前被認為是革命性的API工具,因為它可以讓客戶端在請求中指定希望得到的數(shù)據(jù),而不像傳統(tǒng)的REST那樣只能呆板地在服務(wù)端進行預定義。這樣它就讓前、后端團隊的協(xié)作變得比以往更加的通暢,從而能夠讓組織更好地運作。而實際上,GraphQL與REST都是基于HTTP進行數(shù)據(jù)的請求與接收,而且GraphQL也內(nèi)置了很多REST模型的元素在里面。

那么在技術(shù)層面上,GraphQL和REST這兩種API模型到底有什么異同呢?我的觀點是,他們歸根到底其實沒多大區(qū)別,只不過GraphQL做了一些小改進,使得開發(fā)體驗產(chǎn)生了較大的改變。

我會從API的各個組件分別來討論GraphQL和REST都分別是如何處理的。

資源(Resources)

REST的核心思想就是資源,每個資源都能用一個URL來表示,你能通過一個GET請求訪問該URL從而獲取該資源。根據(jù)當今大多數(shù)API的定義,你很有可能會得到一份JSON格式的數(shù)據(jù)響應(yīng),整個過程大概是這樣:

GET /books/1
{
  "title": "Black Hole Blues",
  "author": { 
    "firstName": "Janna",
    "lastName": "Levin"
  }
  // ... more fields here
}

注:上面的例子里的"author"也會作為一個單獨的資源在其他REST API中被用到

需要注意的是,在REST中,一個資源的種類與你獲取它的方式是耦合的,比如上面這個例子中的API就可以稱之為“book端點”(book endpoint)。

在這一點上GraphQL就大為不同,因為在GraphQL里這兩個概念是完全分離的。比如說在你的schema定義中,你可能會有BookAuthor兩個類型(type):

type Book {
  id: ID
  title: String
  published: Date
  price: String
  author: Author
}
type Author {
  id: ID
  firstName: String
  lastName: String
  books: [Book]
}

注意這里我們雖然定義了數(shù)據(jù)類型,但卻不知道該如何獲取這些數(shù)據(jù)。這是REST與GraphQL的一個核心差異:資源的描述信息與其獲取方式相分離。

如果要去訪問某個特定的book或者author資源,我們需要在schema中創(chuàng)建一個Query類型:

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

然后我們就可以像REST那樣發(fā)送請求了:

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

雖然都是通過請求某個URL來得到相同的響應(yīng),但這里我們已經(jīng)看到GraphQL與REST的差異之處了。

首先,我們看到GraphQL的URL請求里面指定了我們所需要的資源以及在該資源中我們所關(guān)心的字段。另外,我們是主動請求得到與book相關(guān)的author數(shù)據(jù)的,而不是服務(wù)端替我們決定的。

最重要的是,在請求中我們不需要關(guān)心資源的主鍵和資源之間的關(guān)系定義,我們可以通過除id以外的其他字段來獲取到相同的Book資源。

小結(jié)

現(xiàn)在我們知道的異同點有:
相同點:都有資源這個概念,而且都能通過ID去獲取資源。
相同點:都可以通過HTTP GET方式來獲取資源
相同點:都可以使用JSON作為響應(yīng)格式
差異點:在REST中,你所訪問的路徑就是該資源的唯一標識(ID);在GraphQL中,該標識與訪問方式并不相關(guān)
差異點:在REST中,資源的返回結(jié)構(gòu)與返回數(shù)量是由服務(wù)端決定;在GraphQL,服務(wù)端只負責定義哪些資源是可用的,由客戶端自己決定需要得到什么資源

如果你已經(jīng)用過GraphQL和REST,以上這些對你來說肯定相當簡單。如果你之前沒有用過GraphQL,你可以在到這里來實際體驗一下。

路由(URL Route) vs. GraphQL Schema

一個具有可預見性的API才是好的API。因為你通常會把一個API當做程序的一部分來使用,所以你必須要知道它需要接收什么參數(shù)并預期能夠獲取到什么樣的結(jié)果。

這時候,對API的訪問描述信息就顯得很重要。通常我們會通過閱讀API文檔來獲取信息,但通過GraphQL的Introspection機制、以及Swagger這樣的REST API工具,這些信息就能可以自動獲取到。

如今的REST API通常會由一系列的URL端點組成:

GET /books/:id
GET /authors/:id
GET /books/:id/comments
POST /books/:id/comments

你可以把這種API的形態(tài)稱之為線性結(jié)構(gòu)——因為這就是一個列表嘛。當你要獲取數(shù)據(jù)時,第一個事情就是搞清楚你要訪問的是哪個端點。

而在GraphQL中——其實在上一節(jié)里你也看到了——可以通過查看GraphQL schema獲得相關(guān)信息:

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會使用類似GET、POST這樣的動詞去請求相同的URL來表示這到底是一個讀操作還是寫操作,而GraphQL會使用不同的預定義類型:Mutation和Query。在GraphQL請求中,你可以通過不同的關(guān)鍵字進行不同的操作:

query { ... }
mutation { ... }

如果你想知道更多關(guān)于query的用法,請看我之前寫的文章“The Anatomy of a GraphQL Query”.

這里的Query類型定義與上面的REST路由是完全契合的,同樣表示了數(shù)據(jù)的訪問入口,因此這是GraphQL中最能與REST的URL端點所對應(yīng)的概念。

如果是對資源的簡單查詢,GraphQL與REST是類似的,都是通過指定資源的名稱以及相關(guān)參數(shù)來取得,但不同的是,在GraphQL中,你可以根據(jù)資源之間的關(guān)聯(lián)關(guān)系來發(fā)起一個復雜請求,而在REST中你只能定義一些特殊的URL參數(shù)來獲取到特殊的響應(yīng),或者是通過發(fā)起多個請求、再自行把響應(yīng)得到的數(shù)據(jù)進行組裝才行。

小結(jié)

REST對數(shù)據(jù)的描述形式是一連串的URL端點,而GraphQL則是由相互之間有所關(guān)聯(lián)的schema組成。
相同點:REST API的URL端點列表與GraphQL的Query/Mutation中的字段類似,都表示數(shù)據(jù)的訪問入口。
相同點:都能用不同的方式描述一個API請求到底是讀操作還是寫操作。
差異點:GraphQL讓你可以通過一個資源入口訪問到關(guān)聯(lián)的其他資源,只要事先在schema中定義好資源之間的關(guān)系即可;而REST則提供了多個URL端點來獲取相關(guān)的資源。
差異點:在GraphQL中,Query類型可以在一個請求的根節(jié)點中被訪問,除此以外它跟其他類型沒有區(qū)別,比如你也可以對一個query中的字段添加參數(shù)。而在REST中,即使響應(yīng)結(jié)果是嵌套關(guān)系,但在請求中并沒有嵌套的概念。
差異點:REST使用POST這樣的HTTP方法名稱來定義寫操作,GraphQL則是查詢結(jié)構(gòu)中的關(guān)鍵字。

正因為上述的第一個點,人們通常會把Query類型中的字段稱為GraphQL中的“端點”或“查詢條件”。雖然這是一個合理的解釋,但同時也會對其他人造成誤導,讓人以為Query類型是一個非常特殊的類型。

路由處理器(Route Handlers)vs. 解析器(Resolvers)

想象一下,當你調(diào)用一個API的時候,實際上會發(fā)生什么事情?嗯,應(yīng)該是在服務(wù)器上面執(zhí)行了一些代碼來處理這個請求,可能是進行了一些計算,可能從數(shù)據(jù)庫中加載了一些數(shù)據(jù),也可能是再次調(diào)用了一個別的API。雖然總的來說,作為調(diào)用方你并不需要知道內(nèi)部發(fā)生了什么事情,不過由于REST和GraphQL都提供了標準的API實現(xiàn)方法,我們可以通過對比來感受一下兩者之間的差異。

因為我比較熟悉JavaScript語言,所以在這個章節(jié)中我會使用它來做例子,但你也可以使用其他主流編程語言來實現(xiàn)REST或者GraphQL的API。為了突出重點,我會忽略掉一些構(gòu)建服務(wù)用的過程代碼。

首先使用Express實現(xiàn)一個hello world:

app.get('/hello', function (req, res) {
  res.send('Hello World!')
})

這里我們得到了一個可以返回“Hello World!”這個字符串的/hello端點。從這個例子我們可以看到一個REST API請求的的生命周期:

  1. 服務(wù)器收到請求并提取出HTTP方法名(比如這里就是GET方法)與URL路徑
  2. API框架找到提前注冊好的、請求路徑與請求方法都匹配的代碼
  3. 該段代碼被執(zhí)行,并得到相應(yīng)結(jié)果
  4. API框架對結(jié)果進行序列化,添加上適當?shù)臓顟B(tài)碼與響應(yīng)頭后,返回給客戶端

GraphQL差不多也是這樣工作的,我們來看下這個對應(yīng)的hello world例子

const resolvers = {
  Query: {
    hello: () => {
      return 'Hello world!';
    },
  },
};

我們看到,這里并沒有針對某個URL路徑提供函數(shù),而是把Query類型中的hello字段映射到一個函數(shù)上了。在GraphQL中這樣的函數(shù)我們稱之為解析器(Resolver)。

然后我們就可以這樣發(fā)起一個查詢:

query {
  hello
}

至此,總結(jié)一下服務(wù)器對一個GraphQL請求的執(zhí)行過程:

  1. 服務(wù)器收到HTTP請求,取出其中的GraphQL查詢
  2. 遍歷查詢語句,調(diào)用里面每個字段所對應(yīng)的Resolver。在這個例子里,只有Query這個類型中的一個字段hello
  3. Resolver函數(shù)被執(zhí)行并返回相應(yīng)結(jié)果
  4. GraphQL框架把結(jié)果根據(jù)查詢語句的要求進行組裝

因此我們將會得到如下響應(yīng):

{ "hello": "Hello, world!" }

這里有個小技巧:我們其實可以多次調(diào)用同一個Resolver:

query {
  hello
  secondHello: hello
}

在這個例子中的生命周期跟上面的是類似的,但因為我們通過別名來兩次請求了同一個字段,所以對應(yīng)Resolver函數(shù)hello也會被執(zhí)行兩次。雖然這個例子舉得不是很好,不過這里主要想表達的是在一個請求中可以解析多個字段,即使是相同的字段也可以在查詢的不同地方被多次訪問。

再來看下“嵌套”解析器是怎樣的:

{
  Query: {
    author: (root, { id }) => find(authors, { id: id }),
  },
  Author: {
    posts: (author) => filter(posts, { authorId: author.id }),
  },
}

這樣的解析器可以處理如下查詢請求:

query {
  author(id: 1) {
    firstName
    posts {
      title
    }
  }
}

即使解析器的結(jié)構(gòu)是扁平的,但由于它們被不同的類型所引用,所以你還是可以利用它們來實現(xiàn)嵌套查詢。想知道GraphQL如何執(zhí)行請求,請進一步閱讀這篇文章:“GraphQL Explained”

點擊這里可以查看完整的例子并體驗不同的查詢效果


上圖形象地說明了使用REST和GraphQL進行多種資源獲取的方式的差異

小結(jié)

總的來說,REST和GraphQL都提供了很好的API調(diào)用方式。如果你對如何構(gòu)建一個REST API足夠熟悉,使用GraphQL來實現(xiàn)同樣的API功能對你來說并不是一件難事。但GraphQL的一大優(yōu)勢是讓你可以在不需要發(fā)起多次請求的情況下調(diào)用多個函數(shù)來獲取資源數(shù)據(jù)。

相同點:REST的端點與GraphQL查詢字段都在服務(wù)端調(diào)起函數(shù)執(zhí)行。
相同點:REST和GraphQL都使用框架和類庫來進行一些通用的網(wǎng)絡(luò)協(xié)議處理。
差異點:一個REST請求對應(yīng)一個路由處理器(Route Handler),而一個GraphQL的請求可以喚起多個解析器(Resolver)在一次響應(yīng)中訪問多種資源。
差異點:REST需要你自己構(gòu)建整個請求的響應(yīng),而GraphQL的請求響應(yīng)是由查詢方指定結(jié)構(gòu)、并由GraphQL進行構(gòu)建組裝的。

你可以把GraphQL理解為一個可以在一次請求中進行多個端點調(diào)用的系統(tǒng),差不多算是REST的多路復用版。

綜上所述

GraphQL里面還有很多東西由于篇幅限制這里并沒有涉及,像對象識別、超媒體,以及緩存。這些話題以后有機會我們再來介紹。但我希望你通過本文對GraphQL有一個基本認識,知道它跟REST實際上是有很多概念上的相通。

我個人認為,GraphQL是有一些獨特的優(yōu)勢的。特別是使用一系列小的解析器函數(shù)來構(gòu)建一個完整的API這一點,實在是非??帷_@精簡了不同場景下形態(tài)各異的API數(shù)量,并避免讓API消費者取到對它來說并沒有用的冗余數(shù)據(jù)。

但在另一方面,GraphQL還并不像REST那樣有那么豐富的工具體系。比方說,你就不能像REST那樣輕易地對HTTP結(jié)果進行緩存。不過目前GraphQL社區(qū)正在努力地豐富和完善這些工具和基礎(chǔ)建設(shè),就緩存這個例子,其實你也可以通過Apollo ClientRelay這樣的工具去緩存GraphQL結(jié)果。

如果你對REST和GraphQL有更多的想法,請通過評論來告訴我。

聲明:
本譯文僅供個人研習、欣賞語言之用,謝絕任何轉(zhuǎn)載及用于任何商業(yè)用途。本譯文所涉法律后果均由本人承擔。本人同意簡書平臺在接獲有關(guān)著作權(quán)人的通知后,刪除文章。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • REST API 可以讓你用任何支持發(fā)送 HTTP 請求的設(shè)備來與 Parse 進行交互,你可以使用 REST A...
    Caroline嗯哼閱讀 2,189評論 0 0
  • API定義規(guī)范 本規(guī)范設(shè)計基于如下使用場景: 請求頻率不是非常高:如果產(chǎn)品的使用周期內(nèi)請求頻率非常高,建議使用雙通...
    有涯逐無涯閱讀 2,917評論 0 6
  • 作者: 一字馬胡 轉(zhuǎn)載標志 【2017-11-13】 更新日志 導入 作為一種強大的DSQL,學習GraphQL...
    一字馬胡閱讀 11,168評論 0 13
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,254評論 6 342

友情鏈接更多精彩內(nèi)容