解讀GraphQL(一): WHAT & WHY
註:這個系列是去年在公司內部寫的一篇文章,可能有些內容已經過時了。但前些天同事找我要這篇文章的底檔時,有一篇我自己都找不到了……決定放逼乎存下檔。
今天我們解讀一下本期技術雷達中的GraphQL,它位於語言象限,處於評估階段,編號整100,非常方便查找……這項技術比較有意思。對我來說,技術雷達中經常有兩種典型技術:
- 第一種,比如說Apache Kafka這樣的,一看就感覺很牛,然後哇地讚歎一下,但因為離項目場景太遠,大概看看熱鬧就過去了。
- 第二種的典型就是ECMAScript 2017這種,早晚要用並且應用廣泛,但說真的,好像也沒啥可介紹的……。
GraphQL和這兩種都不太一樣——它用來構建我們Web前端/移動客戶端的API,這個覆蓋面就廣泛了。不管你是前端/後端/還是移動端開發,都跑不了和API打交道。然而,GraphQL本身非常激進,和我們現在的API形式大相徑庭,足夠搞個大新聞了。廢話不多說,我們進入正題把。
GraphQL是什麼?
GraphQL是Facebook推出的一個查詢語言,可能和聽起來不同,它並不是一個資料庫查詢語言,而是你可以用任何其他語言實現的一個用於查詢的抽象層。通常你可以通過GraphQL讓你的客戶端請求有權決定獲取的數據結構,也可以通過GraphQL獲得更好的多版本API兼容性。並且與大多數查詢語言不同的是,GraphQL是一個靜態類型的查詢語言,這意味著你可以通過GraphQL獲得更強大更安全的開發體驗。
Facebook自2012年就已經在移動端上使用GraphQL了,只是去年才將其開源。除了GraphQL之外,市面上也有許多類似的方案:比如Netflix的Falcor,以及ClojureScript編寫的om.next還有om.next的靈感來源Datomic等等。
讓我們先來看個例子:{ user(id: 3500401) { id, name, isViewerFriend, profilePicture(size: 50) { uri, width, height } } }
這條查詢很直接——它請求了某個ID下面的id, name以及isViewer狀態,同時還請求了她特定尺寸的頭像和頭像的信息。GraphQL的query很像JSON,而JSON也是我們實現GraphQL的返回值:
{ "user": 3500401, "name": "Jing Chen", "isViewerFriend": true, "profilePicture": { "url": "http://someurl.cdn/pic.jpg", "width": 50, "height": 50 }}
這很符合直覺:你發出去的請求中的結構基本就是你將要獲得的JSON,並且你也可以像函數一樣傳參來影響返回值。此外GraphQL並不關心傳輸協議,你可以將GraphQL作為放在Get請求的URL里,或者任何你能想到的方式:查詢實質上只是普通字元串。
為什麼要選擇GraphQL?
沒聽說過GraphQL的話可能看到上面的解釋還是不清楚,雖然這東西看起來很神奇,但是它是幹什麼用的?用在哪?好吧,那現在告訴你,GraphQL的核心目標就是取代RESTful API。REST是一種古老的面向服務端和客戶端(CS)的架構風格,並不是一項特定的技術。它定義了一系列嚴格的構建API的原則,用簡單的方式描述資源,並認為大部分時候違背這些原則會讓軟體的擴展性受限。隨著服務端SOA和客戶端Ajax的崛起,通信擴展問題變得越來越重要,REST得以廣泛被運用。在Micro Service逐漸流行的今天,RESTful API已經成為主流。如今,隨著前端和移動端的迅猛發展,REST也面臨嚴峻挑戰。
所以,REST有什麼問題?
REST本身作為一種對資源的建模,它的擴展性其實並無特殊問題。我們對於REST的指責經常並不來源於REST本身,而來自於REST並不能解決的問題:諸如性能優化和頁面展示的資源分類等等。我們可以列舉下REST問題的幾個表現——之所以用表現來形容,是因為它們都指向同一個問題——在為客戶端實現RESTful API過程中性能,頁面等等導致的折中設計和REST本身可擴展性之間不可調和的矛盾。注意,以下問題主要針對Web前端/移動端的API,並非Service之間的API——問題來自視圖和網路速度。
1.資源分類導致性能受限
在前端我們很少遇到運行效率問題,效率問題主要來自網路請求——一次HTTP請求的代價非常高昂,特別是對移動端來說。如果我們遵循REST的風格,我們就要將各種資源分門別類用不同的API來表示。而在客戶端中我們經常需要一次請求多種資源。這時候我們就要編寫許多API來為不同的頁面合併這些API。很多時候,我們寫的這些API並不是一個特定資源,但我們還得用URL來表示它們。
此外,當我們選擇不去合併資源時,性能損耗經常比我們想像的嚴重:如果合併完全無關的資源,不合併時也只是兩個並發的請求,返回的時間大多隻取決於更慢的API。而更常見的情況是,資源之間有映射關係:比如我們經常要先請求某個user信息,然後等這個API返回之後再渲染這個user名下的articles。於是我們再去請求不同的article。這時候,我們發現請求之間甚至不是並行的,而是串列的。而我們現實中的前端應用,由於視圖設計中常見層疊的資源,所以也經常會有這種多次串列的請求,這會導致我們的網路請求時間成倍增長。2.在現代場景中難於維護
雖然REST的目標是易於維護和擴展,但在Web前端/客戶端領域,它的表現並沒有想像得那麼好。我們經常說最明顯的Code smell就是重複。許多時候,我們要讓API適應視圖,但我們都知道,這種API僅被客戶端消費,與服務端代碼耦合是非常不合理的。隨著前端/移動端的興起,我們經常還要為多種客戶端編寫多種API。這些API代碼既類似又無聊,並且也要在客戶端修改時一起修改——僅前端和後端的重複我們可以讓IDE查找,然而這種散落在前後端的契約則很容易遺漏。不僅如此,如果這個API是Public API,一點小改動就要修改版本。
3.缺乏約束
RESTful API通過URL表述資源,它本身是無類型的。現在,隨著技術的發展,我們已經有許多非常強大的靜態類型語言,它們有非常強大的開發工具來幫助我們檢查錯誤。而在我們系統的API邊界,這些重量級的強大工具卻無能為力。隨著Micro service越來越流行,隨著系統中的邊界越來越多,靜態類型能捕獲的錯誤則越來越少。有時候我們用Scala編寫的應用在遭遇API錯誤時不能不說是一種諷刺……。
應對這種情形,我們則要花費額外的努力來維護契約測試,還要小心翼翼地對應Service之間的版本依賴,因為對REST來說,不同版本之間的兼容能力非常弱小。4.嚴格,抽象,但並不能解決客戶端問題:
我們經常可以在網上看到互相指責的文章和討論,基本上都是一方列舉出自己使用RESTful API遇到的實際問題,而另一方認為前者實際應用違反了哪條REST原則,因此不是RESTful API。這種情形十分典型,後者說的也正確,但這並沒有實際幫助——付出高昂的性能和開發代價來維護嚴格的RESTful是不現實的。此外,在實現之前反被要求反覆思考資源之間的關係的方式也不夠敏捷。
更糟的是,我們都很難簡短準確地跟他人解釋REST到底是什麼,我沒看過Roy Fielding的論文,其實我自己也不清楚。這類似Java大行其道時的設計模式:有用,但太玄學,用起來也不自然,最關鍵的是不能被代碼有效地抽象。然而最終我們發現大部分設計模式在某些語言里不然被徹底消滅,不然就變得很自然,讓你感覺不到了。而解決REST問題可能也類似:不再糾結教條,徹底換一種思路。那麼,GraphQL好在哪?
比起蒼白的討論而言,直接使用它可能更有說服力。幾個月前,Github宣布他們打算擁抱GraphQL:
We』ve often heard that our REST API was an inspiration for other companies; countless tutorials refer to our endpoints. Today, we』re excited to announce our biggest change to the API since we snubbed XML in favor of JSON: we』re making the GitHub API available through GraphQL.
我們可以試用Github放出的GraphQL API,雖然這個API還在Early Access階段,但我們可以用它直接探索GraphQL和我們熟悉的API查詢方式的區別。Github在他們的網頁里內嵌了一個GraphiQL——Facebook提供的GraphQL開發工具,它本質上是一個React組件,通過它,我們可以不構建代碼直接閱讀查詢文檔,調試我們的查詢。(註:使用需要登錄Github並授權)
1.需求驅動
我們可以先在GraphiQL里嘗試下——在左邊輸入這段查詢,按Ctrl Enter——這樣你就可以在右邊到自己的名字和公司了:
{ viewer { name company }}
如果觀察Network請求的話會發現,無論進行什麼查詢,請求都是指向同一個endpoint的POST請求。你也可以加上其他欄位,或者刪掉欄位試試看結果會怎樣。
GraphQL用來構建客戶端API,但它並不關心視圖,也不關心到底服務的到底是什麼客戶端。至於請求什麼數據,數據怎麼組織,全都是客戶端說了算——這也是為什麼要實現一個查詢語言的原因:有了查詢語言,你就可以精確描述你想要的了,移動端可能只獲取文章標題,而Web端則希望可以預覽部分內容。這被Facebook描述為Demand Driven。
除了減少構建無聊CRUD API之外,另一個明顯的好處是對於大部分前後端分離的項目,客戶端開發人員可以獨立修改頁面的展現形式。對於經常需要探索創意的創業公司來說,這降低了迭代成本,而對於前後端分離的大型項目而言,則減少了溝通成本。2.一次請求複雜數據
這次我們一次請求多種資源,大意就是同時查詢你前10個followers的名字,和他們前5個repository的名字(要是覺得還不複雜,你可以嵌套地查repository的follower的repository的follower……這樣循環下去):
{ viewer { followers(first: 10) { edges { node { name repositories(first: 5) { edges { node { name } } } } } } }}
上面提到了傳統方式會導致串列請求,這對性能的損耗是十分嚴重的。而GraphQL的意思,顧名思義——就是圖查詢語言。不同於平常的請求,實現GraphQL的服務端接收到請求後,雖然還是HTTP的一次請求,但是會根據查詢的結構遞歸地根據查詢來調用各項資源的Resolver(可以不太恰當地類比為Controller action),最後拼回一張JSON Graph返回給客戶端。因此你可以在一次查詢中輕鬆表述諸如「表弟的七大姑的二侄子的小姨子叫啥來著,多大歲數,有沒有對象」這種複雜的關係。
3.靜態類型
可能你已經注意到了,你輸入的查詢都有自動補全和類型糾錯功能,這歸功於GraphQL的靜態類型系統。你可以在定義GraphQL Schema時添加更多的類型來描述不同的資源。在GraphiQL的右邊有個「Docs」面板,點開可以看到各種類型的簽名和描述,每種類型可以繼續點擊查看詳情。你可以完全沒文檔的情況下,僅通過它很快理解所有API。
其實這個「Docs」並不用手動編寫,它完全根據服務端代碼自動生成。而這個面板本身信息的來源,也只不過是GraphQL查詢本身,這被Facebook稱為自省(Introspection)。你可以打開Network,刷新頁面查看GraphiQL是如何查詢所有類型信息的。比如我們可以嘗試:{ __type(name: "Repository") { description } __schema { types { name } }}
這樣就會返回Repository這個type到底是個啥,以及我想知道服務端所有的type。
對於編程語言來說,擁有強大的靜態類型是很常見的。對於查詢語言,卻不是很多見。在這點上GraphQL有點像RPC,可以生成GraphQL Schema來作為服務端和客戶端間的契約。一般這個過程也會自動生成JSON schema來方便你做其他的契約測試。Intellij IDEA還有一個非常完善的GraphQL插件,當你服務端Schema有Breaking Change時,客戶端代碼就會報錯,有些編譯插件也會產生編譯時錯誤。
4.兼容多版本
由於客戶端可以決定請求的內容,服務端也可以不刪除廢棄的欄位,而僅僅加入@Deprecated註解,這樣客戶端查詢時只會被Warning。這樣做的結果就是不同客戶端和不同Service之間的版本依賴也變得非常寬鬆了(註:這段代碼是用來在服務端定義Schema而不是查詢的,所以不能在GraphiQL中用):
type Film { title: String episode: Int releaseDate: String openingCrawl: String director: String @deprecated directedBy: Person}
5.Mutation
我們說RESTful API經常說CRUD這幾個動作,也就是說光查詢不行,還得能向服務端寫數據。當然,GraphQL的核心功能之一就是Mutation,也就是實現CUD這些非只讀操作。比如如下的查詢可以讓我們創建一個新Repository並返回這個新Repository的ID(很可惜,這個API似乎有問題,Github會匹配不到你自己的ownerId):
mutation { createProject( input:{ name:"test repository", ownerId:"MDQ6VXNlcjEwMTkzNDA1" } ) { clientMutationId project { id } }}
GraphQL潛在的問題?
雖然GraphQL看起來很酷炫,但是也有些地方要注意。
1.服務端優化
由於是查詢本身被解析成圖,遞歸地取值,因此可能會存在伺服器性能隱患。特別是對SQL來說,現在非常容易大量出現N+1的情形。因此,現在GraphQL大多還都用在NoSQL上。但是由於整個請求還是在一次HTTP請求中完成的,理論上我們也有Batch為一個查詢的能力,就像許多ORM有一些惰性特性,可以將多個查詢過濾語句合併成一條查詢一樣。Facebook正在研究如何讓GraphQL更好地Batch的問題比如dataloader,社區也有一些不錯的實現。這些庫不僅在嘗試解決這些問題,而且也揭示了GraphQL作為API抽象層本身,可以在普通Web場景下和ORM結合的能力——通過代碼可以將這兩層抽象到一起,這很可能讓我們可以自己輕易搭建一個GraphQL BAAS(Back-end as a service)。現在已經有很多平台提供這種服務了,比如scaphold,reindex,graphcool等,也有Graffiti這種與Mongo ODM直接結合的類庫。
2.安全問題
雖然GraphQL給客戶端提供了強大的查詢能力,但這也意味著被客戶端有濫用的風險。如果不使用某些限制過大的查詢,反覆請求一條Load出所有Github用戶的查詢可能會讓他們的伺服器直接掛掉,GraphQL提高了被DDoS的風險。因此在使用GraphQL的過程中,我們要對安全問題更加重視。
3.需要重新思考Cache策略
REST雖然會引起一些性能問題,但它也以HTTP Cache的方式解決了很多性能問題。而對於多變的GraphQL操作來說,Cache就變成一個需要深入討論的話題了。然而這種Cache策略就要交給客戶端來完成了。
然後呢?
因此,接下來接下來的文章會深入討論基於GraphQL的多種類庫以及不同客戶端。最終我們也可以在這些類庫上看到在現代組件化趨於主流之後,我們的通信應該怎麼與組件化設計有效結合。並且隨著客戶端越來越複雜,我們應該如何同步服務端狀態,如何管理緩存等等:其中一個是Meteor團隊推出的Apollo Data,它提供了一系列的服務端以及客戶端工具來簡化GraphQL的開發,容易上手並且支持Android,IOS,React(Native)以及Angular2。而另外一個更複雜但更強大的選擇則是Facebook自己推出的Relay,它支持React(Native)提供了類似Virtual DOM的Diff演算法來Diff Cache,可以自動精確管理數據來解決Cache問題。我們將會從Apollo開始,感受GraphQL究竟是如何工作的,之後我們也會看到這個方案中的一些問題,而這些問題會將我們引向更深層的Web客戶端問題——最終我們會探討Relay,一個更完整的解決方案,從中我們可以看到Facebook對Web客戶端的未來有著怎樣的思考。
推薦閱讀:
※構建 React.js 應用的十佳 UI 框架,都在這了!
※React 16 發布,Facebook 如約解除了專利條款
※React源碼筆記-虛擬dom
※Vue和React的使用場景和深度有何不同?
※關於在react中request到底是應該寫在哪裡?