GraphQL 基本概念

查詢和變更

一個 GraphQL 服務是通過定義類型和類型上的字段來創建的,然後給每個類型上的每個字段提供解析函數。例如,一個 GraphQL 服務告訴我們當前登錄用戶是 me,這個用戶的名稱可能像這樣:

// 定義類型,類似於Java的對象定義 
type Query {
  me: User
}

// 定義類型上的字段
type User {
  id: ID
  name: String
}

每個類型上字段的解析函數:

function Query_me(request) {
  return request.auth.user;
}

function User_name(user) {
  return user.getName();
}

一旦一個 GraphQL 服務運行起來(通常在 web 服務的一個 URL 上),它就能接收 GraphQL 查詢,並驗證和執行。接收到的查詢首先會被檢查確保它只引用了已定義的類型和字段,然後運行指定的解析函數來生成結果。

字段

簡單而言,GraphQL 是關於請求對象上的特定字段。我們以一個非常簡單的查詢以及其結果爲例:

// 查詢 hero 對象上的 name 字段
{
  hero {
    name
  }
}

// 返回的結果,結構與查詢字段一致
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

對對象的字段進行次級選擇(sub-selection)。GraphQL 查詢能夠遍歷相關對象及其字段,使得客戶端可以一次請求查詢大量相關數據,而不像傳統 REST 架構中那樣需要多次往返查詢。

{
  hero {
    name
    # 查詢可以有備註!
    //次級查詢,可以遍歷該字段
    friends { 
      name
    }
  }
}

// 結果
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

參數

// 查詢參數 id 是 1000 的那個人
{
  human(id: "1000") {
    name
    height
  }
}

// 結果
{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 1.72
    }
  }
}

在 GraphQL 中,每一個字段和嵌套對象都能有自己的一組參數,從而使得 GraphQL 可以完美替代多次 API 獲取請求。甚至你也可以給 標量(scalar)字段傳遞參數,用於實現服務端的一次轉換,而不用每個客戶端分別轉換。

{
  human(id: "1000") { //對象的參數
    name
    height(unit: FOOT) //字段的參數,枚舉類型, GraphQL自帶一套默認類型,也可以聲明自己的定製類型
  }
}

{
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 5.6430448
    }
  }
}

別名

別名,我們沒法通過不同參數來查詢相同字段,你可以通過重命名結果中的字段爲任意你想到的名字使結果字段不會衝突。

{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}

// 結果
{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

片段

片段使你能夠組織一組字段,然後在需要它們的的地方引入。下面例子展示瞭如何使用片段解決上述場景:

{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

// 結果
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "friends": [
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        },
        {
          "name": "C-3PO"
        },
        {
          "name": "R2-D2"
        }
      ]
    },
    "rightComparison": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

你可以看到上面的查詢如何漂亮地重複了字段。片段的概念經常用於將複雜的應用數據需求分割成小塊,特別是你要將大量不同片段的 UI 組件組合成一個初始數據獲取的時候。

在片段內使用變量

query HeroComparison($first: Int = 3) { // 定義片段中的變量,變量值是3
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

// 結果
{
  "data": {
    "leftComparison": {
      "name": "Luke Skywalker",
      "friendsConnection": {
        "totalCount": 4,
        "edges": [
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          },
          {
            "node": {
              "name": "C-3PO"
            }
          }
        ]
      }
    },
    "rightComparison": {
      "name": "R2-D2",
      "friendsConnection": {
        "totalCount": 3,
        "edges": [
          {
            "node": {
              "name": "Luke Skywalker"
            }
          },
          {
            "node": {
              "name": "Han Solo"
            }
          },
          {
            "node": {
              "name": "Leia Organa"
            }
          }
        ]
      }
    }
  }
}

操作名稱

操作類型可以是 query、mutation 或 subscription,描述你打算做什麼類型的操作。操作類型是必需的,除非你使用查詢簡寫語法,在這種情況下,你無法爲操作提供名稱或變量定義。

query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

變量

GraphQL 擁有一級方法將動態值提取到查詢之外,然後作爲分離的字典傳進去。這些動態值即稱爲變量。

