原文標題:GraphQL vs. RESTful Two ways to send data over HTTP: What’s the difference?
原文地址:https://dev-blog.apollodata.com/graphql-vs-RESTful-5d425123e34b
作者:Sashko Stubailo
翻譯:Fanny
前言
GraphQL 是Facebook於 2012 年在內部開發的數據查詢語言,在 2015 年開源,旨在提供RESTful架構體系的替代方案。
GraphQL和RESTful一樣,都是一種網站架構,一種前后端通信規范,不涉及語言,不同語言有不同的實現方案。
GraphQL目前被認為是革命性的API工具,因為它可以讓客戶端在請求中指定希望得到的數據,而不像傳統的RESTful那樣只能呆板地在服務端進行預定義。
這樣它就讓前、后端團隊的協作變得比以往更加的通暢,從而能夠讓組織更好地運作。
而實際上,GraphQL與RESTful都是基於HTTP進行數據的請求與接收,而且GraphQL也內置了很多RESTful模型的元素在里面。
那么在技術層面上,GraphQL和RESTful這兩種API模型到底有什么異同呢?我的觀點是,他們歸根到底其實沒多大區別,只不過GraphQL做了一些小改進,使得開發體驗產生了較大的改變。
我會從API的各個組件分別來討論GraphQL和RESTful都分別是如何處理的。
資源(Resources)
RESTful的核心思想就是資源,每個資源都能用一個URL來表示,你能通過一個GET請求訪問該URL從而獲取該資源。
根據當今大多數API的定義,你得到一份JSON格式的數據響應,整個過程大概是這樣:
GET /books/1
{
"title": "Black Hole Blues",
"author": {
"firstName": "Janna",
"lastName": "Levin"
}
// ... more fields here
}
注:上面的例子里的"author"也會作為一個單獨的資源在其他RESTful API中被用到
需要注意的是,在RESTful中,一個資源的種類與你獲取它的方式是耦合的。比如上面這個例子中的API就可以稱之為“book端點”(book endpoint)。
在這一點上GraphQL就大為不同,因為在GraphQL里這兩個概念是完全分離的。比如在你的schema定義中,你可能會有Book
和Author
兩個類型(type):
type Book {
id: ID
title: String
published: Date
price: String
author: Author
}
type Author {
id: ID
firstName: String
lastName: String
books: [Book]
}
注意這里我們雖然定義了數據類型,但卻不知道該如何獲取這些數據。
這是RESTful與GraphQL的一個核心差異:資源的描述信息與其獲取方式相分離。
如果要去訪問某個特定的book或者author資源,后端需要在schema中創建一個Query
類型:
type Query {
book(id: ID!): Book
author(id: ID!): Author
}
然后,前端就可以像RESTful那樣發送請求了:
GET /graphql?query={ book(id: "1") { title, author { firstName } } }
{
"title": "Black Hole Blues",
"author": {
"firstName": "Janna",
}
}
雖然都是通過請求某個URL來得到相同的響應,但這里我們已經看到GraphQL與RESTful的差異之處了。
首先,我們看到GraphQL的URL請求里面指定了我們所需要的資源以及在該資源中我們所關心的字段。
另外,我們是主動請求得到與book相關的author數據的,而不是服務端替我們決定的。
最重要的是,在請求中我們不需要關心資源的主鍵和資源之間的關系定義,我們可以通過除id以外的其他字段來獲取到相同的Book資源。
小結
現在我們知道的異同點有:
相同點:
- 都有資源這個概念,而且都能通過ID去獲取資源
- 都可以通過HTTP GET方式來獲取資源
- 都可以使用JSON作為響應格式
差異點:
- 在RESTful中,你所訪問的路徑就是該資源的唯一標識(ID);在GraphQL中,該標識與訪問方式並不相關
- 在RESTful中,資源的返回結構與返回數量是由服務端決定;在GraphQL,服務端只負責定義哪些資源是可用的,由客戶端自己決定需要得到什么資源
如果你已經用過GraphQL和RESTful,以上這些對你來說肯定相當簡單。如果你之前沒有用過GraphQL,你可以在到這里來實際體驗一下。
路由(URL Route) vs GraphQL Schema
一個具有可預見性的API才是好的API。
因為你通常會把一個API當做程序的一部分來使用,所以你必須要知道它需要接收什么參數並預期能夠獲取到什么樣的結果。
這時候,對API的訪問描述信息就顯得很重要。通常我們會通過閱讀API文檔來獲取信息,但通過GraphQL的Introspection機制、以及Swagger這樣的RESTful API工具,這些信息就能可以自動獲取到。
如今的RESTful API通常會由一系列的URL端點組成:
GET /books/:id
GET /authors/:id
GET /books/:id/comments
POST /books/:id/comments
你可以把這種API的形態稱之為線性結構——因為這就是一個列表嘛。
當你要獲取數據時,第一個事情就是搞清楚你要訪問的是哪個端點。
而在GraphQL中——其實在上一節里你也看到了——可以通過查看GraphQL schema獲得相關信息:
type Query {
book(id: ID!): Book
author(id: ID!): Author
}
type Mutation {
addComment(input: AddCommentInput): Comment
}
type Book { ... }
type Author { ... }
type Comment { ... }
input AddCommentInput { ... }
RESTful會使用類似GET、POST這樣的動詞去請求相同的URL來表示這到底是一個讀操作還是寫操作,而GraphQL會使用不同的預定義類型:Mutation和Query。
在GraphQL請求中,你可以通過不同的關鍵字進行不同的操作:
query { ... }
mutation { ... }
如果你想知道更多關於query的用法,請看我之前寫的文章“The Anatomy of a GraphQL Query”
這里的Query類型定義與上面的RESTful路由是完全契合的,同樣表示了數據的訪問入口,因此這是GraphQL中最能與RESTful的URL端點所對應的概念。
如果是對資源的簡單查詢,GraphQL與RESTful是類似的,都是通過指定資源的名稱以及相關參數來取得,但不同的是,在GraphQL中,你可以根據資源之間的關聯關系來發起一個復雜請求,而在RESTful中你只能定義一些特殊的URL參數來獲取到特殊的響應,或者是通過發起多個請求、再自行把響應得到的數據進行組裝才行。
小結
RESTful對數據的描述形式是一連串的URL端點,而GraphQL則是由相互之間有所關聯的schema組成。
相同點:
- RESTful API的URL端點列表與GraphQL的Query/Mutation中的字段類似,都表示數據的訪問入口
- 都能用不同的方式描述一個API請求到底是讀操作還是寫操作
差異點:
- GraphQL讓你可以通過一個資源入口訪問到關聯的其他資源,只要事先在schema中定義好資源之間的關系即可;而RESTful則提供了多個URL端點來獲取相關的資源
- 在GraphQL中,Query類型可以在一個請求的根節點中被訪問,除此以外它跟其他類型沒有區別,比如你也可以對一個query中的字段添加參數。而在RESTful中,即使響應結果是嵌套關系,但在請求中並沒有嵌套的概念
- RESTful使用POST這樣的HTTP方法名稱來定義寫操作,GraphQL則是查詢結構中的關鍵字
正因為上述的第一個點,人們通常會把Query類型中的字段稱為GraphQL中的“端點”或“查詢條件”。
雖然這是一個合理的解釋,但同時也會對其他人造成誤導,讓人以為Query類型是一個非常特殊的類型。
路由處理器(Route Handlers)vs 解析器(Resolvers)
想象一下,當你調用一個API的時候,實際上會發生什么事情?
應該是在服務器上面執行了一些代碼來處理這個請求,可能是進行了一些計算,可能從數據庫中加載了一些數據,也可能是再次調用了一個別的API。
雖然總的來說,作為調用方你並不需要知道內部發生了什么事情,不過由於RESTful和GraphQL都提供了標准的API實現方法,我們可以通過對比來感受一下兩者之間的差異。
因為我比較熟悉JavaScript語言,所以在這個章節中我會使用它來做例子,但你也可以使用其他主流編程語言來實現RESTful或者GraphQL的API。為了突出重點,我會忽略掉一些構建服務用的過程代碼。
首先使用Express實現一個hello world:
app.get('/hello', function (req, res) {
res.send('Hello World!')
})
這里我們得到了一個可以返回“Hello World!”這個字符串的/hello
端點。從這個例子我們可以看到一個RESTful API請求的的生命周期:
- 服務器收到請求並提取出HTTP方法名(比如這里就是GET方法)與URL路徑
- API框架找到提前注冊好的、請求路徑與請求方法都匹配的代碼
- 該段代碼被執行,並得到相應結果
- API框架對結果進行序列化,添加上適當的狀態碼與響應頭后,返回給客戶端
GraphQL差不多也是這樣工作的,我們來看下這個對應的hello world例子:
const resolvers = {
Query: {
hello: () => {
return 'Hello world!';
},
},
};
我們看到,這里並沒有針對某個URL路徑提供函數,而是把Query類型中的hello
字段映射到一個函數上了。
在GraphQL中這樣的函數我們稱之為解析器(Resolver)。
然后我們就可以這樣發起一個查詢:
query {
hello
}
至此,總結一下服務器對一個GraphQL請求的執行過程:
- 服務器收到HTTP請求,取出其中的GraphQL查詢
- 遍歷查詢語句,調用里面每個字段所對應的Resolver。在這個例子里,只有Query這個類型中的一個字段hello
- Resolver函數被執行並返回相應結果
- GraphQL框架把結果根據查詢語句的要求進行組裝
因此我們將會得到如下響應:
{ "hello": "Hello, world!" }
這里有個小技巧:我們其實可以多次調用同一個Resolver:
query {
hello
secondHello: hello
}
在這個例子中的生命周期跟上面的是類似的,但因為我們通過別名來兩次請求了同一個字段,所以對應Resolver函數hello
也會被執行兩次。
雖然這個例子舉得不是很好,不過這里主要想表達的是在一個請求中可以解析多個字段,即使是相同的字段也可以在查詢的不同地方被多次訪問。
再來看下“嵌套”解析器是怎樣的:
{
Query: {
author: (root, { id }) => find(authors, { id: id }),
},
Author: {
posts: (author) => filter(posts, { authorId: author.id }),
},
}
這樣的解析器可以處理如下查詢請求:
query {
author(id: 1) {
firstName
posts {
title
}
}
}
即使解析器的結構是扁平的,但由於它們被不同的類型所引用,所以你還是可以利用它們來實現嵌套查詢。想知道GraphQL如何執行請求,請進一步閱讀這篇文章:“GraphQL Explained”
上圖形象地說明了使用RESTful和GraphQL進行多種資源獲取的方式的差異
小結
總的來說,RESTful和GraphQL都提供了很好的API調用方式。如果你對如何構建一個RESTful API足夠熟悉,使用GraphQL來實現同樣的API功能對你來說並不是一件難事。但GraphQL的一大優勢是讓你可以在不需要發起多次請求的情況下調用多個函數來獲取資源數據。
相同點:
- RESTful的端點與GraphQL查詢字段都在服務端調起函數執行
- RESTful和GraphQL都使用框架和類庫來進行一些通用的網絡協議處理
差異點:
- 一個RESTful請求對應一個路由處理器(Route Handler);而一個GraphQL的請求可以喚起多個解析器(Resolver)在一次響應中訪問多種資源
- RESTful需要你自己構建整個請求的響應;而GraphQL的請求響應是由查詢方指定結構、並由GraphQL進行構建組裝的
你可以把GraphQL理解為一個可以在一次請求中進行多個端點調用的系統,差不多算是RESTful的多路復用版。
總結
GraphQL里面還有很多東西由於篇幅限制這里並沒有涉及,像對象識別、超媒體,以及緩存。這些話題以后有機會我們再來介紹。但我希望你通過本文對GraphQL有一個基本認識,知道它跟RESTful實際上是有很多概念上的相通。
主要的不同點就是:1. RESTful一個接口只能返回一個資源,GraphQL一個可以獲取多個資源。2. RESTful用不同的URL來區分資源,GraphQL用類型區分資源。
我個人認為,GraphQL是有一些獨特的優勢的。特別是使用一系列小的解析器函數來構建一個完整的API這一點,實在是非常酷。這精簡了不同場景下形態各異的API數量,並避免讓API消費者取到對它來說並沒有用的冗余數據。
但在另一方面,GraphQL還並不像RESTful那樣有那么豐富的工具體系。比方說,你就不能像RESTful那樣輕易地對HTTP結果進行緩存。不過目前GraphQL社區正在努力地豐富和完善這些工具和基礎建設,就緩存這個例子,其實你也可以通過Apollo Client和Relay這樣的工具去緩存GraphQL結果。