GraphQL指南

GraphQL

GraphQL 是一種新的 API 標(biāo)準(zhǔn),它提供了一種更高效、強(qiáng)大和靈活的數(shù)據(jù)提供方式,由 Facebook 發(fā)起并開源,目前由來(lái)自世界各地的大公司和個(gè)人共同維護(hù)。

GraphQL 本質(zhì)上是一種基于 API 的查詢語(yǔ)言,現(xiàn)在大多數(shù)應(yīng)用程序都需要從服務(wù)器中獲取數(shù)據(jù),這些數(shù)據(jù)存儲(chǔ)可能存儲(chǔ)在數(shù)據(jù)庫(kù)中,API 的職責(zé)是提供與應(yīng)用程序需求相匹配的存儲(chǔ)數(shù)據(jù)的接口。

我們經(jīng)常把 GraphQL 和數(shù)據(jù)庫(kù)技術(shù)相混淆,這是一個(gè)誤解,GraphQL 是 API 的查詢語(yǔ)言,而不是數(shù)據(jù)庫(kù),它是數(shù)據(jù)庫(kù)無(wú)關(guān)的,而且可以在使用 API 的任何環(huán)境中有效使用。

我們經(jīng)常把 GraphQL 和數(shù)據(jù)庫(kù)技術(shù)相混淆,這是一個(gè)誤解,GraphQL 是 API 的查詢語(yǔ)言,而不是數(shù)據(jù)庫(kù),它是數(shù)據(jù)庫(kù)無(wú)關(guān)的,而且可以在使用 API 的任何環(huán)境中有效使用。

GraphQL 是基于 API 之上的一層封裝,目的是為了更好、更靈活的適用于業(yè)務(wù)的需求變化。

GraphQL 產(chǎn)生的歷史背景

提起 API 設(shè)計(jì)的時(shí)候,大家通常會(huì)想到 SOAP、RESTful 等設(shè)計(jì)方式,從 2000 年 RESTful 的理論被 Roy Fielding (HTTP 規(guī)范的主要編寫者之一)在其博士論文中提出的時(shí)候,在業(yè)界引起了很大反響,因?yàn)檫@種設(shè)計(jì)理念更易于用戶的使用,所以便很快的被大家所接受。

RESTful 是目前(2019 年)從服務(wù)器公開 API 的最為流行的方式(沒有之一),當(dāng) RESTful 的概念被提及出來(lái)時(shí),客戶端應(yīng)用程序?qū)?shù)據(jù)的需求相對(duì)簡(jiǎn)單,而開發(fā)的速度并沒有達(dá)到今天的水平。

RESTful 對(duì)于許多簡(jiǎn)單的應(yīng)用程序來(lái)說(shuō)是適合的,然而當(dāng)業(yè)務(wù)越來(lái)越復(fù)雜、客戶對(duì)系統(tǒng)的擴(kuò)展性有更高要求時(shí),API 會(huì)發(fā)生巨大的變化,由此帶來(lái)了 API 設(shè)計(jì)的巨大挑戰(zhàn)。

Google 趨勢(shì)

更高效的數(shù)據(jù)加載方式

Facebook 發(fā)起 GraphQL 的最初原因是移動(dòng)端用戶的爆發(fā)式增長(zhǎng)需要更高效的數(shù)據(jù)加載方式。

GraphQL 最小化了需要網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量,從而極大地改善了在這些條件下運(yùn)行的應(yīng)用程序。

適應(yīng)不同的前端框架和平臺(tái)

各種不同的前端框架和平臺(tái),運(yùn)行客戶端應(yīng)用程序的不同環(huán)境,使得我們?cè)跇?gòu)建和維護(hù)一個(gè)符合所有需求的 API 變得困難,但使用 GraphQL 的每個(gè)客戶機(jī)都可以精確地訪問它需要的數(shù)據(jù)。

適應(yīng)與不同的客戶端

加快產(chǎn)品開發(fā)速度

持續(xù)部署已經(jīng)成為許多公司的標(biāo)準(zhǔn),快速的迭代和頻繁的產(chǎn)品更新是必不可少的。

對(duì)于 RESTful API,服務(wù)器公開數(shù)據(jù)的方式常常需要修改,以滿足客戶端的特定需求和設(shè)計(jì)更改,這阻礙了快速開發(fā)實(shí)踐和產(chǎn)品迭代。

在不同前端框架、不同平臺(tái)下想要加快產(chǎn)品快速開發(fā)變的越來(lái)越難。

什么是 GraphQL

GraphQL 是一套 API 語(yǔ)言規(guī)范,F(xiàn)acebook 于 2012 年應(yīng)用于移動(dòng)應(yīng)用,并于 2015 年開源。

GraphQL 處于 API 層,主要目的是前后端數(shù)據(jù)通訊,代替了傳統(tǒng)的 RESTful。

GraphQL 既是一種用于 API 的查詢語(yǔ)言也是一個(gè)滿足你數(shù)據(jù)查詢的運(yùn)行時(shí)。

GraphQL 對(duì) API 中的數(shù)據(jù)提供了一套易于理解的完整描述,使客戶端能夠準(zhǔn)確地獲得它需要的、沒有冗余的數(shù)據(jù),也使 API 更容易地隨著時(shí)間推移而演進(jìn),還能用于構(gòu)建強(qiáng)大的開發(fā)者工具,GraphQL 可以一次請(qǐng)求獲取你所需的所有數(shù)據(jù)。

GraphQL 是處于 API 層的靈活的、開放的一套規(guī)范,將 GraphQL 置于現(xiàn)有后端上,可以比以往更快捷的構(gòu)建產(chǎn)品。

GraphQL 與 RESTful 簡(jiǎn)單比較

GraphQL 可以理解為基于 RESTful 的一種封裝,目的在于構(gòu)建使客戶端更加易用的服務(wù),可以說(shuō) GraphQL 是更好的 RESTful 設(shè)計(jì),號(hào)稱 RESTful 2.0。

GraphQL 與 RESTful

在過(guò)去的十多年中,RESTful 已經(jīng)成為設(shè)計(jì) WEB API 的實(shí)際標(biāo)準(zhǔn)。它提供了一些很棒的想法,比如無(wú)狀態(tài)服務(wù)器和結(jié)構(gòu)化的資源訪問。然而 RESTful API 表現(xiàn)得過(guò)于僵化,無(wú)法跟上客戶端需求的快速變化。

GraphQL 的開發(fā)是為了提高更多的靈活性和效率,它解決了與 RESTful API 交互時(shí)開發(fā)人員所經(jīng)歷的許多缺點(diǎn)和低效之處。

為了說(shuō)明獲取數(shù)據(jù)時(shí) RESTful 和 GraphQL 之間的主要區(qū)別,讓我們考慮一個(gè)簡(jiǎn)單的示例場(chǎng)景:在 blog 應(yīng)用程序中,應(yīng)用程序需要顯示特定用戶的文章的標(biāo)題。同一屏幕還顯示該用戶最后 3 個(gè)關(guān)注者的名稱。REST 和 GraphQL 如何解決這種情況?

RESTful 訪問數(shù)據(jù)

使用 RESTful API 來(lái)現(xiàn)實(shí)時(shí),我們通??梢酝ㄟ^(guò)訪問多次請(qǐng)求來(lái)收集數(shù)據(jù)。

我們可以通過(guò)下面的三步來(lái)實(shí)現(xiàn):

  1. 通過(guò)/user/<id>獲取初始用戶數(shù)據(jù)
  2. 通過(guò)/user/<id>/posts 返回用戶的所有帖子
  3. 請(qǐng)求/user/<id>/followers返回每個(gè)用戶的關(guān)注者列表

如下圖

RESTful數(shù)據(jù)訪問示例

在 GraphQL 的世界里我們不用多取數(shù)據(jù),也不用擔(dān)心數(shù)據(jù)取少了,我們只需要按需獲取數(shù)據(jù)即可。

