GraphQL,你準備好了么?
一個多月前,facebook 將其開源了一年多的 GraphQL 標記為 production ready ( http://graphql.org/blog/production-ready/ ),幾乎同一時間,github 開放了其 GraphQL API 的 early access ( GitHub GraphQL API )。兩顆重磅炸彈先後落地,是否意味著已有五年多歷史,和 facebook 的 news feed 幾乎同時誕生的 GraphQL 將在接下來的日子裡大放異彩,逐漸取代 REST API 的地位?
REST API
我們先通過一個簡單的例子來說明 REST 和 GraphQL 的異同。假設 TubiTV 有一個 API,要獲取系統中的某個用戶,就 REST API 而言,其介面大概會是這樣:
GET /v1/users/1
然後其返回結果是:
{ "first": "Tyr", "last": "Chen", "email": "...", "bookmarks": [ "12345", "12346", "12347" ], "total_view_time": 10800}
其中,bookmarks 是該用戶收藏的電影。如果客戶端調用這個 API 顯示用戶的 profile,單單這個 API 還不夠,因為 bookmarks 沒有展開,無法直接渲染,所以我們大概會再提供一條 API:
GET /v1/users/1/bookmarks
返回結果是:
[ { "id": "12345", "title": "Doctor Strange", "description": "...", "poster": "//cdn.tubitv.com/12345.jpg", "url": "//cdn.tubitv.com/12345-1280x714-,559,1414,2236,2642,3628,k.mp4.m3u8" }, ...]
這樣的 API 雖然簡潔,每個 API 各司其職,但對客戶端不友好,為了獲取所有用於渲染的數據,需要發出多個請求。尤其是對於手機客戶端,任何多餘的請求都會大大拖累用戶體驗。因此,這樣的 API 往往會折衷一下:
GET /v1/users/1/?expand=bookmarks
由此,通過提供額外的參數,API 返回包含 bookmark 詳細信息的內容。這種折衷會隨著各種各樣的需求,作出各種各樣的變化,比如,允許 API 返回 partial fields:
GET /v1/users/1/?fields=first,last,email,bookmarks(title,poster)&expand=bookmarks
這些各種各樣的「補丁」各自為戰,缺乏整體的觀念,容易互相影響,就像托勒密的地心說模型一樣,起初一切都很美好,隨著天文觀測的發展,這模型不得不添加更多的本輪,以迎合觀測的數據,最終這理論不堪重負而被日心說取代。
上面的例子如果調用者忘記在 fields 里包含 bookmarks,則 expand 做了無用功;反之,fields 里包含了 bookmarks(title,poster),卻沒有 expand,會返回錯誤結果。在理想和現實中妥協的 REST API,會讓其他的事情也變得困難。我們這裡舉的 user API,僅僅是同時支持 fields 和 expand,swagger doc(或者其他 doc,如 RAML,json-schema)寫起來就無比繁雜。什麼?你從不寫類似於 swagger 的 doc?那麼客戶端怎麼探索 API 的 capability?
API 的版本處理和老版本的淘汰就不說了,都是淚 —— 這個世界上總有抵制進步,寧死不升級的 iOS / android 用戶。
GraphQL API
REST 在現實世界裡遇到的諸多問題使 GraphQL 應運而生。作為一個 API 的查詢語言,GraphQL 從產品的角度出發,希望 API 足夠靈活能處理複雜多變的用戶場景。以上的 user API 可以這麼訪問:
{ user(id: 1) { first last email bookmarks { id title description poster url } total_view_time }}
如果產品的某部分不需要訪問 bookmarks,可以這麼查詢:
{ user(id: 1) { first last email total_view_time }}
為了達到這一目標,GraphQL 定義了一套完整的類型系統。伺服器通過定義數據的類型告知客戶端伺服器的 capability,所以它也是一份 contract。REST API 體系本身 及其各個 framework 都沒有定義一套合適的類型系統,這就催生了很多零散的,不完善的實現,或者依託於類似於 swagger 這樣的工具的實現。GraphQL 還定義了一套嚴謹的查詢語言。REST API 在此毫無建樹,基本上 API 的 querystring / body 沒有太多章法可循,大家隨遇而安。由此,GraphQL 可以很容易地通過類型系統和用戶定義的 schema 生成強大的驗證工具,保證 query 是正確的,且滿足伺服器的 capability。
(GraphQL 的查詢器,可自動補全)我們看看處理上述查詢的伺服器的示例代碼:
const UserType = new GraphQLObjectType({ name: "user", description: "User profile", fields: () => ({ first: { type: GraphQLString } last: { type: GraphQLString } email: { type: GraphQLString } username: { type: GraphQLString } bookmarks: { type: GraphQLList, resolve: user => user.getBookmarks() } total_view_time: { type: GraphQLFloat, resolve: user => fetch(URL + user.id).then(res => res.json()) } })});const QueryType = new GraphQLObjectType({ name: "Query", description: "The root of all... queries", fields: () => ({ user: { type: UserType, args: { id: {type: new GraphQLNonNull(GraphQLID)}, }, resolve: (root, args) => UserModel.findById(args.id), }, }),});export default new GraphQLSchema({ query: QueryType,});
上述的代碼很好理解,我就不詳述了。值得注意的是,一個 field 的 resolve 函數返回的是一個 Promise,因此你可以做很多有意思的事情,比如說,這裡的 total_view_time 來自於內部的一個服務。從這個角度來講,GraphQL 對於異構的數據源能夠很好很簡練地處理。傳統的 REST API 並非不能處理異構的數據源,只不過那樣的代碼撰寫起來可讀性會比較差。從這個角度看,GraphQL 很適合作為一層薄薄的 API gateway,成為客戶端和各種內部系統(包括 REST API)的一個橋樑。
前面講到 GraphQL 的客戶端可以很靈活地在伺服器能力範圍內進行各種查詢的組合,這種能力對向後兼容和版本控制很有好處。REST API 在進化的過程中往往隨著 API 版本的變遷,而 GraphQL API 基本沒這個必要。對於同一 API,伺服器只需要添加新的 field,新的客戶端查詢時使用新的 field 即可,不會影響老客戶端。這是一種很優雅的進化方案。
注意事項
講了很多 GraphQL 的好,接下來講講用 GraphQL 做 API 的注意事項。
使用 GraphQL 並不意味著能提高 API 效率
GraphQL 只定義了 API 的 UI 部分,是否比 REST API 高效取決於實現的方式。事實上,不經優化 的 GraphQL API 的性能一般會比 REST API 低。因為 GraphQL 每個 field 單獨 resolve,很容易出現 N+1 query。nodejs 的 GraphQL 實現考慮了這個問題,提供了 loader 允許更高效的查詢方式。所以,在 resolve 的時候,一定要合理地使用 loader。
另外,由於很多場合下 GraphQL 和 Relay 被同時提及,所以有人把 Relay 的能力附著於 GraphQL 上,以為 partial data 的載入是 GraphQL 的功勞,其實不然。使用 Relay 會給客戶端和伺服器都帶來複雜性,如果剛開始使用 GraphQL,不建議直接引入 Relay。飯要一口口吃。
GraphQL 的靈活性是把雙刃劍
日子是問題疊著問題過的,軟體的架構是 tradeoff 疊著 tradeoff 組織的。GraphQL 的強大靈活的查詢能力雖然讓人大流口水,但隱含著很多安全上和性能上的問題。假設 user 有 friends,客戶端可以這麼查詢:
{ user(id: 1) { first last email friends { first friends { first { friends { ... } } } } }}
這樣的查詢在 REST API 里是不可能出現的,但在 GraphQL 中是合理的 query,而且不需要你寫任何代碼,查詢器就會忠實地一次次執行你的 resolve 函數,直到把系統內存耗盡或者棧溢出。所以,雖然理論上查詢可以無極嵌套,但真正部署要指定 nested 的上限,並且規定每個 API 的 timeout,超過 timeout 殺掉這個查詢。
舊有的緩存機制可能會失效
在 REST API 的世界,我們可以使用 nginx cache 或者 HA proxy 在 load balancer 級進行 API 的緩存 —— 如果 API 是冪等的,那麼,同樣的輸入會得到同樣的輸出,因此可以緩存。在使用 GraphQL 之後,由於 query 一般會很大,所以其 query 都是以 POST 的方式提交,POST 並非冪等,因此原有的緩存機制會失效。如果你的 API 的性能很依賴 load balancer 級別的緩存,需要特別注意。
使用 GraphQL 可能會增加實現的複雜度
對於序員來說,使用 GraphQL 意味著她又要學習一門新的東東。原本的一些簡單的 CRUD 的 API,在 GraphQL 下,變得複雜起來。
總結
GraphQL 如今是一門很成熟的技術了,幾乎所有的語言都對其有所支持。如果要採用 GraphQL,一定要注意要控制其靈活性,並做好性能的 benchmark。如果想要將已有的 API 系統遷移到 GraphQL,初期可以使用 GraphQL 包裝已有的 REST API,讓客戶端工程師盡情試驗。隨後再根據重要性和緊迫程度逐步一個個重寫已有的 API,切忌一上來就全部推到重寫。
推薦閱讀:
※邀請你共同開發WeChat API
※想問一下什麼是API介面,具體是什麼意思?
※國內提供話費充值API的公司有哪些?
※為什麼Google play 沒有手機 QQ 標準版?
※Twitter 不再鼓勵第三方客戶端發展,這對我們有什麼「啟示」嗎?