寫在前面
在上一篇文章RPC vs REST vs GraphQL中,對於這三者的優缺點進行了比較巨集觀的對比,而且我們也會發現,一般比較簡單的專案其實並不需要GraphQL,但是我們仍然需要對新的技術有一定的瞭解和掌握,在新技術普及時才不會措手不及。
這篇文章主要介紹一些我接觸GraphQL的這段時間,覺得需要了解的比較核心的概念,比較適合一下人群:
- 聽說過GraphQL的讀者,想深入瞭解一下
- 想系統地學習GraphQL的讀者
- 正在調研GraphQL技術的讀者
這些概念並不侷限於服務端或者是客戶端,如果你熟悉這些概念,在接觸任意使用GraphQL作為技術背景的庫或者框架時,都可以通過文件很快的上手。
如果你已經GraphQL應用於了實際專案中,那麼這篇文章可能不適合你,因為其中並沒有包含一些實踐中的總結和經驗,關於實踐的東西我會在之後再單另寫一篇文章總結。
什麼是GraphQL
介紹GraphQL是什麼的文章網上一搜一大把,篇幅有長有短,但是從最核心上講,它是一種查詢語言,再進一步說,是一種API查詢語言。
這裡可能有的人就會說,什麼?API還能查?API不是用來呼叫的嗎?是的,這正是GraphQL的強大之處,引用官方文件的一句話:
ask exactly what you want.
我們在使用REST介面時,介面返回的資料格式、資料型別都是後端預先定義好的,如果返回的資料格式並不是呼叫者所期望的,作為前端的我們可以通過以下兩種方式來解決問題:
- 和後端溝通,改介面(更改資料來源)
- 自己做一些適配工作(處理資料來源)
一般如果是個人專案,改後端介面這種事情可以隨意搞,但是如果是公司專案,改後端介面往往是一件比較敏感的事情,尤其是對於三端(web、andriod、ios)公用同一套後端介面的情況。大部分情況下,均是按第二種方式來解決問題的。
因此如果介面的返回值,可以通過某種手段,從靜態變為動態,即呼叫者來宣告介面返回什麼資料,很大程度上可以進一步解耦前後端的關聯。
在GraphQL中,我們通過預先定義一張Schema
和宣告一些Type
來達到上面提及的效果,我們需要知道:
- 對於資料模型的抽象是通過Type來描述的
- 對於介面獲取資料的邏輯是通過Schema來描述的
這麼說可能比較抽象,我們一個一個來說明。
Type
對於資料模型的抽象是通過Type來描述的,每一個Type有若干Field組成,每個Field又分別指向某個Type。
GraphQL的Type簡單可以分為兩種,一種叫做Scalar Type(標量型別)
,另一種叫做Object Type(物件型別)
。
Scalar Type
GraphQL中的內建的標量包含,String
、Int
、Float
、Boolean
、Enum
,對於熟悉程式語言的人來說,這些都應該很好理解。
值得注意的是,GraphQL中可以通過Scalar
宣告一個新的標量,比如:
- prisma(一個使用GraphQL來抽象資料庫操作的庫)中,還有
DateTime
和ID
這兩個標量分別代表日期格式和主鍵 - 在使用GraphQL實現檔案上傳介面時,需要宣告一個
Upload
標量來代表要上傳的檔案
總之,我們只需要記住,標量是GraphQL型別系統中最小的顆粒,關於它在GraphQL解析查詢結果時,我們還會再提及它。
Object Type
僅有標量是不夠的抽象一些複雜的資料模型的,這時候我們需要使用物件型別,舉個例子(先忽略語法,僅從字面上看):
type Article {
id: ID
text: String
isPublished: Boolean
}
複製程式碼
上面的程式碼,就宣告瞭一個Article
型別,它有3個Field,分別是ID
型別的id,String
型別的text和Boolean
型別的isPublished。
對於物件型別的Field的宣告,我們一般使用標量,但是我們也可以使用另外一個物件型別,比如如果我們再宣告一個新的User
型別,如下:
type User {
id: ID
name: String
}
複製程式碼
這時我們就可以稍微的更改一下關於Article
型別的宣告程式碼,如下:
type Article {
id: ID
text: String
isPublished: Boolean
author: User
}
複製程式碼
Article
新增的author
的Field是User
型別, 代表這篇文章的作者。
總之,我們通過物件模型來構建GraphQL中關於一個資料模型的形狀,同時還可以宣告各個模型之間的內在關聯(一對多、一對一或多對多)。
Type Modifier
關於型別,還有一個較重要的概念,即型別修飾符,當前的型別修飾符有兩種,分別是List
和Required
,它們的語法分別為[Type]
和Type!
, 同時這兩者可以互相組合,比如[Type]!
或者[Type!]
或者[Type!]!
(請仔細看這裡!
的位置),它們的含義分別為:
- 列表本身為必填項,但其內部元素可以為空
- 列表本身可以為空,但是其內部元素為必填
- 列表本身和內部元素均為必填
我們進一步來更改上面的例子,假如我們又宣告瞭一個新的Comment
型別,如下:
type Comment {
id: ID!
desc: String,
author: User!
}
複製程式碼
你會發現這裡的ID
有一個!
,它代表這個Field是必填的,再來更新Article
物件,如下:
type Article {
id: ID!
text: String
isPublished: Boolean
author: User!
comments: [Comment!]
}
複製程式碼
我們這裡的作出的更改如下:
- id欄位改為必填
- author欄位改為必填
- 新增了comments欄位,它的型別是一個元素為Comment型別的List型別
最終的Article
型別,就是GraphQL中關於文章這個資料模型,一個比較簡單的型別宣告。
Schema
現在我們開始介紹Schema
,我們之前簡單描述了它的作用,即它是用來描述對於介面獲取資料邏輯
的,但這樣描述仍然是有些抽象的,我們其實不妨把它當做REST架構中每個獨立資源的uri
來理解它,只不過在GraphQL中,我們用Query來描述資源的獲取方式。因此,我們可以將Schema
理解為多個Query組成的一張表。
這裡又涉及一個新的概念Query
,GraphQL中使用Query
來抽象資料的查詢邏輯,當前標準下,有三種查詢型別,分別是query(查詢)、mutation(更改)和subscription(訂閱)。
Note: 為了方便區分,Query
特指GraphQL中的查詢(包含三種型別),query
指GraphQL中的查詢型別(僅指查詢型別)
Query
上面所提及的3中基本查詢型別是作為Root Query(根查詢)
存在的,對於傳統的CRUD專案,我們只需要前兩種型別就足夠了,第三種是針對當前日趨流行的real-time
應用提出的。
我們按照字面意思來理解它們就好,如下:
- query(查詢):當獲取資料時,應當選取Query型別
- mutation(更改):當嘗試修改資料時,應當使用mutation型別
- subscription(訂閱):當希望資料更改時,可以進行訊息推送,使用subscription型別
仍然以一個例子來說明。
首先,我們分別以REST和GraphQL的角度,以Article
為資料模型,編寫一系列CRUD的介面,如下:
Rest 介面
GET /api/v1/articles/
GET /api/v1/article/:id/
POST /api/v1/article/
DELETE /api/v1/article/:id/
PATCH /api/v1/article/:id/
複製程式碼
GraphQL Query
query {
articles(): [Article!]!
article(id: Int): Article!
}
mutation {
createArticle(): Article!
updateArticle(id: Int): Article!
deleteArticle(id: Int): Article!
}
複製程式碼
對比我們較熟悉的REST的介面我們可以發現,GraphQL中是按根查詢的型別來劃分Query職能的,同時還會明確的宣告每個Query所返回的資料型別,這裡的關於型別的語法和上一章節中是一樣的。需要注意的是,我們所宣告的任何Query
都必須是Root Query
的子集,這和GraphQL內部的執行機制有關。
例子中我們僅僅宣告瞭Query型別和Mutation型別,如果我們的應用中對於評論列表有real-time
的需求的話,在REST中,我們可能會直接通過長連線或者通過提供一些帶驗證的獲取長連線url的介面,比如:
POST /api/v1/messages/
複製程式碼
之後長連線會將新的資料推送給我們,在GraphQL中,我們則會以更加宣告式的方式進行宣告,如下
subscription {
updatedArticle() {
mutation
node {
comments: [Comment!]!
}
}
}
複製程式碼
我們不必糾結於這裡的語法,因為這篇文章的目的不是讓你在30分鐘內學會GraphQL的語法,而是理解的它的一些核心概念,比如這裡,我們就宣告瞭一個訂閱Query,這個Query會在有新的Article被建立或者更新時,推送新的資料物件。當然,在實際執行中,其內部實現仍然是建立於長連線之上的,但是我們能夠以更加宣告式的方式來進行宣告它。
Resolver
如果我們僅僅在Schema中宣告瞭若干Query,那麼我們只進行了一半的工作,因為我們並沒有提供相關Query所返回資料的邏輯。為了能夠使GraphQL正常工作,我們還需要再瞭解一個核心概念,Resolver(解析函式)
。
GraphQL中,我們會有這樣一個約定,Query和與之對應的Resolver是同名的,這樣在GraphQL才能把它們對應起來,舉個例子,比如關於articles(): [Article!]!
這個Query, 它的Resolver的名字必然叫做articles
。
在介紹Resolver之前,是時候從整體上了解下GraphQL的內部工作機制了,假設現在我們要對使用我們已經宣告的articles
的Query,我們可能會寫以下查詢語句(同樣暫時忽略語法):
Query {
articles {
id
author {
name
}
comments {
id
desc
author
}
}
}
複製程式碼
GraphQL在解析這段查詢語句時會按如下步驟(簡略版):
- 首先進行第一層解析,當前
Query
的Root Query
型別是query
,同時需要它的名字是articles
- 之後會嘗試使用
articles
的Resolver
獲取解析資料,第一層解析完畢 - 之後對第一層解析的返回值,進行第二層解析,當前
articles
還包含三個子Query
,分別是id
、author
和comments
- id在Author型別中為標量型別,解析結束
- author在Author型別中為物件型別User,嘗試使用
User
的Resolver
獲取資料,當前field解析完畢 - 之後對第二層解析的返回值,進行第三層解析,當前
author
還包含一個Query
,name
,由於它是標量型別,解析結束 - comments同上...
我們可以發現,GraphQL大體的解析流程就是遇到一個Query之後,嘗試使用它的Resolver取值,之後再對返回值進行解析,這個過程是遞迴的,直到所解析Field的型別是Scalar Type(標量型別)
為止。解析的整個過程我們可以把它想象成一個很長的Resolver Chain(解析鏈)。
這裡對於GraphQL的解析過程只是很簡單的概括,其內部執行機制遠比這個複雜,當然這些對於使用者是黑盒的,我們只需要大概瞭解它的過程即可。
Resolver本身的宣告在各個語言中是不一樣的,因為它代表資料獲取的具體邏輯。它的函式簽名(以js為例子)如下:
function(parent, args, ctx, info) {
...
}
複製程式碼
其中的引數的意義如下:
- parent: 當前上一個Resolver的返回值
- args: 傳入某個Query中的函式(比如上面例子中
article(id: Int)
中的id
) - ctx: 在Resolver解析鏈中不斷傳遞的中間變數(類似中介軟體架構中的context)
- info: 當前Query的AST物件
值得注意的是,Resolver內部實現對於GraphQL完全是黑盒狀態。這意味著Resolver如何返回資料、返回什麼樣的資料、從哪返回資料,完全取決於Resolver本身,基於這一點,在實際中,很多人往往把GraphQL作為一箇中間層來使用,資料的獲取通過Resolver來封裝,內部資料獲取的實現可能基於RPC、REST、WS、SQL等多種不同的方式。同時,基於這一點,當你在對一些未使用GraphQL的系統進行遷移時(比如REST),可以很好的進行增量式遷移。
總結
大概就這麼多,首先感謝你耐心的讀到這裡,雖然題目是30分鐘熟悉GraphQL核心概念,但是可能已經超時了,不過我相信你對GraphQL中的核心概念已經比較熟悉了。但是它本身所涉及的東西遠遠比這個豐富,同時它還處於飛速的發展中。
最後我嘗試根據這段時間的學習GraphQL的經驗,提供一些進一步學習和了解GraphQL的方向和建議,僅供參考:
想進一步瞭解GraphQL本身
我建議再仔細去官網,讀一下官方文件,如果有興趣的話,看看GraphQL的spec也是極好的。這篇文章雖然介紹了核心概念,但是其他一些概念沒有涉及,比如Union、Interface、Fragment等等,這些概念均是基於核心概念之上的,在瞭解核心概念後,應當會很容易理解。
偏向服務端
偏向服務端方向的話,除了需要進一步瞭解GraphQL在某個語言的具體生態外,還需要了解一些關於快取、上傳檔案等特定方向的東西。如果是想做系統遷移,還需要對特定的框架做一些調研,比如graphene-django。
如果是想使用GraphQL本身做系統開發,這裡推薦瞭解一個叫做prisma的框架,它本身是在GraphQL的基礎上構建的,並且與一些GraphQL的生態框架相容性也較好,在各大程式語言也均有適配,它本身可以當做一個ORM來使用,也可以當做一個與資料庫互動的中間層來使用。
偏向客戶端
偏向客戶端方向的話,需要進一步瞭解關於graphql-client的相關知識,我這段時間瞭解的是apollo,一個開源的grapql-client框架,並且與各個主流前端技術棧如Angular、React等均有適配版本,使用感覺良好。
同時,還需要了解一些額外的查詢概念,比如分頁查詢中涉及的Connection、Edge等。
大概就這麼多,如有錯誤,還望指正。