RESTful 最常見的問題之一是 API 的返回?cái)?shù)據(jù)過(guò)多或者過(guò)少,這是因?yàn)榭蛻舳双@取數(shù)據(jù)的唯一方法是通過(guò)訪問返回固定數(shù)據(jù)結(jié)構(gòu)的 endpoint,這就會(huì)導(dǎo)致我們?cè)O(shè)計(jì) API 非常困難,因?yàn)樗纫軌驗(yàn)榭蛻籼峁┚_的數(shù)據(jù)需求,又需要滿足不同調(diào)用者的需求,這本身就是相互矛盾的。GraphQL 的發(fā)明者 Lee Byron 提出了一個(gè)很重要的概念: “用圖形來(lái)思考,而不是 endpoint”。

GraphQL 優(yōu)勢(shì)

強(qiáng)類型 Schema

對(duì)大多數(shù) API 而言,最大的問題在于缺少?gòu)?qiáng)類型約束。常見場(chǎng)景為,后端 API 更新了,但文檔沒跟上,你沒法知道新的 API 是干什么的,怎么用,這應(yīng)該是前后端掐架的原因之一。

GraphQL Schema 是每個(gè) GraphQL API 的基礎(chǔ),它清晰的定義了每個(gè) API 支持的操作,包括輸入的參數(shù)和返回的內(nèi)容。

GraphQL Schema 是一份契約,用于指明 API 的功能。

GraphQL Schema 是強(qiáng)類型的,可使用 SDL(GraphQL Schema Definition Language)來(lái)定義。相對(duì)而言,強(qiáng)類型系統(tǒng)使開發(fā)人員自行車換摩托。比如,可以使用構(gòu)建工具驗(yàn)證 API 請(qǐng)求,編譯時(shí)檢查 API 調(diào)用可能發(fā)生的錯(cuò)誤,甚至可以在代碼編輯器中自動(dòng)完成 API 操作。

Schema 帶來(lái)的另一個(gè)好處是,不用再去編寫 API 文檔,因?yàn)楦鶕?jù) Schema 自動(dòng)生成了,這改變了 API 開發(fā)的玩法。

按需獲取

我們經(jīng)常談 GraphQL 的主要優(yōu)點(diǎn)——前端可以從 API 獲取想要的數(shù)據(jù),不必依賴服務(wù)端 RESTful API 返回的固定數(shù)據(jù)結(jié)構(gòu)。

如此,解決了傳統(tǒng) REST API 的兩個(gè)典型問題:過(guò)度獲取(Over Fetching) 和 不足獲取(Under Fetching)。

使用 GraphQL,客戶端自己決定返回的數(shù)據(jù)及結(jié)構(gòu)。

過(guò)度獲取

意味著前端得到了實(shí)際不需要的數(shù)據(jù),這可能會(huì)造成性能和帶寬浪費(fèi)。

通常情況下我們?cè)谡{(diào)用一個(gè)通用 API 接口時(shí),客戶端獲取的信息比應(yīng)用程序中實(shí)際需要的要多。例如 UI 需要顯示一個(gè)用戶列表,而實(shí)際上只需要使用他們的名字。在 RESTful API 中通常會(huì)調(diào)用 /users這個(gè) endpoint,并接收一個(gè)帶有用戶數(shù)據(jù)的 JSON 數(shù)組。但是這個(gè)響應(yīng)可能包含更多關(guān)于返回的用戶的信息,例如他們的生日或地址,而這些信息對(duì)客戶來(lái)說(shuō)是無(wú)用的,因?yàn)樗恍枰@示用戶的名字。

不足獲取

不足獲取與過(guò)度獲取想反,API 返回中缺少足夠的數(shù)據(jù),這意味著前端需要請(qǐng)求額外的 API 得到需要的數(shù)據(jù)。

最壞的場(chǎng)景下,會(huì)導(dǎo)致臭名昭著的 N+1 請(qǐng)求問題:獲取數(shù)據(jù)列表,而沒有 API 能夠滿足列表字段要求,不得不對(duì)每行數(shù)據(jù)發(fā)起一次請(qǐng)求,以獲取所需額外數(shù)據(jù)。

假設(shè)我們?cè)趽v鼓一個(gè)博客應(yīng)用,需要顯示 user 列表,除user本身信息外,還要顯示每個(gè) user 最近一篇文章的title。然而,調(diào)用/users/僅得到用戶信息集合,然后不得不對(duì)每個(gè)user調(diào)用/users/<id>/articles獲取其最新文章title。

當(dāng)然你可以再寫一個(gè) API 來(lái)滿足特殊場(chǎng)景,如/users/lastarticles/來(lái)滿足上面的需求,但需要編寫后端相關(guān)代碼,調(diào)試和部署,加班....有這時(shí)間何不去陪家人、孩子。

GraphQL 支持快速產(chǎn)品開發(fā)

GraphQL 使前端開發(fā)人員輕松愉悅,感謝 GraphQL 的前端庫(kù)(ApolloRelayUrql),前端可以用如緩存、實(shí)時(shí)更新 UI 等功能。

前端開發(fā)人員生產(chǎn)力提高,產(chǎn)品開發(fā)速度加快,無(wú)論 UI 如何變化后端服務(wù)不用變。

構(gòu)建 GraphQL API 的過(guò)程大多圍繞 GraphQL Scheme。由此,經(jīng)常聽到 Schema-Driven Development(SDD),這只是指一個(gè)過(guò)程,結(jié)構(gòu)在 Schema 中定義,在 Resolver(解析器)中實(shí)現(xiàn)。

有了像GraphQL Faker這樣的工具,前端開發(fā)可以在 Schema 定義后開始。GraphQL Faker 模擬整個(gè) GraphQL API(依賴于定義的 schema),因此前后端可以獨(dú)立工作。

Schema 與 java 中的 interface 有點(diǎn)類似,java 開發(fā)中,大家可以先定義一個(gè)接口(interface),然后基于接口各自干各自的事情,互不干擾。

組合 GraphQL API

Schema 拼接(stitching)是 GraphQL 中的新概念之一,簡(jiǎn)而言之,可以組合和連接多個(gè) GraphQL API 合并為一個(gè)。與 React 或 Vue 中的組件概念類似,一個(gè) GraphQL API 可以由多個(gè) GraphQL API 組合構(gòu)成。

這對(duì)前端開發(fā)非常有用,不然,需要與多個(gè) GraphQL endpoint 通信(通常在微服務(wù)架構(gòu)或與第三方 API 集成時(shí))。由于 Schema 拼接,前端只需要處理單個(gè) API endpoint,而協(xié)調(diào)與各種服務(wù)通信的復(fù)雜性從前端隱藏。

豐富的開源生態(tài)和牛逼閃閃的社區(qū)

自 Facebook 正式發(fā)布 GraphQL 以來(lái),僅三年多時(shí)間,整個(gè) GraphQL 生態(tài)系統(tǒng)的成熟程度難以置信。

剛發(fā)布時(shí),開發(fā)人員使用 GraphQL 的唯一工具是graphql-js參考實(shí)現(xiàn)——一個(gè) Express.js 中間件和 GraphQL client Relay。

現(xiàn)在,GraphQL 規(guī)范的參考實(shí)現(xiàn)幾乎覆蓋了大部分編程語(yǔ)言,并有大量的 GraphQL 客戶端。此外許多工具提供了無(wú)縫的工作流程,并在構(gòu)建 GraphQL API 時(shí)提供爽爽的開發(fā)體驗(yàn),如:Primsa、GraphQL Faker、GraphQL Playground、graphql-config 等。

核心概念

SDL(Schema Definition Language)

GraphQL 有一套用于定義 API schema 的類型系統(tǒng). 編寫 schema 的語(yǔ)法我們稱其為 schema 定義語(yǔ)言(Schema Definition Language ,簡(jiǎn)稱 SDL)。

以下代碼是我們使用 SDL 定義名為Person的簡(jiǎn)單類型的示例:

type Person {
  name: String!
  age: Int!
}

Person 類型有兩個(gè)字段,它們分別叫做nameage,分別是StringInt類型,類型后面的 ! 該表示該字段是必需的。

也可以描述類型之間的關(guān)系,以博客應(yīng)用為例,一個(gè)人(Person)可能與一篇文章(Post)相關(guān)聯(lián):

type Post {
  title: String!
  author: Person!
}