使用變量之前,我們得做三件事:

  1. 使用 $variableName 替代查詢中的靜態值。
  2. 聲明 $variableName 爲查詢接受的變量之一。
  3. 將 variableName: value 通過傳輸專用(通常是 JSON)的分離的變量字典中。

全部做完之後就像這個樣子:

# { "graphiql": true, "variables": { "episode": JEDI } }
query HeroNameAndFriends($episode: Episode) {
    hero(episode: $episode) {
        name
        friends {
            name
        }
    }
}

這樣一來,我們的客戶端代碼就只需要傳入不同的變量,而不用構建一個全新的查詢了。這事實上也是一個良好實踐,意味着查詢的參數將是動態的 —— 我們決不能使用用戶提供的值來字符串插值以構建查詢。

變量定義

變量定義看上去像是上述查詢中的 ($episode: Episode)。其工作方式跟類型語言中函數的參數定義一樣。它以列出所有變量,變量前綴必須爲 $,後跟其類型,本例中爲 Episode。

所有聲明的變量都必須是標量、枚舉型或者輸入對象類型。所以如果想要傳遞一個複雜對象到一個字段上,你必須知道服務器上其匹配的類型。

變量定義可以是可選的或者必要的。上例中,Episode 後並沒有 !,因此其是可選的。但是如果你傳遞變量的字段要求非空參數,那變量一定是必要的。

默認變量

可以通過在查詢中的類型定義後面附帶默認值的方式,將默認值賦給變量。

query HeroNameAndFriends($episode: Episode = "JEDI") {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }}

指令

query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

// Variables
{
  "episode": "JEDI",
  "withFriends": false
}

// Result
{
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

我們用了 GraphQL 中一種稱作指令的新特性。一個指令可以附着在字段或者片段包含的字段上,然後以任何服務端期待的方式來改變查詢的執行。GraphQL 的核心規範包含兩個指令,其必須被任何規範兼容的 GraphQL 服務器實現所支持:

  • @include(if: Boolean) 僅在參數爲 true 時,包含此字段。
  • @skip(if: Boolean) 如果參數爲 true,跳過此字段。

指令在你不得不通過字符串操作來增減查詢的字段時解救你。服務端實現也可以定義新的指令來添加新的特性。

變更

變更和 REST 中的創建和更新的功能類似,GraphQL的變更操作返回一個對象類型,你也能請求其嵌套字段,以此來獲取一個對象變更後的新狀態。

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

// Variables
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

// Result
{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

查詢字段時,是並行執行,而變更字段時,是線性執行,一個接着一個。

內聯片段

如果你查詢的字段返回的是接口或者聯合類型,那麼你可能需要使用內聯片段來取出下層具體類型的數據:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}
// Variable
{
  "ep": "JEDI"
}
// Result
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

這個查詢中,hero 字段返回 Character 類型,取決於 episode 參數,其可能是 Human 或者 Droid 類型。在直接選擇的情況下,你只能請求 Character 上存在的字段,譬如 name。如果要請求具體類型上的字段,你需要使用一個類型條件內聯片段。因爲第一個片段標註爲 … on Droid,primaryFunction 僅在 hero 返回的 Character 爲 Droid 類型時纔會執行。同理適用於 Human 類型的 height 字段。

元字段

某些情況下,你並不知道你將從 GraphQL 服務獲得什麼類型,這時候你就需要一些方法在客戶端來決定如何處理這些數據。GraphQL 允許你在查詢的任何位置請求__typename,一個元字段,以獲得那個位置的對象類型名稱。

{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}

{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo"
      },
      {
        "__typename": "Human",
        "name": "Leia Organa"
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1"
      }
    ]
  }
}

上面的查詢中,search 返回了一個聯合類型,其可能是三種選項之一。沒有__typename字段的情況下,幾乎不可能在客戶端分辨開這三個不同的類型。

Schema 和類型

類型系統

{
  hero {
    name
    appearsIn
  }
}

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}
  1. 我們以一個特殊的對象 “root” 開始
  2. 選擇其上的 hero 字段
  3. 對於 hero 返回的對象,我們選擇 name 和 appearsIn 字段

