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

原文標(biāo)題: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目前被認(rèn)為是革命性的API工具,因?yàn)樗梢宰尶蛻舳嗽谡?qǐng)求中指定希望得到的數(shù)據(jù),而不像傳統(tǒng)的REST那樣只能呆板地在服務(wù)端進(jìn)行預(yù)定義。這樣它就讓前、后端團(tuán)隊(duì)的協(xié)作變得比以往更加的通暢,從而能夠讓組織更好地運(yùn)作。而實(shí)際上,GraphQL與REST都是基于HTTP進(jìn)行數(shù)據(jù)的請(qǐng)求與接收,而且GraphQL也內(nèi)置了很多REST模型的元素在里面。

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

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

資源(Resources)

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

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

注:上面的例子里的"author"也會(huì)作為一個(gè)單獨(dú)的資源在其他REST API中被用到

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

在這一點(diǎn)上GraphQL就大為不同,因?yàn)樵贕raphQL里這兩個(gè)概念是完全分離的。比如說在你的schema定義中,你可能會(huì)有BookAuthor兩個(gè)類型(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的一個(gè)核心差異:資源的描述信息與其獲取方式相分離。

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

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

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

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

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

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

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

小結(jié)

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

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

路由(URL Route) vs. GraphQL Schema

一個(gè)具有可預(yù)見性的API才是好的API。因?yàn)槟阃ǔ?huì)把一個(gè)API當(dāng)做程序的一部分來使用,所以你必須要知道它需要接收什么參數(shù)并預(yù)期能夠獲取到什么樣的結(jié)果。

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

如今的REST API通常會(huì)由一系列的URL端點(diǎn)組成:

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

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

而在GraphQL中——其實(shí)在上一節(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會(huì)使用類似GET、POST這樣的動(dòng)詞去請(qǐng)求相同的URL來表示這到底是一個(gè)讀操作還是寫操作,而GraphQL會(huì)使用不同的預(yù)定義類型:Mutation和Query。在GraphQL請(qǐng)求中,你可以通過不同的關(guān)鍵字進(jìn)行不同的操作:

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

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

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

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

小結(jié)

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

正因?yàn)樯鲜龅牡谝粋€(gè)點(diǎn),人們通常會(huì)把Query類型中的字段稱為GraphQL中的“端點(diǎn)”或“查詢條件”。雖然這是一個(gè)合理的解釋,但同時(shí)也會(huì)對(duì)其他人造成誤導(dǎo),讓人以為Query類型是一個(gè)非常特殊的類型。

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

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

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

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

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

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

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

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

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

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

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

query {
  hello
}

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

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

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

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

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

query {
  hello
  secondHello: hello
}

在這個(gè)例子中的生命周期跟上面的是類似的,但因?yàn)槲覀兺ㄟ^別名來兩次請(qǐng)求了同一個(gè)字段,所以對(duì)應(yīng)Resolver函數(shù)hello也會(huì)被執(zhí)行兩次。雖然這個(gè)例子舉得不是很好,不過這里主要想表達(dá)的是在一個(gè)請(qǐng)求中可以解析多個(gè)字段,即使是相同的字段也可以在查詢的不同地方被多次訪問。

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

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

這樣的解析器可以處理如下查詢請(qǐng)求:

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

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

點(diǎn)擊這里可以查看完整的例子并體驗(yàn)不同的查詢效果


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

小結(jié)

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

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

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

綜上所述

GraphQL里面還有很多東西由于篇幅限制這里并沒有涉及,像對(duì)象識(shí)別、超媒體,以及緩存。這些話題以后有機(jī)會(huì)我們?cè)賮斫榻B。但我希望你通過本文對(duì)GraphQL有一個(gè)基本認(rèn)識(shí),知道它跟REST實(shí)際上是有很多概念上的相通。

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

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

如果你對(duì)REST和GraphQL有更多的想法,請(qǐng)通過評(píng)論來告訴我。

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

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

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

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

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