相應(yīng)的,關(guān)系的另一端需要放在人員(Person)類型上:

type Person {
  name: String!
  age: Int!
  posts: [Post!]!
}

請(qǐng)注意,我們剛剛創(chuàng)建了 PersonPost 之間的一對(duì)多關(guān)系,因?yàn)?Person 上的 posts 字段實(shí)際上是一個(gè)帖子(Post)數(shù)組。

使用查詢(Query)獲取數(shù)據(jù)

在使用 REST API 時(shí),從特定的端點(diǎn)( endpoint )加載數(shù)據(jù)。每個(gè)端點(diǎn)都有明確定義的返回信息結(jié)構(gòu)。 這意味著客戶端的數(shù)據(jù)要求在其連接的 URL 中有效編碼。

GraphQL 采用的方法完全不同。 GraphQL API 通常只暴露單個(gè)端點(diǎn),而不是具有多個(gè)返回固定數(shù)據(jù)結(jié)構(gòu)的端點(diǎn)。 這是有效的,因?yàn)榉祷氐臄?shù)據(jù)結(jié)構(gòu)不固定。 相反,它是完全靈活的,讓客戶決定實(shí)際需要什么數(shù)據(jù)。這意味著客戶端需要向服務(wù)器發(fā)送更多信息以表達(dá)其數(shù)據(jù)需求 - 此信息稱為查詢。

基本查詢

我們看一下客戶端可以發(fā)送到服務(wù)器的示例查詢:

{
  allPersons {
    name
  }
}

此查詢中的allPersons字段稱為查詢的根字段。 根字段后面的所有內(nèi)容稱為查詢的有效內(nèi)容。 此查詢的有效內(nèi)容中指定的唯一字段是name

此查詢將返回當(dāng)前存儲(chǔ)在數(shù)據(jù)庫(kù)中的所有人員的列表。 這是一個(gè)示例響應(yīng):

{
  "data": {
    "allPersons": [
      {
        "name": "Johnny"
      },
      {
        "name": "Sarah"
      },
      {
        "name": "Alice"
      }
    ]
  }
}

請(qǐng)注意,每個(gè)人在響應(yīng)中只有name,但服務(wù)器不會(huì)返回age。 這正是因?yàn)?code>name是查詢中指定的唯一字段。

如果客戶端還需要人員的age,則只需稍微調(diào)整查詢并在查詢的有效負(fù)載中包含新字段:

{
  allPersons {
    name
    age
  }
}

添加了age字段的查詢將返回

{
  "data": {
    "allPersons": [
      {
        "name": "Johnny",
        "age": 23
      },
      {
        "name": "Sarah",
        "age": 20
      },
      {
        "name": "Alice",
        "age": 20
      }
    ]
  }
}

嵌套查詢

GraphQL 的一個(gè)主要優(yōu)點(diǎn)是它允許查詢嵌套。 例如,如果您想加載Person編寫的所有posts,您只需按照類型的結(jié)構(gòu)來(lái)請(qǐng)求此信息:

{
  allPersons {
    name
    age
    posts {
      title
    }
  }
}

嵌套查詢的返回

{
  "data": {
    "allPersons": [
      {
        "name": "Johnny",
        "age": 23,
        "posts": [
          {
            "title": "GraphQL is awesome"
          },
          {
            "title": "Relay is a powerful GraphQL Client"
          }
        ]
      },
      {
        "name": "Sarah",
        "age": 20,
        "posts": [
          {
            "title": "How to get started with React & GraphQL"
          }
        ]
      },
      {
        "name": "Alice",
        "age": 20,
        "posts": []
      }
    ]
  }
}

帶參數(shù)查詢

在 GraphQL 中,如果在schema中指定了參數(shù),則每個(gè)字段可以包含零個(gè)或多個(gè)參數(shù)。 例如,allPersons字段可以具有last參數(shù),以僅返回特定數(shù)量的人。 這是相應(yīng)的查詢的樣子:

{
  allPersons(last: 2) {
    name
  }
}

返回結(jié)果

{
  "data": {
    "allPersons": [
      {
        "name": "Sarah"
      },
      {
        "name": "Alice"
      }
    ]
  }
}

使用變更(Mutation)寫數(shù)據(jù)

在從服務(wù)器請(qǐng)求信息之后,大多數(shù)應(yīng)用程序還需要某種方式來(lái)更改當(dāng)前存儲(chǔ)在后端中的數(shù)據(jù)。 使用 GraphQL,這些更改是使用所謂的變更(Mutations,有時(shí)也叫突變)進(jìn)行的。 通常有三種變更:

  • 創(chuàng)建新數(shù)據(jù)
  • 修改現(xiàn)有數(shù)據(jù)
  • 刪除現(xiàn)有數(shù)據(jù)

變更遵循與查詢相同的語(yǔ)法結(jié)構(gòu),但它們始終需要以mutation關(guān)鍵字開頭。以下是我們?nèi)绾蝿?chuàng)建新Person的示例:

mutation {
  createPerson(name: "Bob", age: 36) {
    name
    age
  }
}

以上代碼創(chuàng)建一個(gè)nameBob,age36Person對(duì)象,并返回執(zhí)行結(jié)果中的nameage字段,執(zhí)行結(jié)果如下:

{
  "data": {
    "createPerson": {
      "name": "Bob",
      "age": 36
    }
  }
}

注意,與我們之前編寫的查詢類似,變異也有一個(gè)根字段 - 在這里為createPerson。 我們還已經(jīng)了解了字段參數(shù)的概念。 在這里,createPerson字段采用兩個(gè)參數(shù)來(lái)指定新人的nameage

與查詢一樣,我們也可以為變更(mutation)指定有效負(fù)載(playload),我們可以在其中請(qǐng)求新Person對(duì)象的不同屬性。 例子中,我們?cè)儐柕氖?code>name和age - 盡管在我們的例子中這并不是非常有用,因?yàn)槲覀冿@然已經(jīng)知道了它們,因?yàn)槲覀儗⑺鼈儌鬟f給了變更。 但是,能夠在發(fā)送變更時(shí)查詢信息可以是一個(gè)非常強(qiáng)大的工具,允許您在單個(gè)往返中從服務(wù)器檢索新信息!

以后會(huì)發(fā)現(xiàn),GraphQL 類型中通常會(huì)由服務(wù)端創(chuàng)建新對(duì)象時(shí)生成唯一 ID,在之前的Person類型中添加id字段:

type Person {
  id: ID!
  name: String!
  age: Int!
}

現(xiàn)在,當(dāng)創(chuàng)建一個(gè)新Person時(shí),您可以直接在變更的有效負(fù)載中詢問id,因?yàn)檫@是事先在客戶端上無(wú)法獲得的信息:

mutation {
  createPerson(name: "Alice", age: 36) {
    id
    name
    age
  }
}
{
  "data": {
    "createPerson": {
      "id": "cjyz7ocx10mwp0171660kf1gm",
      "name": "Alice",
      "age": 36
    }
  }
}

訂閱(Subscription)的實(shí)時(shí)更新

當(dāng)今許多應(yīng)用程序的另一個(gè)重要要求是與服務(wù)器建立實(shí)時(shí)連接,以便立即了解重要事件。 對(duì)于此用例,GraphQL 提供了訂閱(subscriptions)的概念。

當(dāng)客戶端訂閱某個(gè)事件時(shí),它將啟動(dòng)并保持與服務(wù)器的穩(wěn)定連接。 每當(dāng)該特定事件實(shí)際發(fā)生時(shí),服務(wù)器就將相應(yīng)的數(shù)據(jù)推送到客戶端。 與典型的“request-response-cycle”之后的查詢和更改不同,訂閱表示發(fā)送到客戶端的數(shù)據(jù)流。

訂閱使用與查詢和突變相同的語(yǔ)法編寫。 這是我們訂閱Person類型上發(fā)生的事件的示例:

subscription {
  newPerson {
    name
    age
  }
}

客戶端將此訂閱(subscription)發(fā)送到服務(wù)器后,將在它們之間建立長(zhǎng)連接。 然后,每當(dāng)執(zhí)行創(chuàng)建新 Person 的變更時(shí),服務(wù)器都會(huì)將有關(guān)此人的信息發(fā)送給客戶端:

{
  "newPerson": {
    "name": "Jane",
    "age": 23
  }
}

定義 Schema

至此,您已經(jīng)基本了解了query,mutationsubscription,讓我們將它們結(jié)合起來(lái),并了解如何編寫一個(gè) schema,使您能夠執(zhí)行到目前為止看到的示例。

在使用 GraphQL API 時(shí),schema 是最重要的概念之一。

Schema 指定 API 的功能并定義客戶端如何請(qǐng)求數(shù)據(jù),其通常被視為服務(wù)端和客戶端之間的契約。

通常,schema 只是 GraphQL 類型的集合。 但是,在為 API 編寫 schema 時(shí),有一些特殊的根(root)類型:

type Query { ... }
type Mutation { ... }
type Subscription { ... }

Query,MutationSubscription類型是客戶端發(fā)送的請(qǐng)求的入口點(diǎn)。 要使我們之前看到的allPersons查詢可用,必須按如下方式編寫Query類型:

type Query {
  allPersons: [Person!]!
}

allPersons 被稱為 API 的根(root)字段。 再次考慮我們?cè)?allPersons 字段中添加最后一個(gè)參數(shù)的示例,我們必須按如下方式編寫 Query:

type Query {
  allPersons(last: Int): [Person!]!
}

同樣,對(duì)于createPerson變更,我們必須在Mutation類型中添加一個(gè)根字段:

type Mutation {
  createPerson(name: String!, age: Int!): Person!
}

請(qǐng)注意,此根字段也有兩個(gè)參數(shù),即新Personnameage。

最后,對(duì)于Subscription,我們必須添加newPerson根字段:

type Subscription {
  newPerson: Person!
}

總而言之,這是您在本章中看到的所有查詢和變更的完整 schema:

type Query {
  allPersons(last: Int): [Person!]!
}

type Mutation {
  createPerson(name: String!, age: Int!): Person!
}

type Subscription {
  newPerson: Person!
}

type Person {
  name: String!
  age: Int!
  posts: [Post!]!
}

type Post {
  title: String!
  author: Person!
}

GraphQL 服務(wù)端如何執(zhí)行查詢

GraphQL 通常被描述為以前端為中心的 API 技術(shù),因?yàn)樗箍蛻裟軌蛞员纫郧案玫姆绞将@取數(shù)據(jù)。 但是,API 本身當(dāng)然是在服務(wù)器端實(shí)現(xiàn)的。 在服務(wù)端提供 GraphQL 服務(wù)還有許多好處,因?yàn)?strong>GraphQL 使服務(wù)器開發(fā)人員能夠?qū)W⒂诿枋隹捎脭?shù)據(jù),而不是實(shí)現(xiàn)和優(yōu)化特定端點(diǎn)。

GraphQL不僅指定了描述Schema和從Schema中檢索數(shù)據(jù)的方法,而且還指定了將查詢轉(zhuǎn)換為結(jié)果的實(shí)際執(zhí)行算法。 該算法的核心非常簡(jiǎn)單:逐個(gè)遍歷查詢,為每個(gè)字段執(zhí)行“解析器”,然后返回與請(qǐng)求的結(jié)構(gòu)相對(duì)應(yīng)的結(jié)果,該結(jié)果通常會(huì)是 JSON 的格式。

本節(jié)闡述如下內(nèi)容:

  • GraphQL查詢
  • Schema和解析器
  • GraphQL查詢執(zhí)行步驟。

GraphQL查詢

GraphQL查詢結(jié)構(gòu)非常簡(jiǎn)單,易于理解。如下:

{
  subscribers(publication: "apollo-stack"){
    name
    email
  }
}

上面API響應(yīng)結(jié)果大致如下:

{
  subscribers: [
    { name: "Jane Doe", email: "jane@doe.com" },
    { name: "John Doe", email: "john@doe.com" },
    ...
  ]
}

響應(yīng)的結(jié)構(gòu)幾乎與查詢中的結(jié)構(gòu)相同。 GraphQL的客戶端非常簡(jiǎn)單,它實(shí)際上是非常明顯的!

但服務(wù)端如何?是不是超級(jí)復(fù)雜?

事實(shí)上,GraphQL服務(wù)端也非常簡(jiǎn)單。繼續(xù)往下看。

Schema與解析器(Resolve Functions)

每個(gè)GraphQL服務(wù)端都有兩個(gè)核心部分:Schema和解析器(Resolve Functions)。

Schema是可以通過(guò)GraphQL服務(wù)端獲取的數(shù)據(jù)模型。 它定義了允許客戶端進(jìn)行的查詢,及可以從服務(wù)端獲取的數(shù)據(jù)類型以及這些類型中的字段和之間的關(guān)系。

下圖表示一個(gè)簡(jiǎn)單的GraphQL Schema,有三種類型:AuthorPostQuery

簡(jiǎn)單的GraphQL Schema示例

使用GraphQL Schema表示,代碼如下:

type Author {
  id: Int
  name: String
  posts: [Post]
}
type Post {
  id: Int
  title: String
  text: String
  author: Author
}
type Query {
  getAuthor(id: Int): Author
  getPostsByTitle(titleContains: String): [Post]
}
schema {
  query: Query
}

這個(gè)Schema非常簡(jiǎn)單:它聲明應(yīng)用程序有三種類型 - Author,PostQuery。

第三種類型——Query——就是標(biāo)記Schema的入口。每個(gè)查詢都必須從其中一個(gè)字段開始:getAuthorgetPostsByTitle??梢詫⑺鼈兛醋饔悬c(diǎn)像RESTful的斷電(endpoints),不過(guò)更強(qiáng)大。

Author,Post互相引用:

  • 可以通過(guò)Authorposts字段獲取Author發(fā)布的Post,即從作者獲取作者發(fā)布的帖子;
  • 也可以通過(guò)Postauthor字段獲取PostAuthor,即從帖子獲取帖子的作者。

Schema告訴服務(wù)端允許客戶端進(jìn)行哪些查詢,以及不同類型的關(guān)聯(lián)方式,但是它不包含一條關(guān)鍵信息:每種類型的數(shù)據(jù)來(lái)自何處!這是解析器的作用所在,請(qǐng)繼續(xù)往下看。

解析器(Resolve Functions)

解析函數(shù)就像微型“路由器”。

解析器指定了Schema中的類型和字段如何連接到各種后端,回答了“如何獲取作者的數(shù)據(jù)?”和“需要使用哪些后端調(diào)用以獲取帖子的數(shù)據(jù)?”等問題。

GraphQL解析器可以包含任意代碼,這意味著GraphQL服務(wù)端可以連接任何類型的后端,甚至與其它GraphQL服務(wù)端進(jìn)行通信。 例如,Author類型可以存儲(chǔ)在SQL數(shù)據(jù)庫(kù)中,而Posts存儲(chǔ)在MongoDB中,甚至可以由微服務(wù)處理。

也許GraphQL的最大特點(diǎn)是它隱藏了客戶端的所有后端復(fù)雜性。 無(wú)論您的應(yīng)用程序使用多少個(gè)后端,所有客戶端都會(huì)看到一個(gè)GraphQL端點(diǎn)(endpoint),并為應(yīng)用程序提供了一個(gè)簡(jiǎn)單的自帶文檔的API。

以下是兩個(gè)解析函數(shù)的示例:

getAuthor(_, args){
  return sql.raw('SELECT * FROM authors WHERE id = %s', args.id);
}
posts(author){
  return request(`https://api.blog.io/by_author/${author.id}`);
}

當(dāng)然,您不會(huì)直接在解析函數(shù)中編寫查詢或URL,而會(huì)將其放在單獨(dú)的模塊中,你懂的???。?/p>

GraphQL查詢執(zhí)行過(guò)程

Alright,既然已了解Schema和解析器(Resolve Functions),那么讓我們看一下實(shí)際查詢的執(zhí)行。

附注:下面的代碼是GraphQL-JS——GraphQL的JavaScript參考實(shí)現(xiàn),但執(zhí)行模式在所有GraphQL服務(wù)端中都是相同的。

