當前位置:
首頁 > 最新 > 阻礙你使用 GraphQL 的十個問題

阻礙你使用 GraphQL 的十個問題

作者: 鄒潤陽

http://jerryzou.com/posts/10-questions-about-graphql/

在最近半年中,LeetCode 的一些新功能已經開始嘗試使用 GraphQL。如果你在 LeetCode 網站中查看一下開發者工具中收集到的請求,也許會發現一個與眾不同的請求POST https://leetcode.com/graphql/。是的,要是你想給 LeetCode 寫些爬蟲的話,可得好好研究一下我們在這個 Endpoint 下掛載了哪些數據。

我在使用 GraphQL 的過程中經歷了理解、誤解、再理解的過程。作為吃了 GraphQL 這個螃蟹的人,希望能夠通過這篇文章更好地幫助各位讀者理解 GraphQL 是什麼,GraphQL 會給你帶來什麼,以及將 GraphQL 應用於你的系統中需要注意哪些問題。這不是一篇系統的科普文,如果你有什麼其他疑問可以評論中留言詢問。

以下內容皆為作者個人理解,如果錯誤,歡迎討論指出。我會隨時修正,拜謝!

1. 什麼是 GraphQL?

從官方的定義來說,GraphQL 是一種針對 API 的查詢語言;在我看來,GraphQL 是一種標準,而與標準相對的便是實現。就像 EcmaScript 與 JavaScript 的關係,從一開始你就需要有這樣一種認知:GraphQL 只定義了這種查詢語言語法如何、具體的語句如何執行等。但是,你在真正使用某種 GraphQL 的服務端實現時,是有可能發現 GraphQL 標準中所描述的特性尚未被實現;或者這種 GraphQL 的實現擴展了 GraphQL 標準所定義的內容。

舉例來說,就像 ES 2017 標準正式納入了 async/await,而從實現的角度上說,IE 沒有實現這一標準,而 Edge 16 和 Chrome 62 則實現了這一標準(數據來源於caniuse)說回 GraphQL 標準,與之相對的有相當多的伺服器端實現。他們的大多遵循 GraphQL 標準來實現,但也可能稍有差別,這一切需要你自己去探索。

2. 如何從零入門 GraphQL?

先看標準:http://graphql.org/(純 GraphQL,與任何什麼 JavaScript, Python 都無關)

再看服務端實現:到http://graphql.org/code/里找到跟你的服務端技術有關的實現

再看客戶端實現:Relay或appollo-client, etc.

學習使用 DataLoader 來獲取列表數據

3. GraphQL 與 RESTful 有什麼區別?

首先放上一張來自於 graphql.org 的圖片。REST 與 GraphQL 都是服務端所承載的系統對外的服務介面,因此兩者肯定是可以共存的,甚至可以共用一套 Authorization 等業務邏輯。

那麼 GraphQL 與 RESTful 的具體區別有什麼呢?我覺得主要是兩點。

1. 入口 (entry point)

RESTful 的核心理念在於資源 (resource),且講究一個 RESTful 介面僅操作單一資源;因此在你使用 RESTful 時,會設計出大量的介面。GraphQL 是單一入口,一般配置在[host]/graphql/,所有的資源都從該入口通過 graphql 的語句獲取或修改(當然 GraphQL 亦支持多入口,但顯然多入口的數量也遠小於 RESTful)。

2. 數據的關聯性

RESTful 所操作的資源相對是離散的;而 GraphQL 的數據更有整體性。

舉個例子,如果要獲取 A 的朋友的朋友,用 RESTful 該怎麼做呢?

假設我們有這樣一個介面:

GET/user/:userId/friends/

而 A 有 20 個好朋友,那麼我們總共需要發送 20 + 1 = 21 次 REST 請求。

或者我們為了這種特殊場景設計出以下介面:

GET/user/:userId/friendsAndHisFriends/

