為什么我使用 GraphQL 而放棄 REST API?

本文最初發(fā)布于 Max Desiatov 的個(gè)人博客,經(jīng)原作者授權(quán)由 InfoQ 中文站翻譯并分享。
在大多數(shù)移動(dòng)和 Web 應(yīng)用中,服務(wù)器交互需要花費(fèi)開(kāi)發(fā)人員大量時(shí)間和精力來(lái)開(kāi)發(fā)和測(cè)試。
在我所開(kāi)發(fā)的那些擁有最復(fù)雜 API 應(yīng)用程序中,網(wǎng)絡(luò)層設(shè)計(jì)和維護(hù)占去高達(dá) 40% 的開(kāi)發(fā)時(shí)間,特別是由于我在本文中提到的一些邊緣情況。這樣實(shí)現(xiàn)過(guò)幾次后,很容易就會(huì)發(fā)現(xiàn),有一些不同的模式、工具和框架可以帶來(lái)幫助。雖然我們很幸運(yùn),不必再關(guān)心 SOAP,但 REST 也不是歷史的終結(jié)。
最近,我有機(jī)會(huì)為自己的項(xiàng)目和客戶(hù)開(kāi)發(fā)和運(yùn)行一些使用 GraphQL API 構(gòu)建的移動(dòng)和 Web 應(yīng)用程序。這真是一個(gè)很好的體驗(yàn),尤其要感謝令人驚嘆的 PostGraphile 和 Apollo。至此,我再也無(wú)法回過(guò)頭來(lái)享受使用 REST 的工作了。
公平地說(shuō),REST 甚至不是一個(gè)標(biāo)準(zhǔn)。維基百科將其定義為:
一種架構(gòu)風(fēng)格,基于 HTTP 定義了一組約束和屬性。
雖然確實(shí)存在像 JSON API 規(guī)范這樣的東西,但在實(shí)踐中,我們很少看到有 RESTful 后端實(shí)現(xiàn)它。在最好的情況下,你可能會(huì)偶然發(fā)現(xiàn)一些使用 OpenAPI/Swagger 的東西。即使這樣,OpenAPI 也沒(méi)有指定 API 的形狀或格式,它只是一個(gè)機(jī)器可讀的規(guī)范,允許(但不是要求)你對(duì) API 運(yùn)行自動(dòng)化測(cè)試、自動(dòng)生成文檔等。
主要問(wèn)題仍然存在。你可能會(huì)說(shuō)你的 API 是 RESTful 的,但是對(duì)于如何安排端點(diǎn)或是否應(yīng)該(例如)使用 HTTP 方法PATCH進(jìn)行對(duì)象更新,一般沒(méi)有嚴(yán)格的規(guī)則。
還有一些東西乍一看是 RESTful 的,但如果你仔細(xì)看,就不是那么像了:Dropbox HTTP API。
端點(diǎn)接受請(qǐng)求體中的文件內(nèi)容,因此,它們的參數(shù)將以 JSON 的形式在
Dropbox-API-Arg請(qǐng)求頭或 arg URL 參數(shù)中傳遞。
JSON 在請(qǐng)求頭中?
沒(méi)錯(cuò),Dropbox API 端點(diǎn)要求你將請(qǐng)求正文留空,并將有效載荷序列化為 JSON,放到一個(gè)自定義的 HTTP 頭中。為這種特殊情況編寫(xiě)客戶(hù)端代碼很有趣。我們不能抱怨,因?yàn)楫吘箾](méi)有廣泛使用的標(biāo)準(zhǔn)。
事實(shí)上,下面提到的大多數(shù)注意事項(xiàng)都是由于缺乏標(biāo)準(zhǔn)造成的,但是我想強(qiáng)調(diào)一下在實(shí)踐中經(jīng)常看到的情況。
在一個(gè)有經(jīng)驗(yàn)的團(tuán)隊(duì)中,你可以避免這些問(wèn)題,但是你難道不希望一些問(wèn)題已經(jīng)在軟件方面得到解決嗎?
無(wú)論如何努力避免這種情況,你遲早會(huì)遇到 JSON 屬性拼寫(xiě)錯(cuò)誤、發(fā)送或接收的數(shù)據(jù)類(lèi)型錯(cuò)誤、字段丟失等問(wèn)題。如果你的客戶(hù)端和 / 或服務(wù)器編程語(yǔ)言是靜態(tài)類(lèi)型的,并且你不能用錯(cuò)誤的字段名或類(lèi)型構(gòu)造對(duì)象,那可能沒(méi)問(wèn)題。如果你的 API 是版本化的,舊 API 的 URL 為/API/v1,新版本的 URL 為/API/v2,那么你可能做得很好。如果有一個(gè) OpenAPI 規(guī)范,可以為你生成客戶(hù)端 / 服務(wù)器類(lèi)型聲明,那就更好了。
但你真能負(fù)擔(dān)得起在所有項(xiàng)目中都做到這樣嗎?當(dāng)你的團(tuán)隊(duì)在沖刺期間決定重命名或重新安排對(duì)象字段時(shí),你能負(fù)擔(dān)得起上線(xiàn)/api/v1.99端點(diǎn)的成本嗎?即使完成了,團(tuán)隊(duì)會(huì)不會(huì)忘記更新規(guī)范并通知客戶(hù)端開(kāi)發(fā)人員更新內(nèi)容?
在客戶(hù)端或服務(wù)器上的所有驗(yàn)證邏輯,你確定都是正確的嗎?理想情況下,你希望它在兩邊都得到驗(yàn)證,對(duì)吧?維護(hù)所有這些自定義代碼非常有趣?;蛘弑3?API JSON 模式是最新的。
大多數(shù) API 都使用對(duì)象集合。在待辦事項(xiàng)列表應(yīng)用中,列表本身就是一個(gè)集合。大多數(shù)集合都可以包含 100 多個(gè)項(xiàng)。對(duì)于大多數(shù)服務(wù)器來(lái)說(shuō),在一次響應(yīng)的一個(gè)集合中返回所有項(xiàng)是一個(gè)繁重的操作。如果再乘以在線(xiàn)用戶(hù)的數(shù)量,就會(huì)產(chǎn)生很大的 AWS 賬單。顯而易見(jiàn)的解決方案:只返回集合的子集。
分頁(yè)相對(duì)簡(jiǎn)單。在查詢(xún)參數(shù)中傳遞類(lèi)似offset和limit這樣的值:/todos?Limit =10&offset=20以獲得從 20 開(kāi)始的 10 個(gè)對(duì)象。每個(gè)人對(duì)這些參數(shù)的命名都不一樣,有些人喜歡count和skip,而我喜歡offset和limit,因?yàn)樗鼈冎苯訉?duì)應(yīng)于 SQL 修飾符。
一些后端數(shù)據(jù)庫(kù)會(huì)暴露要傳遞給下一頁(yè)查詢(xún)的游標(biāo)或標(biāo)記。請(qǐng)查看 Elasticsearch API,該 API 建議在需要依次瀏覽大量結(jié)果文檔時(shí)使用scroll調(diào)用。還有一些 API 在頭中傳遞相關(guān)信息。參見(jiàn) GitHub REST API(至少不是在頭中傳遞 JSON)。
說(shuō)到過(guò)濾,就有趣多了……需要按一個(gè)字段過(guò)濾嗎?沒(méi)問(wèn)題,可能是/todos?filter=key%3Dvalue,也可能是可讀性更好的/todos?filterKey=key&filterValue=value。那么按兩個(gè)值過(guò)濾呢?這應(yīng)該很簡(jiǎn)單,對(duì)吧?使用 URL 編碼,查詢(xún)看起來(lái)是這個(gè)樣子:/todos?filterKeys=key1%2Ckey2&filterValue=value。但通常,我們沒(méi)有辦法阻止特性蔓延,可能會(huì)出現(xiàn)使用AND/OR操作符進(jìn)行高級(jí)過(guò)濾的需求。或者復(fù)雜的全文搜索查詢(xún)和復(fù)雜的過(guò)濾。遲早你會(huì)看到一些 API 發(fā)明了自己的過(guò)濾 DSL。URL 查詢(xún)組件已經(jīng)不夠用了,但是GET請(qǐng)求中的請(qǐng)求體也不太好,這意味著你最終要在POST請(qǐng)求中發(fā)送非可變查詢(xún)(Elasticsearch 就是這樣做的)。至此,API 還是 RESTful 的嗎?
無(wú)論哪種方式,客戶(hù)端和服務(wù)器都需要特別注意解析、格式化和驗(yàn)證所有這些參數(shù)。如此多的樂(lè)趣!舉例來(lái)說(shuō),如果沒(méi)有恰當(dāng)?shù)尿?yàn)證且存在未初始化的變量,你就很容易地得到類(lèi)似這樣的東西:/todos?offset=undefined。
上面提到的 Swagger 可能是目前最好的工具,但其應(yīng)用還不夠廣泛。根據(jù)我的觀察,更常見(jiàn)的情況是,API 文檔單獨(dú)維護(hù)。對(duì)一個(gè)穩(wěn)定且廣泛使用的 API 來(lái)說(shuō),這沒(méi)什么大不了的,但是在敏捷流程的開(kāi)發(fā)過(guò)程中,這就比較糟糕了。文檔單獨(dú)存儲(chǔ)意味著,它經(jīng)常不會(huì)更新,特別是當(dāng)更改是一個(gè)小的、但會(huì)破壞客戶(hù)端的更改時(shí)。
如果你不使用 Swagger,這可能意味著你需要維護(hù)專(zhuān)門(mén)的測(cè)試基礎(chǔ)設(shè)施。與單元測(cè)試相比,你對(duì)集成測(cè)試(即同時(shí)測(cè)試客戶(hù)端和服務(wù)器端代碼)的需求會(huì)更多。
對(duì)于比較大的 API,這就成了一個(gè)問(wèn)題,因?yàn)槟憧赡苡性S多相關(guān)的集合。讓我們進(jìn)一步來(lái)看一個(gè)待辦事項(xiàng)列表應(yīng)用程序的例子:假設(shè)每個(gè)待辦事項(xiàng)也可以屬于一個(gè)項(xiàng)目。你是否總是希望一次獲取所有相關(guān)的項(xiàng)目?可能不需要,但是還需要添加更多的查詢(xún)參數(shù)。也許你不想一次獲取所有對(duì)象字段。如果應(yīng)用程序需要項(xiàng)目有所有者,并且除了每個(gè)集合有單獨(dú)的視圖顯示外,還有一個(gè)視圖顯示所有這些數(shù)據(jù)的聚合?它要么是三個(gè)獨(dú)立的 HTTP 請(qǐng)求,要么是一個(gè)復(fù)雜的請(qǐng)求,同時(shí)獲取所有數(shù)據(jù)用于聚合。
無(wú)論哪種方式,都存在復(fù)雜性和性能上的權(quán)衡,在不斷發(fā)展的應(yīng)用程序中維護(hù)這些請(qǐng)求會(huì)帶來(lái)更多令人頭痛的問(wèn)題。
還有大量的庫(kù)可以在 ORM 或直接數(shù)據(jù)庫(kù)自省的幫助下自動(dòng)生成 REST 端點(diǎn)。即使使用了這樣的庫(kù),它們通常也不是很靈活或可擴(kuò)展的。也就是說(shuō),如果需要自定義參數(shù)、高級(jí)過(guò)濾行為或?qū)φ?qǐng)求 / 響應(yīng)有效負(fù)載的一些更智能的處理,就需要從頭重新實(shí)現(xiàn)端點(diǎn)。
另一項(xiàng)任務(wù)是在客戶(hù)端代碼中使用這些端點(diǎn)。如果有的話(huà),最好使用代碼生成,但是它似乎不夠靈活。即使是使用像 Moya 這樣的輔助庫(kù),也會(huì)遇到同樣障礙:有許多自定義行為需要處理,這是由前面提到的邊緣情況引起的。
如果開(kāi)發(fā)團(tuán)隊(duì)不是全棧的,那么服務(wù)器和客戶(hù)端團(tuán)隊(duì)之間的溝通就至關(guān)重要,在沒(méi)有機(jī)器可讀的 API 規(guī)范的情況下更是如此。
對(duì)于所有討論過(guò)的問(wèn)題,我傾向于認(rèn)為,在 CRUD 應(yīng)用程序中,有一種標(biāo)準(zhǔn)方式來(lái)生成和使用 API 會(huì)非常棒。通用的工具和模式、集成測(cè)試和文檔基礎(chǔ)設(shè)施將有助于解決技術(shù)和組織問(wèn)題。
GraphQL 有一個(gè) RFC 規(guī)范草案 和一個(gè)參考實(shí)現(xiàn)。此外,請(qǐng)參閱 GraphQL 教程,它描述了你需要了解的大多數(shù)概念。有針對(duì)不同平臺(tái)的實(shí)現(xiàn),也有許多可用的開(kāi)發(fā)工具,其中最著名的是 GraphiQL,它捆綁了一個(gè)很好的、具有自動(dòng)完成功能的 API 瀏覽器,以及一個(gè)文檔瀏覽器,可以瀏覽從 GraphQL 模式自動(dòng)生成的文檔。
事實(shí)上,我發(fā)現(xiàn) GraphiQL 是不可或缺的。它可以幫助解決我前面提到的客戶(hù)端和服務(wù)器團(tuán)隊(duì)之間的溝通問(wèn)題。只要 GraphQL 模式中有任何更改,你就可以在 GraphQL 瀏覽器中看到它,就像嵌入式 API 文檔?,F(xiàn)在,客戶(hù)端和服務(wù)器團(tuán)隊(duì)可以以一種更好的方式在 API 設(shè)計(jì)上開(kāi)展合作,縮短迭代時(shí)間,共享自動(dòng)生成的文檔,它們讓每次 API 更新對(duì)每個(gè)人都可見(jiàn)。要了解這些工具是如何工作的,請(qǐng)查看 Star Wars API 示例,它可以作為 GraphiQL 的在線(xiàn)演示。
能指定從服務(wù)器請(qǐng)求的對(duì)象字段讓客戶(hù)端可以根據(jù)需要只獲取需要的數(shù)據(jù)。不再有多個(gè)重量級(jí)的查詢(xún)發(fā)送到一個(gè)剛性的 REST API,為了讓客戶(hù)端可以在應(yīng)用程序 UI 中一次性顯示它。你不再受限于一組端點(diǎn),而是有一個(gè)可以查詢(xún)和修改的模式,能夠挑選客戶(hù)端指定的字段和對(duì)象。服務(wù)器只需以這種方式實(shí)現(xiàn)頂級(jí)模式對(duì)象。
GraphQL 模式定義了可用于在服務(wù)器和客戶(hù)端之間通信的類(lèi)型。有兩種特殊類(lèi)型,它們同時(shí)也是 GraphQL 的核心概念:Query和Mutation。在大多數(shù)情況下,向 GraphQL API 發(fā)出的每個(gè)請(qǐng)求要么是沒(méi)有副作用的Query實(shí)例,要么是會(huì)修改存儲(chǔ)在服務(wù)器上的對(duì)象的Mutation實(shí)例。
type Project {
id: ID
name: String!
}
type TodoItem {
id: ID
description: String!
isCompleted: Boolean!
dueDate: Date
project: Project
}
type TodoList {
totalCount: Int!
items: [TodoItem]!
}
type Query {
allTodos(limit: Int, offset: Int): TodoList!
todoByID(id: ID!): TodoItem
}
type Mutation {
createTodo(item: TodoItem!): TodoItem
deleteTodo(id: ID!): TodoItem
updateTodo(id: ID!, newItem: TodoItem!): TodoItem
}
schema {
query: Query
mutation: Mutation
}schema塊是特定的,定義了前面描述的根類(lèi)型Query和Mutation。此外,它非常簡(jiǎn)單:type塊定義新的類(lèi)型,每個(gè)塊包含具有自己類(lèi)型的字段定義。類(lèi)型可以是非可選的,例如String!字段不能有空值,而String可以。字段也可以有命名參數(shù),所以TodoList!類(lèi)型的字段allTodos(limit: Int, offset: Int): TodoList!接受兩個(gè)可選參數(shù),而其本身的值是非可選的,這意味著它將始終返回一個(gè)不能為空的TodoList實(shí)例。然后,要查詢(xún)所有待辦事項(xiàng)的id和名稱(chēng),你可以編寫(xiě)這樣一個(gè)查詢(xún):query {
allTodos(limit: 5) {
totalCount
items {
id
description
isCompleted
}
}
}allTodos字段的offset參數(shù)是缺失的。作為可選項(xiàng),它的缺失意味著它有null值。如果服務(wù)器提供這種模式,文檔中可能會(huì)聲明,null偏移量意味著默認(rèn)情況下應(yīng)該返回第一頁(yè)。響應(yīng)可能是這樣的:{
"data": {
"allTodos": {
"totalCount": 42,
"items": [
{
"id": 1,
"description": "write a blogpost",
"isCompleted": true
},
{
"id": 2,
"description": "edit until looks good",
"isCompleted": true
},
{
"id": 2,
"description": "proofread",
"isCompleted": false
},
{
"id": 4,
"description": "publish on the website",
"isCompleted": false
},
{
"id": 5,
"description": "share",
"isCompleted": false
}
]
}
}
}如果你從查詢(xún)中刪除isCompleted字段,它將從結(jié)果中消失?;蛘吣憧梢蕴砑?code>project字段,用其id和name來(lái)遍歷關(guān)系。將offset參數(shù)添加到allTodos字段進(jìn)行分頁(yè),這樣allTodos(count: 5, offset: 5)將返回第二頁(yè)。結(jié)果中提供了totalCount字段,這很有用,因?yàn)楝F(xiàn)在你知道總共有42 / 5 = 9頁(yè)。但顯然,如果不需要totalCount,你可以忽略它。查詢(xún)可以完全控制將要接收的實(shí)際信息,但是底層的 GraphQL 基礎(chǔ)設(shè)施還必須確保所有必需的字段和參數(shù)都在那里。如果你的 GraphQL 服務(wù)器足夠聰明,它將不會(huì)對(duì)你不需要的字段運(yùn)行數(shù)據(jù)庫(kù)查詢(xún),而且有些庫(kù)好到免費(fèi)提供這種查詢(xún)。此模式中的其他變體和查詢(xún)也是如此:對(duì)輸入進(jìn)行類(lèi)型檢查和驗(yàn)證,并且基于查詢(xún),GraphQL 服務(wù)器知道期望的結(jié)果形狀。本質(zhì)上,所有通信都通過(guò)服務(wù)器上一個(gè)預(yù)定義的 URL(通常是/graphql)運(yùn)行,借助一個(gè)簡(jiǎn)單的POST請(qǐng)求,其中包含序列化為 JSON 有效負(fù)載的查詢(xún)。但是,你幾乎從來(lái)都不需要接觸如此低的抽象層。
總體來(lái)說(shuō)還不錯(cuò):我們已經(jīng)解決了類(lèi)型級(jí)別的驗(yàn)證問(wèn)題,分頁(yè)看起來(lái)也不錯(cuò),并且在需要時(shí)可以輕松地遍歷實(shí)體關(guān)系。如果使用一些現(xiàn)成的 GraphQL->數(shù)據(jù)庫(kù)查詢(xún)翻譯庫(kù),你甚至不需要在服務(wù)器上編寫(xiě)大多數(shù)數(shù)據(jù)庫(kù)查詢(xún)??蛻?hù)端庫(kù)可以很容易地將 GraphQL 響應(yīng)自動(dòng)解包為所需類(lèi)型的對(duì)象實(shí)例,因?yàn)閺哪J胶筒樵?xún)可以提前知道響應(yīng)形狀。
雖然 Netflix falcor 似乎在解決類(lèi)似問(wèn)題,它比 GraphQL 早幾個(gè)月發(fā)布在 GitHub 上,也更早地引起我的注意,但很明顯,似乎 GraphQL 贏了。良好的工具和強(qiáng)大的行業(yè)支持使其非常有吸引力。
除了一些客戶(hù)端庫(kù)中存在的一些小問(wèn)題(現(xiàn)在已經(jīng)解決了)之外,我強(qiáng)烈推薦你仔細(xì)看看 GraphQL 在你的技術(shù)棧中可以提供什么。它已經(jīng)出技術(shù)預(yù)覽四年多了,而且這個(gè)生態(tài)系統(tǒng)正在變得更加強(qiáng)大。在 Facebook 設(shè)計(jì) GraphQL 的同時(shí),我們也看到越來(lái)越多的大公司在他們的產(chǎn)品中使用它:GitHub、Shopify、Khan Academy、Coursera,而且 這個(gè)列表還在不斷增長(zhǎng)。
有很多流行的開(kāi)源項(xiàng)目都在使用 GraphQL:這個(gè)博客是基于靜態(tài)站點(diǎn)生成器 Gatsby,它將 GraphQL 查詢(xún)的結(jié)果轉(zhuǎn)換成數(shù)據(jù),然后呈現(xiàn)到 HTML 文件中。如果你使用的是 WordPress,也有 GraphQL API 可以使用。Reaction Commerce 是 Shopify 的開(kāi)源替代方案,同樣是基于 GraphQL。
另外值得一提的兩個(gè) GraphQL 庫(kù)是 PostGraphile 和 Apollo。
如果你使用 PostgreSQL 作為后端數(shù)據(jù)庫(kù),PostGraphile 能夠掃描 SQL 模式并自動(dòng)生成一個(gè)帶有實(shí)現(xiàn)的 GraphQL 模式。你可以將所有常見(jiàn)的 CRUD 操作暴露為所有表的查詢(xún)和修改。它可能看起來(lái)像 ORM,但它不是:你可以完全控制如何設(shè)計(jì)數(shù)據(jù)庫(kù)模式,以及使用什么索引。
最妙的是,PostGraphile 還以查詢(xún)和修改的方式暴露視圖和函數(shù),所以如果有特別復(fù)雜的 SQL 查詢(xún)需要映射到 GraphQL 字段,只需創(chuàng)建 SQL 視圖或函數(shù),它就會(huì)自動(dòng)出現(xiàn)在 GraphQL 模式中。通過(guò)像行級(jí)安全這樣的高級(jí) Postgres 特性,你可以通過(guò)編寫(xiě)少量 SQL 策略實(shí)現(xiàn)復(fù)雜的訪問(wèn)控制邏輯。PostGraphile 甚至還有模式文檔這樣的東西,可以從 Postgres 注釋自動(dòng)生成。
相應(yīng)地,Apollo 提供了多個(gè)平臺(tái)的客戶(hù)端庫(kù),以及在最流行的編程語(yǔ)言(包括 TypeScript 和 Swift)中生成類(lèi)型定義的代碼生成器。
總的來(lái)說(shuō),我發(fā)現(xiàn),Apollo 比 Relay 等更簡(jiǎn)單和易于使用。由于 Apollo 客戶(hù)端庫(kù)架構(gòu)簡(jiǎn)單,我能夠?qū)⒁粋€(gè)使用 React.js 與 Redux 的應(yīng)用慢慢過(guò)渡到 React Apollo,一個(gè)組件一個(gè)組件的,只在有意義的時(shí)候才這樣做。與原生 iOS 應(yīng)用一樣,Apollo iOS 是一個(gè)相對(duì)輕量級(jí)的、易于使用的庫(kù)。
https://desiatov.com/why-graphql/?fileGuid=cGOKAr3CJtY4Y9Rh