本小節(jié)來(lái)說(shuō)說(shuō)GraphQL服務(wù)端如何使用Schema和解析器以執(zhí)行查詢并生成所需結(jié)果。

這是一個(gè)與前面介紹的Schema一起使用的查詢。它獲取作者的姓名,該作者的所有帖子以及每個(gè)帖子的作者姓名。

{
  getAuthor(id: 5){
    name
    posts {
      title
      author {
        name # this will be the same as the name above
      }
    }
  }
}

您會(huì)注意到此查詢兩次獲取同一作者的名稱。在這里用來(lái)說(shuō)明GraphQL,同時(shí)保持Schema盡可能簡(jiǎn)單。不要慌??!!

以下是服務(wù)器響應(yīng)查詢所需的三個(gè)主要步驟:

  • 解析(Parse)
  • 驗(yàn)證(Validate)
  • 執(zhí)行(Execute)

第一步:解析查詢(Parsing the query)

首先,服務(wù)器解析字符串并將其轉(zhuǎn)換為AST - 一種抽象語(yǔ)法樹(Abstract Syntax Tree)。如果存在任何語(yǔ)法錯(cuò)誤,服務(wù)器將停止執(zhí)行并將語(yǔ)法錯(cuò)誤返回給客戶端。

第二步:驗(yàn)證(Validate)

查詢可以在語(yǔ)法上正確,但仍然沒有意義,就像下面的英語(yǔ)句子在語(yǔ)法上是正確的,但沒有任何意義:“The sand left through the idea”。

驗(yàn)證階段確保在執(zhí)行開始之前查詢?cè)诮o定Schema中是有效的。它會(huì)檢查以下內(nèi)容:

  • getAuthorQuery類型的一個(gè)字段?
  • getAuthor接受名為id的參數(shù)嗎?
  • getAuthor返回的類型上的名稱和帖子字段是什么?
  • 更多…

作為應(yīng)用程序開發(fā)人員,您無(wú)需擔(dān)心此部分,因?yàn)?strong>GraphQL服務(wù)器會(huì)自動(dòng)執(zhí)行驗(yàn)證操作。

與RESTful API形成對(duì)比,RESTful API中取決于開發(fā)人員來(lái)確保所有參數(shù)都有效。

第三步:執(zhí)行(Execute)

如果驗(yàn)證通過(guò),GraphQL服務(wù)端將執(zhí)行查詢。

每個(gè)GraphQL查詢都樹形的,從查詢的根(root)開始執(zhí)行。 首先,執(zhí)行程序使用提供的參數(shù)調(diào)用頂級(jí)字段的解析器 - 在本例中為getAuthor。 它等待所有解析器返回一個(gè)值,然后以樹形方式繼續(xù)向下進(jìn)行。 如果一個(gè)解析器返回promise,則執(zhí)行程序?qū)⒌却?,直到該promise被解析。

執(zhí)行從頂部開始。同時(shí)執(zhí)行同一級(jí)別的解析器,如下圖所示:

GraphQL 查詢執(zhí)行流程

如果對(duì)如何構(gòu)建GraphQL服務(wù)感興趣可參見Apollo。

Apollo Engine

更多參見GraphQL explained-How GraphQL turns a query into a response。

架構(gòu)

GraphQL 僅作為規(guī)范發(fā)布。 這意味著 GraphQL 實(shí)際上只不過(guò)是一個(gè)詳細(xì)描述 GraphQL 服務(wù)器行為的長(zhǎng)文檔。

用例

在本節(jié)中,我們將向您介紹包含 GraphQL 服務(wù)器的 3 種不同架構(gòu):

  • 連接數(shù)據(jù)庫(kù)的 GraphQL 服務(wù)
  • GraphQL 服務(wù)是許多第三方或遺留系統(tǒng)前面的一層,并通過(guò)單個(gè) GraphQL API 集成它們
  • 連接數(shù)據(jù)庫(kù)和第三方或遺留系統(tǒng)的混合方法,可以通過(guò)相同的 GraphQL API 進(jìn)行訪問

所有這三種體系結(jié)構(gòu)都代表了 GraphQL 的主要用例,并展示了可以使用它的上下文的靈活性。

連接數(shù)據(jù)庫(kù)的 GraphQL 服務(wù)

這種架構(gòu)將是綠地項(xiàng)目最常見的。 在設(shè)置中,您有一個(gè)實(shí)現(xiàn) GraphQL 規(guī)范的單個(gè)(Web)服務(wù)器。 當(dāng)查詢到達(dá) GraphQL 服務(wù)器時(shí),服務(wù)器將讀取查詢的有效內(nèi)容并從數(shù)據(jù)庫(kù)中獲取所需的信息。 這稱為解析查詢。 然后,它按照官方規(guī)范中的描述構(gòu)造響應(yīng)對(duì)象,并將其返回給客戶端。

GraphQL 實(shí)際上與傳輸層無(wú)關(guān)。 它可以與任何可用的網(wǎng)絡(luò)協(xié)議一起使用。 因此,有可能實(shí)現(xiàn)基于 TCP,WebSockets 等的 GraphQL 服務(wù)器。

GraphQL 也不關(guān)心數(shù)據(jù)庫(kù)或用于存儲(chǔ)數(shù)據(jù)的格式。 您可以使用 AWS Aurora 等 SQL 數(shù)據(jù)庫(kù)或 MongoDB 等 NoSQL 數(shù)據(jù)庫(kù)。

連接到單個(gè)數(shù)據(jù)庫(kù)的 GraphQL 服務(wù)器。

GraphQL 層集成現(xiàn)有系統(tǒng)

GraphQL 的另一個(gè)主要用例是將多個(gè)現(xiàn)有系統(tǒng)集成在一個(gè)統(tǒng)一的 GraphQL API 之后。 對(duì)于擁有傳統(tǒng)基礎(chǔ)架構(gòu)和許多不同 API 的公司來(lái)說(shuō),這尤其具有吸引力,這些 API 已經(jīng)發(fā)展多年,現(xiàn)在帶來(lái)了很高的維護(hù)負(fù)擔(dān)。 這些遺留系統(tǒng)的一個(gè)主要問題是,它們幾乎不可能構(gòu)建需要訪問多個(gè)系統(tǒng)的創(chuàng)新產(chǎn)品。

在這種情況下,GraphQL 可用于統(tǒng)一這些現(xiàn)有系統(tǒng),并隱藏其優(yōu)秀的 GraphQL API 背后的復(fù)雜性。 這樣,可以開發(fā)新的客戶端應(yīng)用程序,只需與 GraphQL 服務(wù)器通信即可獲取所需的數(shù)據(jù)。 然后,GraphQL 服務(wù)器負(fù)責(zé)從現(xiàn)有系統(tǒng)中獲取數(shù)據(jù)并以 GraphQL 響應(yīng)格式對(duì)其進(jìn)行打包。

就像之前的架構(gòu)一樣,GraphQL 服務(wù)器并不關(guān)心所使用的數(shù)據(jù)庫(kù)類型,在這里它并不關(guān)心獲取解析(resolve) 查詢獲取數(shù)據(jù)所需的數(shù)據(jù)源。

GraphQL 允許您隱藏現(xiàn)有系統(tǒng)的復(fù)雜性,例如微服務(wù),傳統(tǒng)基礎(chǔ)架構(gòu)或單個(gè) GraphQL 接口背后的第三方 API。

混合方式:連接數(shù)據(jù)庫(kù)并與現(xiàn)有系統(tǒng)集成

最后,可以將這兩種方法結(jié)合起來(lái)構(gòu)建一個(gè) GraphQL 服務(wù)器,該服務(wù)器具有連接的數(shù)據(jù)庫(kù),但仍然可以與傳統(tǒng)或第三方系統(tǒng)進(jìn)行通信。

當(dāng)服務(wù)器收到查詢時(shí),它將解析(resolve)它并從連接的數(shù)據(jù)庫(kù)或某些集成的 API 中檢索所需的數(shù)據(jù)。