emmmmm… 雖然看起來很彆扭,但是只需要一次請求呢!

那麼在 GraphQL 中,怎麼做呢?

首先我們需要給 User 定義 Schema (GraphQL 有一套完整的類型系統):

type User{

id:ID!

name:String!

friends:[User]

}

假設我們在 Graph root 上只掛了一個 Node,叫 user:

type Query{

user(id:ID!):User

}

那麼我們從客戶端發送的 query 就可以寫成這樣:

query($userId:ID){

user(id:$userId){

name

friends{

name

friends{

name

}

}

}

}

最後這一個請求就搞定查詢朋友的朋友這個蛋疼的需求啦!機智的你肯定已經發現了:這個 query 是不是可以無限循環地寫下去?你想的沒錯,確實可以這麼干!在 GraphQL 的官網上是這麼形容自己的:

It』s Graphs All the Way Down*

它就像是一顆無限向下延伸的樹。所以在我看來,GraphQL 更應該叫 TreeQL,當然在圖論里,Tree 就是 Graph 也沒毛病啦。需要注意的是,這也會引出 「N + 1 problem」 的話題——naive 的 GraphQL 服務端實現會讓這段 query 變得異常慢!

怎麼解決這個棘手的問題?心急的小夥伴請跳轉到6.1 N+1 問題!

4. GraphQL 能做到修改數據嗎?

看了上面的 query 的例子,你肯定很好奇,graphql 這種看上去好像只為查詢而存在的語言,是不是有辦法做到修改數據呢?

答案是:能。

僅僅為了使得 GraphQL 這個 data platform變得更加完整,GraphQL 標準中加入了一種操作符,名為mutation。因為我覺得這種設計確實比較一般,此處就不舉例了,詳情見:http://graphql.org/learn/queries/#mutations

5. GraphQL 與 RESTful 相比有什麼優點?

1. 數據的關聯性和結構化更好

這一點已經在本文的第 3 個問題中有所描述。

2. 更易於前端緩存數據

這個一般像 Relay 和 apollo-client 都替你做好了,如果你想了解它的緩存原理,請移步GraphQL Caching

3. Versionless API

相比於 RESTful 為了兼容新老客戶端所添加的版本號,在 GraphQL 中,如果你需要服務端提供與以前不一樣的行為,只需要修改 root Query 的定義,在上面額外添加你想要的 Node 即可。

4. 更健壯的介面

不用再因為在缺乏溝通的情況下修改介面,而為系統埋下不穩定的定時炸彈。一切面向前端的介面都有強類型的 Schema 做保證,且完整類型定義因introspection完全對前端可見,一旦前端發送的 query 與 Schema 不符,能快速感知到產生了錯誤。

5. 令人期待的 subscription

如何在瀏覽器中接受服務端的推送信息是個老生常談的問題。從最初的輪詢,到後來的 WebSocket。如今 GraphQL 也計劃引入除了query,mutation以外的第三種操作符subscription,以便於直接接受伺服器推送數據。在 2015 年底 GraphQL 官方發布了一篇博文:Subscriptions in GraphQL and Relay來介紹subscription在他們的 iOS 和 Android App 中的應用。可惜的是相關的代碼仍未開源,目前開源社區能找到的解決方案目前只有 Apollo 社區為 Node.js 寫的graphql-subscriptions。

6. GraphQL 有什麼缺點?

說了 GraphQL 的那麼多優點,那麼它有沒有缺點呢?當然也是有的。

6.1. N+1 問題

最大的問題莫過於:在實現 GraphQL 服務端介面時,很容易就能寫出效率極差的代碼,引起 「N+1 問題」。

什麼是 N+1 問題?首先我們來舉個簡單的例子:

這是一段簡單的 python 代碼,使用了 Django 的 QuerySet 來從資料庫抓取數據。假設我們的資料庫中有兩張表 User 和 UserScore 這兩張表的關係如下所示:

由於用戶的分數並沒有保存在 User 表中,又因為 QuerySet 有 lazy load 的特性,所以在 for 循環中,每一次獲取user.score都會查一次表,最終原本 1 次資料庫查詢能搞定的問題,卻在不恰當的實現中產生了 N+1 次對資料庫的訪問。

相對於 RESTful,在 GraphQL 中更加容易引起 N+1 問題。主要是由於 GraphQL query 的逐層解析方式所引起的,關於 GraphQL 如何執行 query 的細節,可以參閱Graphql Execution。

6.2. 如何解決在 GraphQL 中的 N + 1 問題?

以下解決方案僅針對關係型資料庫,如果你的項目中使用的是 NoSQL,可能解決方案有較大差別。

針對一對一的關係(比如我上面舉例中提到的這個 User 與 UserScore 的關係),在從資料庫里抓取數據時,就將所需數據 join 到一張表裡。別等著 ORM 框架替你懶載入那些你需要的數據。

針對多對一或者多對多的關係,你就要用到一個叫做DataLoader的工具庫了。其中,Facebook 為 Node.js 社區提供了DataLoader 的實現。DataLoader 的主要功能是 batching & caching,可以將多次資料庫查詢的請求合併為一個,同時已經載入過的數據可以直接從 DataLoader 的緩存空間中獲取到。這個話題要是展開說也得寫一篇新的文章了,此處不多贅述。

7. 有什麼可以快樂地調試 GraphQL 介面的方法?

GraphiQL/live demo

使用 GraphiQL 可以很容易地讓人感受到「代碼即文檔」的快樂。

8. 如何選擇 GraphQL 的客戶端實現?

客戶端常見的框架有Relay和Apollo Client。Relay 是由 Facebook 官方提供的解決方案,而 Apollo 則是在 GraphQL 方面熱度超高的社區。

使用 Relay 的好處是,許多 GraphQL 的服務端框架都會支持 Relay 的標準(比如數據的分頁介面)。而 apollo-client 的實現其實又在諸多方面宣稱自己兼容 Relay 的風格,所以使用起來應該也是大同小異。當然筆者並沒有真實地使用過 Relay,在 GraphQL 方面的經驗也不夠深刻,所以也不好妄下斷言。

做技術選型時,同事 Ashish 說擔心 Relay 太過於重量級,所以並沒有決定使用 Relay。這種選擇的正確性尚待考證,目前在 LeetCode 實際項目中使用了 apollo-client。

9. GraphQL 中是怎麼處理分頁的?

這是一個可能讓 GraphQL 初學者擔憂的問題,又是一個可以從官方文檔中找到答案的問題。

再以找朋友為例,借用 SQL 查詢中常用的篩選方法,分頁介面可以設計為:

query($userId:ID){

user(id:$userId){

name

friends(first:2,offset:3){

name

}

}

}

上面的例子意為從 $userId 的第 4 個朋友開始算起,取前 2 個朋友。

類似地,分頁介面還可以設計為

friends(first: 2, after: $friendId);

friends(first: 2, after: $friendCursor)

無論分頁介面設計成怎麼樣,都需要前後端共同的封裝與支持。其中 Relay 風格的分頁介面在各大前後端 GraphQL 框架中基本都已有比較完整的實現。

9.1 Relay 風格的分頁介面

參閱Relay Cursor Connections Specification

註:apollo-client 兼容該分頁介面

query{

user{

name

friends(first:2,after:$cursor){

edges{

cursor

node{

id

name

}

}

pageInfo{

hasNextPage

}

}

}

}

10. GraphQL 中是怎麼實現用戶校驗的?

你可以回看一下3. GraphQL 與 RESTful 有什麼區別中展示的圖片,答案就在其中:Authentication 屬於業務邏輯層乾的事情,別讓 GraphQL 承擔太多工作啦。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 前端大全 的精彩文章:

加速 Webpack

TAG:前端大全 |