因爲一個 GraphQL 查詢的結構和結果非常相似,因此即便不知道服務器的情況,你也能預測查詢會返回什麼結果。但是一個關於我們所需要的數據的確切描述依然很有意義,我們能選擇什麼字段?服務器會返回哪種對象?這些對象下有哪些字段可用?這便是引入 schema 的原因。

每一個 GraphQL 服務都會定義一套類型,用以描述你可能從那個服務查詢到的數據。每當查詢到來,服務器就會根據 schema 驗證並執行查詢。

類型語言

我們定義了自己的簡單語言,稱之爲 “GraphQL schema language” —— 它和 GraphQL 的查詢語言很相似,讓我們能夠和 GraphQL schema 之間可以無語言差異地溝通。

對象類型和字段

一個 GraphQL schema 中的最基本的組件是對象類型,它就表示你可以從服務上獲取到什麼類型的對象,以及這個對象有什麼字段。
使用 GraphQL schema language,我們可以這樣表示它:

type Character {
  name: String!
  appearsIn: [Episode!]!
 }

雖然這語言可讀性相當好,但我們還是一起看看其用語,以便我們可以有些共通的詞彙:

  • Character 是一個 GraphQL 對象類型,表示其是一個擁有一些字段的類型。你的 schema 中的大多數類型都會是對象類型。
  • name 和 appearsIn 是 Character 類型上的字段。這意味着在一個操作 Character 類型的 GraphQL 查詢中的任何部分,都只能出現 name 和 appearsIn 字段。
  • String 是內置的標量類型之一 —— 標量類型是解析到單個標量對象的類型,無法在查詢中對它進行次級選擇。後面我們將細述標量類型。
  • String! 表示這個字段是非空的,GraphQL 服務保證當你查詢這個字段後總會給你返回一個值。在類型語言裏面,我們用一個感嘆號來表示這個特性。
  • [Episode!]! 表示一個 Episode 數組。因爲它也是非空的,所以當你查詢 appearsIn字段的時候,你也總能得到一個數組(零個或者多個元素)。且由於 Episode! 也是非空的,你總是可以預期到數組中的每個項目都是一個 Episode 對象。現在你知道一個 GraphQL 對象類型看上去是怎樣,也知道如何閱讀基礎的 GraphQL 類型語言了。

參數

GraphQL 對象類型上的每一個字段都可能有零個或者多個參數,例如下面的 length 字段:

type Starship {
  id: ID!
  name: String!
  length(unit: LengthUnit = METER): Float
}

所有參數都是具名的,不像 JavaScript 或者 Python 之類的語言,函數接受一個有序參數列表,而在 GraphQL 中,所有參數必須具名傳遞。本例中,length 字段定義了一個參數,unit。

參數可能是必選或者可選的,當一個參數是可選的,我們可以定義一個默認值 —— 如果 unit 參數沒有傳遞,那麼它將會被默認設置爲 METER。

查詢和變更類型

你的 schema 中大部分的類型都是普通對象類型,但是一個 schema 內有兩個特殊類型:

schema {
  query: Query
  mutation: Mutation
}

每一個 GraphQL 服務都有一個 query 類型,可能有一個 mutation 類型。這兩個類型和常規對象類型無差,但是它們之所以特殊,是因爲它們定義了每一個 GraphQL 查詢的入口。因此如果你看到一個像這樣的查詢:

query {
  hero {
    name
  }
  droid(id: "2000") {
    name
  }
}

// Result
{
  "data": {
    "hero": {
      "name": "R2-D2"
    },
    "droid": {
      "name": "C-3PO"
    }
  }
}

那表示這個 GraphQL 服務需要一個 Query 類型,且其上有 hero 和 droid 字段:

type Query {
  hero(episode: Episode): Character
  droid(id: ID!): Droid
}

變更也是類似的工作方式 —— 你在 Mutation 類型上定義一些字段,然後這些字段將作爲 mutation 根字段使用,接着你就能在你的查詢中調用。