兩種方法也可以組合在一起,GraphQL 服務(wù)器可以從單個(gè)數(shù)據(jù)庫(kù)以及現(xiàn)有系統(tǒng)中獲取數(shù)據(jù) - 從而實(shí)現(xiàn)完全的靈活性并將所有數(shù)據(jù)管理復(fù)雜性推向服務(wù)器。

解析(Resolver)器

但是我們?nèi)绾瓮ㄟ^(guò) GraphQL 獲得這種靈活性? 它是如何非常適合這些非常不同的用例?

GraphQL Resolver

正如您在前面所了解到的,GraphQL 查詢(query)或變更(mutation)的有效負(fù)載(payload)由一組字段組成。 在 GraphQL 服務(wù)器實(shí)現(xiàn)中,這些字段中的每一個(gè)實(shí)際上都對(duì)應(yīng)于一個(gè)稱為解析器的函數(shù)。 解析器功能的唯一目的是獲取其字段的數(shù)據(jù)。

當(dāng)服務(wù)器收到查詢時(shí),它將調(diào)用查詢有效負(fù)載中指定的字段的所有函數(shù)。 因此,它解析了查詢,并能夠?yàn)槊總€(gè)字段檢索正確的數(shù)據(jù)。 一旦所有解析器返回,服務(wù)器將以查詢描述的格式打包數(shù)據(jù)并將其發(fā)送回客戶端。

上圖包含一些已解析的字段名稱。 查詢中的每個(gè)字段對(duì)應(yīng)一個(gè)解析器函數(shù)。 當(dāng)查詢進(jìn)入以獲取指定數(shù)據(jù)時(shí),GraphQL 會(huì)調(diào)用所有必需的解析器。

GraphQL 客戶端庫(kù)

GraphQL 對(duì)于前端開發(fā)人員來(lái)說(shuō)特別棒,因?yàn)樗耆?REST API 遇到的許多不便和缺點(diǎn),例如上面提到的過(guò)度獲取不足獲取。 復(fù)雜性被推向服務(wù)器端,強(qiáng)大的機(jī)器可以處理繁重的計(jì)算工作。 客戶端不必知道它所獲取的數(shù)據(jù)實(shí)際來(lái)自何處,并且可以使用單個(gè),連貫且靈活的 API。

考慮一下 GraphQL 引入的主要變化,從一個(gè)相當(dāng)命令式的數(shù)據(jù)獲取方法轉(zhuǎn)變?yōu)榧兇獾穆暶鞣绞健?從 REST API 獲取數(shù)據(jù)時(shí),大多數(shù)應(yīng)用程序必須執(zhí)行以下步驟:

  1. 構(gòu)造并發(fā)送 HTTP 請(qǐng)求(例如,使用 Javascript 中的 fetch)
  2. 接收和解析服務(wù)器響應(yīng)
  3. 在本地存儲(chǔ)數(shù)據(jù)(簡(jiǎn)單地在內(nèi)存中或持久存儲(chǔ))
  4. 在 UI 中顯示數(shù)據(jù)

使用理想的聲明性數(shù)據(jù)獲取方法,客戶不應(yīng)該超過(guò)以下兩個(gè)步驟:

  • 描述數(shù)據(jù)要求
  • 在 UI 中顯示數(shù)據(jù)

應(yīng)該抽象出所有較低級(jí)別的網(wǎng)絡(luò)任務(wù)以及數(shù)據(jù)存儲(chǔ),并且數(shù)據(jù)依賴性的聲明應(yīng)該是主要部分。

這正是像 RelayApollo Client 這樣的 GraphQL 客戶端庫(kù)將使您能夠做到的事情。 它們提供了所需的抽象,使您能夠?qū)W⒂趹?yīng)用程序的重要部分,而不必處理重復(fù)的基礎(chǔ)結(jié)構(gòu)實(shí)現(xiàn)。

查詢和變更

字段(Fields)

{
  hero {
    name
    # 這是備注
    friends {
      name
    }
  }
}

返回結(jié)果

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

針對(duì)hero的查詢包含兩個(gè)字段:

  • name字段返回String類型,
  • friends字段返回?cái)?shù)組,其中包含的字段有:
    • name字段返回String類型

GraphQL 查詢能夠遍歷相關(guān)對(duì)象及其字段,使得客戶端可以一次請(qǐng)求查詢大量相關(guān)數(shù)據(jù),而不像傳統(tǒng) REST 架構(gòu)中那樣需要多次往返查詢。

參數(shù)(Arguments)

GraphQL 中每一個(gè)字段和嵌套對(duì)象都可以有的一組參數(shù),從而使得 GraphQL 可以替代多次 API 請(qǐng)求。

{
  human(id: "1000") {
    name
    height(unit: FOOT)
  }
}

返回結(jié)果

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

::: warning
GraphQL 中,所有參數(shù)必須具名傳遞。
:::

別名(Aliases)

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

返回結(jié)果

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

如果兩個(gè)hero字段會(huì)存在沖突,通過(guò)別名以避免.

片段(Fragments)

從這里開始,為了精簡(jiǎn)本文內(nèi)容,不再列出返回結(jié)果。

::: tip
片段定義了一段可重復(fù)使用的查詢代碼,通常用在復(fù)雜的查詢場(chǎng)景,如多級(jí)嵌套查詢。
:::

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

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

上面查詢?cè)诘?10-16 行定義了一個(gè)名稱為comparisonFields的片段,且其類型為Character,在第 3 和第 6 行使用。

注意,片段不能引用其自身,因?yàn)檫@會(huì)導(dǎo)致結(jié)果無(wú)限循環(huán)。

片段中使用變量

片段可以訪問查詢(query)或變更(mutation)中聲明的變量。

query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

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

使用片段時(shí),片段名稱前面的...可以看作 ES6 中的展開運(yùn)算符類似。

操作名稱(Operation name)

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

示例代碼包含了操作類型(query)和操作名稱(HeroNameAndFriends)。

這之前,我們使用了簡(jiǎn)寫句法,省略了 query 關(guān)鍵字和操作名稱,實(shí)際使用其可以代碼減少歧義。

  • 操作類型

    可以是query、mutation、subscription,用以描述什么類型的操作,簡(jiǎn)寫語(yǔ)法可省略query。

  • 操作名稱

    一個(gè)有意義的名稱,可以理解為 js 中的函數(shù)名稱,方便調(diào)試和維護(hù)。

變量(Variables)

GraphQL 可以將查詢或變更的參數(shù)分離出來(lái),作為變量傳遞給查詢或變更。

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

可以簡(jiǎn)單的理解為 js 中的函數(shù)定義,不同類型的參數(shù),調(diào)用時(shí)給參數(shù)提供不同的變量值。

變量格式為:$variableName : variableType = defaultValue,其中= defaultValue為默認(rèn)值,可選。

  • 變量名前綴必須為 $ ,后跟其類型,本例中為 Episode
  • 所有聲明的變量都必須是標(biāo)量、枚舉型或者輸入對(duì)象類型
  • 變量定義可以是可選的或者必要的,本例中,Episode 后并沒有 !,因此其是可選的
  • 變量可以帶默認(rèn)值,本例中默認(rèn)值為"JEDI"。

指令(Directives)

假設(shè)我們需要使用變量,來(lái)動(dòng)態(tài)改變查詢結(jié)果的結(jié)構(gòu)。

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

1.當(dāng)參數(shù)為"withFriends": false

