30分鐘理解GraphQL核心概念

NO IMAGE

寫在前面

在上一篇文章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中的內建的標量包含,StringIntFloatBooleanEnum,對於熟悉編程語言的人來說,這些都應該很好理解。

值得注意的是,GraphQL中可以通過Scalar聲明一個新的標量,比如:

  • prisma(一個使用GraphQL來抽象數據庫操作的庫)中,還有DateTimeID這兩個標量分別代表日期格式和主鍵
  • 在使用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

關於類型,還有一個較重要的概念,即類型修飾符,當前的類型修飾符有兩種,分別是ListRequired,它們的語法分別為[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在解析這段查詢語句時會按如下步驟(簡略版):

  • 首先進行第一層解析,當前QueryRoot Query類型是query,同時需要它的名字是articles
  • 之後會嘗試使用articlesResolver獲取解析數據,第一層解析完畢
  • 之後對第一層解析的返回值,進行第二層解析,當前articles還包含三個子Query,分別是idauthorcomments
    • id在Author類型中為標量類型,解析結束
    • author在Author類型中為對象類型User,嘗試使用UserResolver獲取數據,當前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等。

大概就這麼多,如有錯誤,還望指正。

相關文章

高級vue組件模式2

高級vue組件模式1

30分鐘理解CORB是什麼

【譯】AngularElements及其運作原理