有必要記住的是,除了作爲 schema 的入口,Query 和 Mutation 類型與其它 GraphQL 對象類型別無二致,它們的字段也是一樣的工作方式。

標量類型

一個對象類型有自己的名字和字段,而某些時候,這些字段必然會解析到具體數據。這就是標量類型的來源:它們表示對應 GraphQL 查詢的葉子節點。下列查詢中,name 和 appearsIn 字段將解析到標量類型:

{
  hero {
    name
    appearsIn
  }
}
// Result
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}

我們知道這些字段沒有任何次級字段 —— 因爲讓它們是查詢的葉子節點。
GraphQL 自帶一組默認標量類型:

  • Int:有符號 32 位整數。
  • Float:有符號雙精度浮點值。
  • String:UTF‐8 字符序列。
  • Boolean:true 或者 false。
  • ID:ID 標量類型表示一個唯一標識符,通常用以重新獲取對象或者作爲緩存中的鍵。ID 類型使用和 String 一樣的方式序列化;然而將其定義爲 ID 意味着並不需要人類可讀型。

大部分的 GraphQL 服務實現中,都有自定義標量類型的方式。例如,我們可以定義一個 Date 類型:

scalar Date

然後就取決於我們的實現中如何定義將其序列化、反序列化和驗證。例如,你可以指定 Date 類型應該總是被序列化成整型時間戳,而客戶端應該知道去要求任何 date 字段都是這個格式。

枚舉類型

枚舉類型是一種特殊的標量,它限制在一個特殊的可選值集合內。這讓你能夠:

  1. 驗證這個類型的任何參數是可選值的的某一個
  2. 與類型系統溝通,一個字段總是一個有限值集合的其中一個值。
    下面是一個用 GraphQL schema 語言表示的 enum 定義:
enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

這表示無論我們在 schema 的哪處使用了 Episode,都可以肯定它返回的是 NEWHOPE、EMPIRE 和 JEDI 之一。

注意,各種語言實現的 GraphQL 服務會有其獨特的枚舉處理方式。對於將枚舉作爲一等公民的語言,它的實現就可以利用這個特性;而對於像 JavaScript 這樣沒有枚舉支持的語言,這些枚舉值可能就被內部映射成整數值。當然,這些細節都不會泄漏到客戶端,客戶端會根據字符串名稱來操作枚舉值。

列表和非空

對象類型、標量以及枚舉是 GraphQL 中你唯一可以定義的類型種類。但是當你在 schema 的其他部分使用這些類型時,或者在你的查詢變量聲明處使用時,你可以給它們應用額外的類型修飾符來影響這些值的驗證。我們先來看一個例子:

type Character {
  name: String!
  appearsIn: [Episode]!
}

此處我們使用了一個 String 類型,並通過在類型名後面添加一個感嘆號!將其標註爲非空。這表示我們的服務器對於這個字段,總是會返回一個非空值,如果它結果得到了一個空值,那麼事實上將會觸發一個 GraphQL 執行錯誤,以讓客戶端知道發生了錯誤。

非空類型修飾符也可以用於定義字段上的參數,如果這個參數上傳遞了一個空值(不管通過 GraphQL 字符串還是變量),那麼會導致服務器返回一個驗證錯誤。

query DroidById($id: ID!) {
  droid(id: $id) {
    name
  }
}
{
  "id": null
}

// Result
{
  "errors": [
    {
      "message": "Variable \"$id\" of required type \"ID!\" was not provided.",
      "locations": [
        {
          "line": 1,
          "column": 17
        }
      ]
    }
  ]
}

列表的運作方式也類似:我們也可以使用一個類型修飾符來標記一個類型爲 List,表示這個字段會返回這個類型的數組。在 GraphQL schema 語言中,我們通過將類型包在方括號([ 和 ])中的方式來標記列表。列表對於參數也是一樣的運作方式,驗證的步驟會要求對應值爲數組。

非空和列表修飾符可以組合使用。例如你可以要求一個非空字符串的數組:

myField: [String!]

這表示數組本身可以爲空,但是其不能有任何空值成員。用 JSON 舉例如下:

myField: null // 有效
myField: [] // 有效
myField: ['a', 'b'] // 有效
myField: ['a', null, 'b'] // 錯誤

然後,我們來定義一個不可爲空的字符串數組:

myField: [String]!

這表示數組本身不能爲空,但是其可以包含空值成員:

myField: null // 錯誤
myField: [] // 有效
myField: ['a', 'b'] // 有效
myField: ['a', null, 'b'] // 有效

你可以根據需求嵌套任意層非空和列表修飾符。

接口

跟許多類型系統一樣,GraphQL 支持接口。一個接口是一個抽象類型,它包含某些字段,而對象類型必須包含這些字段,才能算實現了這個接口。

例如:

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

這意味着任何實現 Character 的類型都要具有這些字段,並有對應參數和返回類型

例如,這裏有一些可能實現了 Character 的類型:

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
  }

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
  }

聯合類型

聯合類型和接口十分相似,但是它並不指定類型之間的任何共同字段。

union SearchResult = Human | Droid | Starship

在我們的schema中,任何返回一個 SearchResult 類型的地方,都可能得到一個 Human、Droid 或者 Starship。注意,聯合類型的成員需要是具體對象類型;你不能使用接口或者其他聯合類型來創造一個聯合類型。

這時候,如果你需要查詢一個返回 SearchResult 聯合類型的字段,那麼你得使用條件片段才能查詢任意字段。

{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

//Result
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo",
        "height": 1.8
      },
      {
        "__typename": "Human",
        "name": "Leia Organa",
        "height": 1.5
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1",
        "length": 9.2
      }
    ]
  }
}

此外,在這種情況下,由於 Human 和 Droid 共享一個公共接口(Character),你可以在一個地方查詢它們的公共字段,而不必在多個類型中重複相同的字段:

{
  search(text: "an") {
    __typename
    ... on Character {
      name // 需要查詢的公共字段
    }
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }}

注意 name 仍然需要指定在 Starship 上,否則它不會出現在結果中,因爲 Starship 並不是一個 Character!

輸入類型(Input Types)

目前爲止,我們只討論過將例如枚舉和字符串等標量值作爲參數傳遞給字段,但是你也能很容易地傳遞複雜對象。這在變更(mutation)中特別有用,因爲有時候你需要傳遞一整個對象作爲新建對象。在 GraphQL schema language 中,輸入對象看上去和常規對象一模一樣,除了關鍵字是 input 而不是 type:

input ReviewInput {
  stars: Int!
  commentary: String
}

你可以像這樣在變更(mutation)中使用輸入對象類型:

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}
// Variables
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}
// Result
{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}

輸入對象類型上的字段本身也可以指代輸入對象類型,但是你不能在你的 schema 混淆輸入和輸出類型。輸入對象類型的字段當然也不能擁有參數。

執行

GraphQL 不能脫離類型系統處理查詢,讓我們用一個類型系統的例子來說明一個查詢的執行過程:

type Query {
  human(id: ID!): Human
}

type Human {
  name: String
  appearsIn: [Episode]
  starships: [Starship]
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Starship {
  name: String
}

現在讓我們用一個例子來描述當一個查詢請求被執行的全過程。

{
  human(id: 1002) {
    name
    appearsIn
    starships {
      name
    }
  }
}

// Result
{
  "data": {
    "human": {
      "name": "Han Solo",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ],
      "starships": [
        {
          "name": "Millenium Falcon"
        },
        {
          "name": "Imperial shuttle"
        }
      ]
    }
  }
}

您可以將 GraphQL 查詢中的每個字段視爲返回子類型的父類型函數或方法。事實上,這正是 GraphQL 的工作原理。每個類型的每個字段都由一個 resolver 函數支持,該函數由 GraphQL 服務器開發人員提供。當一個字段被執行時,相應的 resolver 被調用以產生下一個值。

如果字段產生標量值,例如字符串或數字,則執行完成。如果一個字段產生一個對象,則該查詢將繼續執行該對象對應字段的解析器,直到生成標量值。GraphQL 查詢始終以標量值結束。

根字段 & 解析器

