30分鐘理解GraphQL核心概念

寫在前面

在上一篇文章RPC vs REST vs GraphQL中,對於這三者的優缺點進行了比較宏觀的對比,並且咱們也會發現,通常比較簡單的項目其實並不須要GraphQL,可是咱們仍然須要對新的技術有必定的瞭解和掌握,在新技術普及時纔不會措手不及。前端

這篇文章主要介紹一些我接觸GraphQL的這段時間,以爲須要瞭解的比較核心的概念,比較適合一下人羣:node

  • 據說過GraphQL的讀者,想深刻了解一下
  • 想系統地學習GraphQL的讀者
  • 正在調研GraphQL技術的讀者

這些概念並不侷限於服務端或者是客戶端,若是你熟悉這些概念,在接觸任意使用GraphQL做爲技術背景的庫或者框架時,均可以經過文檔很快的上手。ios

若是你已經GraphQL應用於了實際項目中,那麼這篇文章可能不適合你,由於其中並無包含一些實踐中的總結和經驗,關於實踐的東西我會在以後再單另寫一篇文章總結。web

什麼是GraphQL

介紹GraphQL是什麼的文章網上一搜一大把,篇幅有長有短,可是從最核心上講,它是一種查詢語言,再進一步說,是一種API查詢語言。數據庫

這裏可能有的人就會說,什麼?API還能查?API不是用來調用的嗎?是的,這正是GraphQL的強大之處,引用官方文檔的一句話:django

ask exactly what you want.

咱們在使用REST接口時,接口返回的數據格式、數據類型都是後端預先定義好的,若是返回的數據格式並非調用者所指望的,做爲前端的咱們能夠經過如下兩種方式來解決問題:編程

  • 和後端溝通,改接口(更改數據源)
  • 本身作一些適配工做(處理數據源)

通常若是是我的項目,改後端接口這種事情能夠隨意搞,可是若是是公司項目,改後端接口每每是一件比較敏感的事情,尤爲是對於三端(web、andriod、ios)公用同一套後端接口的狀況。大部分狀況下,均是按第二種方式來解決問題的。segmentfault

所以若是接口的返回值,能夠經過某種手段,從靜態變爲動態,即調用者來聲明接口返回什麼數據,很大程度上能夠進一步解耦先後端的關聯。後端

在GraphQL中,咱們經過預先定義一張Schema和聲明一些Type來達到上面說起的效果,咱們須要知道:api

  • 對於數據模型的抽象是經過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等。

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

歡迎關注公衆號 全棧101,只談技術,不談人生
clipboard.png
相關文章
相關標籤/搜索