"withFriends": false`
{
  "episode": "JEDI",
  "withFriends": false
}

返回結(jié)果如下:

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

2.當(dāng)參數(shù)為"withFriends": true

"withFriends": true
{
  "episode": "JEDI",
  "withFriends": true
}

返回結(jié)果如下:

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

指令是附加在字段(或片段 fragments 中的字段),并以服務(wù)端期待的方式改變查詢的執(zhí)行。

GraphQL 的核心規(guī)范包含兩個(gè)指令,其必須被任何規(guī)范兼容的 GraphQL 服務(wù)器實(shí)現(xiàn)所支持

  • @include(if: Boolean) 僅在參數(shù)為 true 時(shí),包含此字段。
  • @skip(if: Boolean) 如果參數(shù)為 true,跳過(guò)此字段。

需要?jiǎng)討B(tài)改變查詢結(jié)果結(jié)構(gòu)時(shí)記得有指令來(lái)幫你搞定。

當(dāng)然,服務(wù)端實(shí)現(xiàn)也可以定義新的指令來(lái)添加新的特性。

變更(Mutations)

RESTful 中不建議使用GET請(qǐng)求修改數(shù)據(jù),建議使用PUTPOSTHTTP 方法來(lái)修改數(shù)據(jù)。

GraphQL 中 約定采用變更(mutaion)來(lái)發(fā)送所有會(huì)導(dǎo)致數(shù)據(jù)修改的請(qǐng)求。

變更請(qǐng)求如下例,注意操作類型變成了mutation

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

參數(shù)如下:

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

返回結(jié)果如下:

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

注意,createReview操作返回了新建的Review類型的startscommentary字段,返回結(jié)果與 mutation 中定義的結(jié)構(gòu)依然相同;同查詢(query)一樣,也可以使用嵌套結(jié)構(gòu)的結(jié)果。

參數(shù)中review并非標(biāo)量,而是一個(gè)對(duì)象類型,GraphQL 中稱其為輸入對(duì)象類型(nput object type),詳情見 Schema - 輸入類型(Input Types)。

變更中的多個(gè)操作字段(Multiple fields in mutations)

一個(gè)變更中也能包含多個(gè)操作字段,和查詢一樣(參見別名)。

查詢(query)是并行執(zhí)行,而變更(mutation)是線性執(zhí)行,即一個(gè)成功后再接著執(zhí)行下一個(gè)。

在一個(gè)請(qǐng)求中發(fā)送兩個(gè)incrementCredits變更,第一個(gè)會(huì)確保在第二個(gè)之前執(zhí)行。

內(nèi)聯(lián)片段(Inline Fragments)

GraphQL schema 也具備定義接口和聯(lián)合類型的能力(更多參見 Schema-接口(Interface))。

如果查詢返回的字段是接口或者聯(lián)合類型,那么需要使用內(nèi)聯(lián)片段來(lái)取出下層具體類型的數(shù)據(jù)。

假如存在如下 schema 定義:

interface Character {
  id: ID!
  name: String!
}

type Human implements Character {
  id: ID!
  name: String!
  height: Float
}

type Droid implements Character {
  id: ID!
  name: String!
  primaryFunction: String
}

查詢?nèi)缦拢?/p>

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
  }
}

這個(gè)查詢中,hero 字段返回 Character 接口類型,取決于 episode 參數(shù)實(shí)際可能是 Human 或者 Droid 類型。在直接選擇的情況下,你只能請(qǐng)求 Character接口中存在的字段,譬如 name。

如果要請(qǐng)求具體類型上的字段,你需要使用類型條件內(nèi)聯(lián)片段。因?yàn)榈谝粋€(gè)片段標(biāo)注為 ... on Droid,primaryFunction字段僅在 hero 返回的 CharacterDroid 類型時(shí)才會(huì)執(zhí)行。 Human 類型的 height 字段也是這個(gè)道理。

具名片段也可以用于同樣的情況,因?yàn)榫呙慰偸歉綆Я艘粋€(gè)類型。

1.當(dāng)參數(shù)為"ep":"EMPIRE"時(shí)

"ep":"EMPIRE"
{
  "ep": "EMPIRE"
}

返回結(jié)果如下,包含 Human 類型的height字段:

{
  "data": {
    "hero": {
      "name": "Luke Skywalker",
      "height": 1.72
    }
  }
}

2.當(dāng)參數(shù)為"ep":"JEDI"時(shí)

"ep":"JEDI"
{
  "ep": "JEDI"
}

返回結(jié)果如下,包含 Droid 類型的primaryFunction字段:

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

元字段(Meta fields)

有時(shí),我們并不知道從 GraphQL 服務(wù)端獲取什么類型,這時(shí)需要一些方法在客戶端處理數(shù)據(jù)。

GraphQL 允許你在查詢的任何位置請(qǐng)求 __typename 元字段,以獲得其對(duì)象類型名稱。

{
  jediHero: hero(episode: JEDI) {
    __typename
  }

  emprireHero: hero(episode: EMPIRE) {
    __typename
  }
}

返回

{
  "data": {
    "jediHero": {
      "__typename": "Droid"
    },
    "emprireHero": {
      "__typename": "Human"
    }
  }
}

如果沒有__typename字段,沒法獲取返回具體的類型到底是什么。

GraphQL 服務(wù)提供了不少元字段,剩下的部分用于描述內(nèi)省系統(tǒng),更多參見內(nèi)省(Introspection)

Schema 和類型

GrqphQL Schema Language速覽

類型系統(tǒng)(Type System)

每一個(gè) GraphQL 服務(wù)都會(huì)定義一套類型即 schema,用以描述服務(wù)端提供什么樣的數(shù)據(jù)。每當(dāng)查詢請(qǐng)求到來(lái),服務(wù)端會(huì)根據(jù) schema 驗(yàn)證并執(zhí)行查詢。

schema 是對(duì)服務(wù)端提供數(shù)據(jù)的準(zhǔn)確描述。

類型語(yǔ)言(Type Language)

GraphQL 服務(wù)端可以用任何語(yǔ)言編寫,其并不依賴于任何特定語(yǔ)言(譬如 JavaScript)來(lái)與 GraphQL schema 溝通。

GraphQL schema language —— 它和 GraphQL 的查詢語(yǔ)言很相似。

對(duì)象類型和字段(Object Types and Fields)

GraphQL schema 中的最基本的組件是對(duì)象類型,它就表示你可以從服務(wù)端獲取到什么類型的對(duì)象,以及對(duì)象包含哪些字段。

使用 GraphQL schema language,可以這樣表示:

type Character {
  name: String!
  appearsIn: [Episode!]!
}
  • Character 是一個(gè) GraphQL 對(duì)象類型,表示其是一個(gè)擁有一些字段的類型。你的 schema 中的大多數(shù)類型都會(huì)是對(duì)象類型。
    nameappearsInCharacter 類型上的字段。這意味著在一個(gè)操作 Character 類型的 GraphQL 查詢中的任何部分,都只能出現(xiàn) nameappearsIn 字段。
  • String 是內(nèi)置的標(biāo)量類型之一 —— 標(biāo)量類型是解析到單個(gè)標(biāo)量對(duì)象的類型,無(wú)法在查詢中對(duì)它進(jìn)行次級(jí)選擇。后面我們將細(xì)述標(biāo)量類型。
  • String! 表示這個(gè)字段是非空的,GraphQL 服務(wù)保證當(dāng)你查詢這個(gè)字段后總會(huì)給你返回一個(gè)值。在類型語(yǔ)言里面,我們用一個(gè)感嘆號(hào)來(lái)表示這個(gè)特性。
  • [Episode!]! 表示一個(gè) Episode類型的對(duì)象數(shù)組。因?yàn)樗彩欠强盏?,所以?dāng)查詢 appearsIn 字段的時(shí)候,總能得到一個(gè)數(shù)組(零個(gè)或者多個(gè)元素)。且由于 Episode! 也是非空的,你總是可以預(yù)期到數(shù)組中的每個(gè)項(xiàng)目都是一個(gè) Episode 對(duì)象,而不會(huì)為NULL

Schema 中的參數(shù)(Arguments)

GraphQL 對(duì)象類型上的每一個(gè)字段都可能有零個(gè)或者多個(gè)參數(shù).

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

GraphQL 中,所有參數(shù)必須具名傳遞。

參數(shù)可能是必選或者可選的,當(dāng)一個(gè)參數(shù)是可選的,可以定義一個(gè)默認(rèn)值。

查詢和變更類型(The Query and Mutation Types)

schema 中大部分的類型都是普通對(duì)象類型,但是一個(gè) schema 內(nèi)有兩個(gè)特殊類型QueryMutation

schema {
  query: Query
  mutation: Mutation
}

QueryMutation類型定義了每一個(gè) GraphQL 查詢的入口。

每一個(gè) GraphQL 服務(wù)都有一個(gè) query 類型,可能(也可能沒有)有一個(gè) mutation 類型。

如果看到下面的查詢:

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

則 GraphQL 服務(wù)端的 schema 必然有:

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

標(biāo)量類型(Scalar Types)

標(biāo)量類型字段沒有任何次級(jí)字段,是 GraphQL 查詢的葉子節(jié)點(diǎn)。

GraphQL 自帶一組默認(rèn)標(biāo)量類型:

  • Int:有符號(hào) 32 位整數(shù)。
  • Float:有符號(hào)雙精度浮點(diǎn)值。
  • String:UTF‐8 字符序列。
  • Boolean:true 或者 false。
  • ID:ID 標(biāo)量類型表示一個(gè)唯一標(biāo)識(shí)符,通常用以重新獲取對(duì)象或者作為緩存中的鍵。ID 類型使用和 String 一樣的方式序列化;然而將其定義為 ID 意味著并不需要人類可讀型。

大部分的 GraphQL 服務(wù)實(shí)現(xiàn)中,都有自定義標(biāo)量類型。例如,我們可以定義一個(gè) Date 類型:

scalar Date

自定義標(biāo)量就取決于服務(wù)端的實(shí)現(xiàn)時(shí)如何定義將其序列化、反序列化和驗(yàn)證。例如,你可以指定 Date 類型應(yīng)該總是被序列化成整型時(shí)間戳,而客戶端應(yīng)該知道去要求任何 Date 字段都是這個(gè)格式。

枚舉類型(Enumeration Types)

枚舉類型是一種特殊的標(biāo)量,它限制在一個(gè)特殊的可選值集合內(nèi)。

定義一個(gè)Episode枚舉

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

列表和非空(Lists and Non-Null)

可以給類型附加類型修飾符。

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

非空

類型名后面添加一個(gè)感嘆號(hào)!將其標(biāo)注為非空。

有點(diǎn)像數(shù)據(jù)庫(kù)表結(jié)構(gòu)定義時(shí)字段不允許為空。即服務(wù)端對(duì)于這個(gè)字段,總是會(huì)返回一個(gè)非空值

如果它結(jié)果得到了一個(gè)空值,那么事實(shí)上將會(huì)觸發(fā)一個(gè) GraphQL 執(zhí)行錯(cuò)誤,以讓客戶端知道發(fā)生了錯(cuò)誤。

非空類型修飾符也可以用于參數(shù),如果參數(shù)上傳遞了一個(gè)空值(不管通過(guò) GraphQL 字符串還是變量),那么會(huì)導(dǎo)致服務(wù)器返回一個(gè)驗(yàn)證錯(cuò)誤。

如:

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

參數(shù)為"id": null

{
  "id": null
}

服務(wù)端返回驗(yàn)證錯(cuò)誤

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

列表

通過(guò)方括號(hào) [] 將類型包裹起來(lái)以表示列表。

即服務(wù)端會(huì)返回列表類型,列表用于參數(shù)也類似。

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

myField: [String!]

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

myField: null // 錯(cuò)誤
myField: [] // 有效
myField: ['a', 'b'] // 有效
myField: ['a', null, 'b'] // 有效

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

接口(Interfaces)

接口是一個(gè)抽象類型,它包含某些字段,而接口的實(shí)現(xiàn)必須包含這些字段。

例如,Character 接口用以表示《星球大戰(zhàn)》三部曲中的任何角色:

interface Character {
  id: ID!
  name: String!
}

注意,上面代碼中的interface,其表示定義一個(gè)接口。

一旦定義了接口,那么任何實(shí)現(xiàn) Character 接口的類型都要必須具有這些字段,并有對(duì)應(yīng)參數(shù)和返回類型。例如:

type Human implements Character {
  id: ID!
  name: String!
  height: Float
}

type Droid implements Character {
  id: ID!
  name: String!
  primaryFunction: String
}

如上代碼所示,HumanDroid類型是對(duì)Character接口的實(shí)現(xiàn),同時(shí)也具有自己的字段heightprimaryFunction。

查詢和變更-內(nèi)聯(lián)片段中所示:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
  }
}

當(dāng)返回一個(gè)對(duì)象或者一組對(duì)象,特別是一組不同的類型時(shí),接口就顯得特別有用。

可參見內(nèi)聯(lián)片段了解更多相關(guān)信息。

聯(lián)合類型(Union Types)

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

union SearchResult = Human | Droid | Starship

上面的 schema 定義中,任何返回一個(gè) SearchResult 類型的地方,都可能得到一個(gè) Human、Droid 或者 Starship類型。注意,聯(lián)合類型的成員需要是具體對(duì)象類型;你不能使用接口或者其他聯(lián)合類型來(lái)創(chuàng)造一個(gè)聯(lián)合類型。

如果你需要查詢一個(gè)返回 SearchResult 聯(lián)合類型的字段,則需要使用內(nèi)聯(lián)片段,如:

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

返回

{
  "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
      }
    ]
  }
}

前面說(shuō)過(guò),_typename 元字段解析為 String類型,可以在客戶端區(qū)分不同的數(shù)據(jù)類型。

由于 HumanDroid 共享一個(gè)公共接口(Character),我們可以在一個(gè)地方查詢它們的公共字段,而不必在多個(gè)類型中重復(fù)相同的字段:

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

輸入類型(Input Types)

目前為止,我們只討論過(guò)將例如枚舉和字符串等標(biāo)量值作為參數(shù)傳遞給字段,但是你也能很容易地傳遞復(fù)雜對(duì)象。這在變更(mutation)中特別有用,因?yàn)橛袝r(shí)候你需要傳遞一整個(gè)對(duì)象作為新建對(duì)象。在 GraphQL schema language 中,輸入對(duì)象看上去和常規(guī)對(duì)象一模一樣,除了關(guān)鍵字是 input 而不是 type

input ReviewInput {
  stars: Int!
  commentary: String
}

可以像下面代碼這樣在變更(mutation)中使用輸入對(duì)象類型:

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

參數(shù)為

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

輸入對(duì)象類型上的字段本身也可以為輸入對(duì)象類型(即輸入對(duì)象類型可嵌套)。

不能在 schema 中混淆輸入和輸出類型。輸入對(duì)象類型的字段不能擁有參數(shù)。

內(nèi)省(Introspection)

GraphQL 通過(guò)內(nèi)省機(jī)制告訴客戶端,服務(wù)端都提供哪些查詢、變更或訂閱,以及服務(wù)端定義的類型和字段。

如果是自己設(shè)計(jì)了類型系統(tǒng),那自己當(dāng)然知道哪些類型是可用的。但如果類型不是自己設(shè)計(jì)的,可以通過(guò)查詢 __schema 字段來(lái)向 GraphQL 服務(wù)端詢問哪些類型是可用的。一個(gè)查詢的根類型總是有 __schema 這個(gè)字段。如下代碼所示:

{
  __schema {
    types {
      name
    }
  }
}

返回結(jié)果如下:

{
  "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"
        }
      ]
    }
  }
}
  • 內(nèi)建類型:Query、Mutation、Subscription是 GraphQL Schema 內(nèi)建的特殊類型,其中Query必須有。
  • 自定義類型:Character, Human, Episode, Droid等 - 這些是我們?cè)陬愋拖到y(tǒng)中定義的類型。
  • 標(biāo)量類型:String、IntFLoat、BooleanID為標(biāo)量類型.
  • 內(nèi)省系統(tǒng)類型:__Schema、__Type、__TypeKind、__Field、__InputValue__EnumValue、__Directive、__DirectiveLocation這些以兩個(gè)下劃線__開頭的類型屬于 GraphQL Schema 內(nèi)省系統(tǒng)。

GraphQL Schema 的內(nèi)省系統(tǒng)可通過(guò)查詢操作的根級(jí)類型上的元字段__schema__type來(lái)進(jìn)行。

可以通過(guò)內(nèi)省系統(tǒng)接觸到類型系統(tǒng)的文檔,并做出文檔瀏覽器,或是提供豐富的 IDE 體驗(yàn)。

GraphQL Playground

更多關(guān)于內(nèi)省的說(shuō)明參見GraphQL 規(guī)范-內(nèi)省。

參考 & 引用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容