每一個 GraphQL 服務端應用的頂層,必有一個類型代表着所有進入 GraphQL API 可能的入口點,我們將它稱之爲 Root 類型或 Query 類型。

在這個例子中查詢類型提供了一個字段 human,並且接受一個參數 id。這個字段的解析器可能請求了數據庫之後通過構造函數返回一個 Human 對象。

Query: {
  human(obj, args, context, info) {
    return context.db.loadHumanByID(args.id).then(
      userData => new Human(userData)
    )
  }
 }

這個例子使用了 JavaScript 語言,但 GraphQL 服務端應用可以被 多種語言實現。
解析器函數接收 4 個參數:

  • obj 上一級對象,如果字段屬於根節點查詢類型通常不會被使用。
  • args 可以提供在 GraphQL 查詢中傳入的參數。
  • context 會被提供給所有解析器,並且持有重要的上下文信息比如當前登入的用戶或者數據庫訪問對象。
  • info 一個保存與當前查詢相關的字段特定信息以及 schema 詳細信息的值。

異步解析器

讓我們來分析一下在這個解析器函數中發生了什麼。

human(obj, args, context, info) {
  return context.db.loadHumanByID(args.id).then(
    userData => new Human(userData)
  )
}

context 提供了一個數據庫訪問對象,用來通過查詢中傳遞的參數 id 來查詢數據,因爲從數據庫拉取數據的過程是一個異步操作,該方法返回了一個 Promise 對象,在 JavaScript 語言中 Promise 對象用來處理異步操作,但在許多語言中存在相同的概念,通常稱作 Futures、Tasks 或者 Defferred。當數據庫返回查詢結果,我們就能構造並返回一個新的 Human 對象。

這裏要注意的是,只有解析器能感知到 Promise 的進度,GraphQL 查詢只關注一個包含着 name 屬性的 human 字段是否返回,在執行期間如果異步操作沒有完成,則 GraphQL 會一直等待下去,因此在這個環節需要關注異步處理上的優化。

不重要的解析器

現在 Human 對象已經生成了,但 GraphQL 還是會繼續遞歸執行下去。

Human: {
  name(obj, args, context, info) {
    return obj.name
  }
}

GraphQL 服務端應用的業務取決於類型系統的結構。在 human 對象返回值之前,由於類型系統確定了 human 字段將返回一個 Human 對象,GraphQL 會根據類型系統預設好的 Human類型決定如何解析字段。

在這個例子中,對 name 字段的處理非常的清晰,name 字段對應的解析器被調用的時候,解析器回調函數的 obj 參數是由上層回調函數生成的 new Human 對象。在這個案例中,我們希望 Human 對象會擁有一個 name 屬性可以讓我們直接讀取。

事實上,許多 GraphQL 庫可以讓你省略這些簡單的解析器,假定一個字段沒有提供解析器時,那麼應該從上層返回對象中讀取和返回和這個字段同名的屬性。

列表解析器

我們已經看到一個字段返回上面的 appearsIn 字段的事物列表時會發生什麼。它返回了枚舉值的列表,因爲這是系統期望的類型,列表中的每個項目被強制爲適當的枚舉值。讓我們看下 startships 被解析的時候會發生什麼?

Human: {
  starships(obj, args, context, info) {
    return obj.starshipIDs.map(
      id => context.db.loadStarshipByID(id).then(
        shipData => new Starship(shipData)
      )
    )
}
}

解析器在這個字段中不僅僅是返回了一個 Promise 對象,它返回一個 Promises 列表。

Human 對象具有他們正在駕駛的 Starships 的 ids 列表,但是我們需要通過這些 id 來獲得真正的 Starship 對象。

GraphQL 將併發執行這些 Promise,當執行結束返回一個對象列表後,它將繼續併發加載列表中每個對象的 name 字段。

產生結果

當每個字段被解析時,結果被放置到鍵值映射中,字段名稱(或別名)作爲鍵值映射的鍵,解析器的值作爲鍵值映射的值,這個過程從查詢字段的底部葉子節點開始返回,直到根 Query 類型的起始節點。最後合併成爲能夠鏡像到原始查詢結構的結果,然後可以將其發送(通常爲 JSON 格式)到請求的客戶端。

