30分鐘理解GraphQL核心概念

HaoliangWu發表於2018-04-02

寫在前面

在上一篇文章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等。

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

相關文章