內省

我們有時候會需要去問 GraphQL Schema 它支持哪些查詢。GraphQL 通過內省系統讓我們可以做到這點!

如果是我們親自設計了類型,那我們自然知道哪些類型是可用的。但如果類型不是我們設計的,我們也可以通過查詢 __schema 字段來向 GraphQL 詢問哪些類型是可用的。一個查詢的根類型總是有 __schema 這個字段。現在來試試,查詢一下有哪些可用的類型。

{
  __schema {
    types {
      name
    }
  }
}

// Result
{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "Episode"
        },
        {
          "name": "Character"
        },
        {
          "name": "ID"
        },
        {
          "name": "String"
        },
        {
          "name": "Int"
        },
        {
          "name": "FriendsConnection"
        },
        {
          "name": "FriendsEdge"
        },
        {
          "name": "PageInfo"
        },
        {
          "name": "Boolean"
        },
        {
          "name": "Review"
        },
        {
          "name": "SearchResult"
        },
        {
          "name": "Human"
        },
        {
          "name": "LengthUnit"
        },
        {
          "name": "Float"
        },
        {
          "name": "Starship"
        },
        {
          "name": "Droid"
        },
        {
          "name": "Mutation"
        },
        {
          "name": "ReviewInput"
        },
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__EnumValue"
        },
        {
          "name": "__Directive"
        },
        {
          "name": "__DirectiveLocation"
        }
      ]
    }
  }
}
  • Query, Character, Human, Episode, Droid - 這些是我們在類型系統中定義的類型。
  • String, Boolean - 這些是內建的標量,由類型系統提供。
  • __Schema,__Type,__TypeKind,__Field,__InputValue, __EnumValue,__Directive - 這些有着兩個下劃線的類型是內省系統的一部分。

現在,來試試找到一個可以探索出有哪些可用查詢的地方。當我們設計類型系統的時候,我們確定了一個所有查詢開始的地方,來問問內省系統它是什麼!

{
  __schema {
    queryType {
      name
    }
  }
}
// Result
{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query"
      }
    }
  }
}

這和我們在類型系統那章裏說的一樣,Query 類型是我們開始的地方!注意這裏的命名只是一個慣例,我們也可以把 Query 取成別的名字,只要我們把它定義爲所有查詢出發的地方,它也依然會在這裏被返回。儘管如此,還是把它命名爲 Query 吧,這是一個有用的慣例。

有時候也需要檢驗一個特定的類型。來看看 Droid 類型:

{
  __type(name: "Droid") {
    name
    kind
  }
}

//Result
{
  "data": {
    "__type": {
      "name": "Droid",
      "kind": "OBJECT"
    }
  }
}

kind 返回一個枚舉類型 __TypeKind,其中一個值是 OBJECT。

對於一個對象來說,知道它有哪些字段是很有用的,所以來問問內省系統 Droid 有哪些字段:

{
  __type(name: "Droid") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

// Result
{
  "data": {
    "__type": {
      "name": "Droid",
      "fields": [
        {
          "name": "id",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        },
        {
          "name": "name",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        },
        {
          "name": "friends",
          "type": {
            "name": null,
            "kind": "LIST"
          }
        },
        {
          "name": "friendsConnection",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        },
        {
          "name": "appearsIn",
          "type": {
            "name": null,
            "kind": "NON_NULL"
          }
        },
        {
          "name": "primaryFunction",
          "type": {
            "name": "String",
            "kind": "SCALAR"
          }
        }
      ]
    }
  }
}

id 看起來有點兒奇怪,這個類型沒有名字。這是因爲它是一個 NON_NULL 類型的“包裝” 。如果我們請求它的 ofType 字段,我們會發現它是 ID ,告訴我們這是一個非空的 ID。相似地,friends 和 appearsIn 都沒有名字,因爲它們都是 LIST 包裝類型。我們可以看看它們的 ofType,就能知道它們是裝什麼東西的列表。

參考資料:

  1. GraphQL官網入門
  2. How to GraphQL 全棧教程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章