GraphQL 是一種新的 API 標準,它提供了一種更高效运嗜、強大和靈活的數(shù)據(jù)提供方式从媚,由 Facebook 發(fā)起并開源甘磨,目前由來自世界各地的大公司和個人共同維護橡羞。
GraphQL 本質(zhì)上是一種基于 API 的查詢語言,現(xiàn)在大多數(shù)應(yīng)用程序都需要從服務(wù)器中獲取數(shù)據(jù)济舆,這些數(shù)據(jù)存儲可能存儲在數(shù)據(jù)庫中卿泽,API 的職責(zé)是提供與應(yīng)用程序需求相匹配的存儲數(shù)據(jù)的接口。
我們經(jīng)常把 GraphQL 和數(shù)據(jù)庫技術(shù)相混淆滋觉,這是一個誤解签夭,GraphQL 是 API 的查詢語言,而不是數(shù)據(jù)庫椎侠,它是數(shù)據(jù)庫無關(guān)的覆致,而且可以在使用 API 的任何環(huán)境中有效使用。
我們經(jīng)常把 GraphQL 和數(shù)據(jù)庫技術(shù)相混淆肺蔚,這是一個誤解煌妈,GraphQL 是 API 的查詢語言,而不是數(shù)據(jù)庫宣羊,它是數(shù)據(jù)庫無關(guān)的璧诵,而且可以在使用 API 的任何環(huán)境中有效使用。
GraphQL 是基于 API 之上的一層封裝仇冯,目的是為了更好之宿、更靈活的適用于業(yè)務(wù)的需求變化。
GraphQL 產(chǎn)生的歷史背景
提起 API 設(shè)計的時候苛坚,大家通常會想到 SOAP比被、RESTful 等設(shè)計方式,從 2000 年 RESTful 的理論被 Roy Fielding (HTTP 規(guī)范的主要編寫者之一)在其博士論文中提出的時候泼舱,在業(yè)界引起了很大反響等缀,因為這種設(shè)計理念更易于用戶的使用,所以便很快的被大家所接受娇昙。
RESTful 是目前(2019 年)從服務(wù)器公開 API 的最為流行的方式(沒有之一)尺迂,當 RESTful 的概念被提及出來時,客戶端應(yīng)用程序?qū)?shù)據(jù)的需求相對簡單冒掌,而開發(fā)的速度并沒有達到今天的水平噪裕。
RESTful 對于許多簡單的應(yīng)用程序來說是適合的,然而當業(yè)務(wù)越來越復(fù)雜股毫、客戶對系統(tǒng)的擴展性有更高要求時膳音,API 會發(fā)生巨大的變化,由此帶來了 API 設(shè)計的巨大挑戰(zhàn)铃诬。
更高效的數(shù)據(jù)加載方式
Facebook 發(fā)起 GraphQL 的最初原因是移動端用戶的爆發(fā)式增長需要更高效的數(shù)據(jù)加載方式祭陷。
GraphQL 最小化了需要網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量苍凛,從而極大地改善了在這些條件下運行的應(yīng)用程序。
適應(yīng)不同的前端框架和平臺
各種不同的前端框架和平臺颗胡,運行客戶端應(yīng)用程序的不同環(huán)境毫深,使得我們在構(gòu)建和維護一個符合所有需求的 API 變得困難吩坝,但使用 GraphQL 的每個客戶機都可以精確地訪問它需要的數(shù)據(jù)毒姨。
加快產(chǎn)品開發(fā)速度
持續(xù)部署已經(jīng)成為許多公司的標準,快速的迭代和頻繁的產(chǎn)品更新是必不可少的钉寝。
對于 RESTful API弧呐,服務(wù)器公開數(shù)據(jù)的方式常常需要修改,以滿足客戶端的特定需求和設(shè)計更改嵌纲,這阻礙了快速開發(fā)實踐和產(chǎn)品迭代俘枫。
在不同前端框架、不同平臺下想要加快產(chǎn)品快速開發(fā)變的越來越難逮走。
什么是 GraphQL
GraphQL 是一套 API 語言規(guī)范鸠蚪,F(xiàn)acebook 于 2012 年應(yīng)用于移動應(yīng)用,并于 2015 年開源师溅。
GraphQL 既是一種用于 API 的查詢語言也是一個滿足你數(shù)據(jù)查詢的運行時蘸鲸。
GraphQL 對 API 中的數(shù)據(jù)提供了一套易于理解的完整描述,使客戶端能夠準確地獲得它需要的窿锉、沒有冗余的數(shù)據(jù)酌摇,也使 API 更容易地隨著時間推移而演進,還能用于構(gòu)建強大的開發(fā)者工具嗡载,GraphQL 可以一次請求獲取你所需的所有數(shù)據(jù)窑多。
GraphQL 是處于 API 層的靈活的、開放的一套規(guī)范洼滚,將 GraphQL 置于現(xiàn)有后端上怯伊,可以比以往更快捷的構(gòu)建產(chǎn)品。
GraphQL 與 RESTful 簡單比較
GraphQL 可以理解為基于 RESTful 的一種封裝判沟,目的在于構(gòu)建使客戶端更加易用的服務(wù)耿芹,可以說 GraphQL 是更好的 RESTful 設(shè)計,號稱 RESTful 2.0挪哄。
在過去的十多年中吧秕,RESTful 已經(jīng)成為設(shè)計 WEB API 的實際標準。它提供了一些很棒的想法迹炼,比如無狀態(tài)服務(wù)器和結(jié)構(gòu)化的資源訪問砸彬。然而 RESTful API 表現(xiàn)得過于僵化颠毙,無法跟上客戶端需求的快速變化。
GraphQL 的開發(fā)是為了提高更多的靈活性和效率砂碉,它解決了與 RESTful API 交互時開發(fā)人員所經(jīng)歷的許多缺點和低效之處蛀蜜。
為了說明獲取數(shù)據(jù)時 RESTful 和 GraphQL 之間的主要區(qū)別,讓我們考慮一個簡單的示例場景:在 blog 應(yīng)用程序中增蹭,應(yīng)用程序需要顯示特定用戶的文章的標題滴某。同一屏幕還顯示該用戶最后 3 個關(guān)注者的名稱。REST 和 GraphQL 如何解決這種情況?
RESTful 訪問數(shù)據(jù)
使用 RESTful API 來現(xiàn)實時滋迈,我們通出荩可以通過訪問多次請求來收集數(shù)據(jù)。
我們可以通過下面的三步來實現(xiàn):
- 通過
/user/<id>
獲取初始用戶數(shù)據(jù) - 通過
/user/<id>/posts
返回用戶的所有帖子 - 請求
/user/<id>/followers
返回每個用戶的關(guān)注者列表
如下圖
在 GraphQL 的世界里我們不用多取數(shù)據(jù)饼灿,也不用擔(dān)心數(shù)據(jù)取少了幕侠,我們只需要按需獲取數(shù)據(jù)即可。
RESTful 最常見的問題之一是 API 的返回數(shù)據(jù)過多或者過少碍彭,這是因為客戶端獲取數(shù)據(jù)的唯一方法是通過訪問返回固定數(shù)據(jù)結(jié)構(gòu)的 endpoint晤硕,這就會導(dǎo)致我們設(shè)計 API 非常困難,因為它既要能夠為客戶提供精確的數(shù)據(jù)需求庇忌,又需要滿足不同調(diào)用者的需求舞箍,這本身就是相互矛盾的。GraphQL 的發(fā)明者 Lee Byron 提出了一個很重要的概念: “用圖形來思考漆枚,而不是 endpoint”创译。
GraphQL 優(yōu)勢
強類型 Schema
對大多數(shù) API 而言,最大的問題在于缺少強類型約束墙基。常見場景為左电,后端 API 更新了镶殷,但文檔沒跟上,你沒法知道新的 API 是干什么的,怎么用为朋,這應(yīng)該是前后端掐架的原因之一盲链。
GraphQL Schema 是每個 GraphQL API 的基礎(chǔ)壕吹,它清晰的定義了每個 API 支持的操作所踊,包括輸入的參數(shù)和返回的內(nèi)容。
GraphQL Schema 是一份契約恼布,用于指明 API 的功能螺戳。
GraphQL Schema 是強類型的,可使用 SDL(GraphQL Schema Definition Language)來定義折汞。相對而言倔幼,強類型系統(tǒng)使開發(fā)人員自行車換摩托。比如爽待,可以使用構(gòu)建工具驗證 API 請求损同,編譯時檢查 API 調(diào)用可能發(fā)生的錯誤翩腐,甚至可以在代碼編輯器中自動完成 API 操作。
Schema 帶來的另一個好處是膏燃,不用再去編寫 API 文檔茂卦,因為根據(jù) Schema 自動生成了,這改變了 API 開發(fā)的玩法组哩。
按需獲取
我們經(jīng)常談 GraphQL 的主要優(yōu)點——前端可以從 API 獲取想要的數(shù)據(jù)等龙,不必依賴服務(wù)端 RESTful API 返回的固定數(shù)據(jù)結(jié)構(gòu)。
如此禁炒,解決了傳統(tǒng) REST API 的兩個典型問題:過度獲取(Over Fetching) 和 不足獲取(Under Fetching)而咆。
使用 GraphQL霍比,客戶端自己決定返回的數(shù)據(jù)及結(jié)構(gòu)幕袱。
過度獲取
意味著前端得到了實際不需要的數(shù)據(jù),這可能會造成性能和帶寬浪費悠瞬。
通常情況下我們在調(diào)用一個通用 API 接口時们豌,客戶端獲取的信息比應(yīng)用程序中實際需要的要多。例如 UI 需要顯示一個用戶列表浅妆,而實際上只需要使用他們的名字望迎。在 RESTful API 中通常會調(diào)用 /users
這個 endpoint,并接收一個帶有用戶數(shù)據(jù)的 JSON 數(shù)組凌外。但是這個響應(yīng)可能包含更多關(guān)于返回的用戶的信息辩尊,例如他們的生日或地址,而這些信息對客戶來說是無用的康辑,因為它只需要顯示用戶的名字摄欲。
不足獲取
不足獲取與過度獲取想反,API 返回中缺少足夠的數(shù)據(jù)疮薇,這意味著前端需要請求額外的 API 得到需要的數(shù)據(jù)胸墙。
最壞的場景下,會導(dǎo)致臭名昭著的 N+1 請求問題:獲取數(shù)據(jù)列表按咒,而沒有 API 能夠滿足列表字段要求迟隅,不得不對每行數(shù)據(jù)發(fā)起一次請求,以獲取所需額外數(shù)據(jù)励七。
假設(shè)我們在搗鼓一個博客應(yīng)用智袭,需要顯示 user
列表,除user
本身信息外掠抬,還要顯示每個 user
最近一篇文章的title
吼野。然而,調(diào)用/users/
僅得到用戶信息集合剿另,然后不得不對每個user
調(diào)用/users/<id>/articles
獲取其最新文章title
箫锤。
當然你可以再寫一個 API 來滿足特殊場景贬蛙,如/users/lastarticles/
來滿足上面的需求,但需要編寫后端相關(guān)代碼谚攒,調(diào)試和部署阳准,加班....有這時間何不去陪家人、孩子馏臭。
GraphQL 支持快速產(chǎn)品開發(fā)
GraphQL 使前端開發(fā)人員輕松愉悅野蝇,感謝 GraphQL 的前端庫(Apollo、Relay或Urql)括儒,前端可以用如緩存绕沈、實時更新 UI 等功能。
前端開發(fā)人員生產(chǎn)力提高帮寻,產(chǎn)品開發(fā)速度加快乍狐,無論 UI 如何變化后端服務(wù)不用變。
構(gòu)建 GraphQL API 的過程大多圍繞 GraphQL Scheme固逗。由此浅蚪,經(jīng)常聽到 Schema-Driven Development(SDD),這只是指一個過程烫罩,結(jié)構(gòu)在 Schema 中定義惜傲,在 Resolver(解析器)中實現(xiàn)。
有了像GraphQL Faker這樣的工具贝攒,前端開發(fā)可以在 Schema 定義后開始盗誊。GraphQL Faker 模擬整個 GraphQL API(依賴于定義的 schema),因此前后端可以獨立工作隘弊。
Schema 與 java 中的 interface 有點類似哈踱,java 開發(fā)中,大家可以先定義一個接口(interface)长捧,然后基于接口各自干各自的事情嚣鄙,互不干擾。
組合 GraphQL API
Schema 拼接(stitching)是 GraphQL 中的新概念之一串结,簡而言之哑子,可以組合和連接多個 GraphQL API 合并為一個。與 React 或 Vue 中的組件概念類似肌割,一個 GraphQL API 可以由多個 GraphQL API 組合構(gòu)成卧蜓。
這對前端開發(fā)非常有用,不然把敞,需要與多個 GraphQL endpoint 通信(通常在微服務(wù)架構(gòu)或與第三方 API 集成時)弥奸。由于 Schema 拼接,前端只需要處理單個 API endpoint奋早,而協(xié)調(diào)與各種服務(wù)通信的復(fù)雜性從前端隱藏盛霎。
豐富的開源生態(tài)和牛逼閃閃的社區(qū)
自 Facebook 正式發(fā)布 GraphQL 以來赠橙,僅三年多時間,整個 GraphQL 生態(tài)系統(tǒng)的成熟程度難以置信愤炸。
剛發(fā)布時期揪,開發(fā)人員使用 GraphQL 的唯一工具是graphql-js參考實現(xiàn)——一個 Express.js 中間件和 GraphQL client Relay。
現(xiàn)在规个,GraphQL 規(guī)范的參考實現(xiàn)幾乎覆蓋了大部分編程語言凤薛,并有大量的 GraphQL 客戶端。此外許多工具提供了無縫的工作流程诞仓,并在構(gòu)建 GraphQL API 時提供爽爽的開發(fā)體驗缤苫,如:Primsa、GraphQL Faker墅拭、GraphQL Playground活玲、graphql-config 等。
核心概念
SDL(Schema Definition Language)
GraphQL 有一套用于定義 API schema 的類型系統(tǒng). 編寫 schema 的語法我們稱其為 schema 定義語言(Schema Definition Language ,簡稱 SDL)帜矾。
以下代碼是我們使用 SDL 定義名為Person
的簡單類型的示例:
type Person {
name: String!
age: Int!
}
Person
類型有兩個字段翼虫,它們分別叫做name
和age
屑柔,分別是String
和Int
類型屡萤,類型后面的 !
該表示該字段是必需的。
也可以描述類型之間的關(guān)系掸宛,以博客應(yīng)用為例死陆,一個人(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!]!
}
請注意唧瘾,我們剛剛創(chuàng)建了 Person
和 Post
之間的一對多關(guān)系措译,因為 Person
上的 posts
字段實際上是一個帖子(Post
)數(shù)組。
使用查詢(Query)獲取數(shù)據(jù)
在使用 REST API 時饰序,從特定的端點( endpoint )加載數(shù)據(jù)领虹。每個端點都有明確定義的返回信息結(jié)構(gòu)。 這意味著客戶端的數(shù)據(jù)要求在其連接的 URL 中有效編碼求豫。
GraphQL 采用的方法完全不同塌衰。 GraphQL API 通常只暴露單個端點,而不是具有多個返回固定數(shù)據(jù)結(jié)構(gòu)的端點蝠嘉。 這是有效的最疆,因為返回的數(shù)據(jù)結(jié)構(gòu)不固定。 相反蚤告,它是完全靈活的努酸,讓客戶決定實際需要什么數(shù)據(jù)。這意味著客戶端需要向服務(wù)器發(fā)送更多信息以表達其數(shù)據(jù)需求 - 此信息稱為查詢杜恰。
基本查詢
我們看一下客戶端可以發(fā)送到服務(wù)器的示例查詢:
{
allPersons {
name
}
}
此查詢中的allPersons
字段稱為查詢的根字段获诈。 根字段后面的所有內(nèi)容稱為查詢的有效內(nèi)容仍源。 此查詢的有效內(nèi)容中指定的唯一字段是name
。
此查詢將返回當前存儲在數(shù)據(jù)庫中的所有人員的列表舔涎。 這是一個示例響應(yīng):
{
"data": {
"allPersons": [
{
"name": "Johnny"
},
{
"name": "Sarah"
},
{
"name": "Alice"
}
]
}
}
請注意镜会,每個人在響應(yīng)中只有name
,但服務(wù)器不會返回age
终抽。 這正是因為name
是查詢中指定的唯一字段戳表。
如果客戶端還需要人員的age
,則只需稍微調(diào)整查詢并在查詢的有效負載中包含新字段:
{
allPersons {
name
age
}
}
添加了age
字段的查詢將返回
{
"data": {
"allPersons": [
{
"name": "Johnny",
"age": 23
},
{
"name": "Sarah",
"age": 20
},
{
"name": "Alice",
"age": 20
}
]
}
}
嵌套查詢
GraphQL 的一個主要優(yōu)點是它允許查詢嵌套昼伴。 例如匾旭,如果您想加載Person
編寫的所有posts
,您只需按照類型的結(jié)構(gòu)來請求此信息:
{
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ù)价涝,則每個字段可以包含零個或多個參數(shù)。 例如持舆,allPersons
字段可以具有last
參數(shù)色瘩,以僅返回特定數(shù)量的人。 這是相應(yīng)的查詢的樣子:
{
allPersons(last: 2) {
name
}
}
返回結(jié)果
{
"data": {
"allPersons": [
{
"name": "Sarah"
},
{
"name": "Alice"
}
]
}
}
使用變更(Mutation)寫數(shù)據(jù)
在從服務(wù)器請求信息之后逸寓,大多數(shù)應(yīng)用程序還需要某種方式來更改當前存儲在后端中的數(shù)據(jù)居兆。 使用 GraphQL,這些更改是使用所謂的變更(Mutations,有時也叫突變)進行的竹伸。 通常有三種變更:
- 創(chuàng)建新數(shù)據(jù)
- 修改現(xiàn)有數(shù)據(jù)
- 刪除現(xiàn)有數(shù)據(jù)
變更遵循與查詢相同的語法結(jié)構(gòu)泥栖,但它們始終需要以mutation
關(guān)鍵字開頭。以下是我們?nèi)绾蝿?chuàng)建新Person
的示例:
mutation {
createPerson(name: "Bob", age: 36) {
name
age
}
}
以上代碼創(chuàng)建一個name
為Bob
勋篓,age
為36
的Person
對象吧享,并返回執(zhí)行結(jié)果中的name
和age
字段,執(zhí)行結(jié)果如下:
{
"data": {
"createPerson": {
"name": "Bob",
"age": 36
}
}
}
注意譬嚣,與我們之前編寫的查詢類似钢颂,變異也有一個根字段 - 在這里為createPerson
。 我們還已經(jīng)了解了字段參數(shù)的概念拜银。 在這里殊鞭,createPerson
字段采用兩個參數(shù)來指定新人的name
和age
。
與查詢一樣盐股,我們也可以為變更(mutation)指定有效負載(playload)钱豁,我們可以在其中請求新Person
對象的不同屬性。 例子中疯汁,我們詢問的是name
和age
- 盡管在我們的例子中這并不是非常有用牲尺,因為我們顯然已經(jīng)知道了它們,因為我們將它們傳遞給了變更。 但是谤碳,能夠在發(fā)送變更時查詢信息可以是一個非常強大的工具溃卡,允許您在單個往返中從服務(wù)器檢索新信息!
以后會發(fā)現(xiàn)蜒简,GraphQL 類型中通常會由服務(wù)端創(chuàng)建新對象時生成唯一 ID瘸羡,在之前的Person
類型中添加id
字段:
type Person {
id: ID!
name: String!
age: Int!
}
現(xiàn)在,當創(chuàng)建一個新Person
時搓茬,您可以直接在變更的有效負載中詢問id
犹赖,因為這是事先在客戶端上無法獲得的信息:
mutation {
createPerson(name: "Alice", age: 36) {
id
name
age
}
}
{
"data": {
"createPerson": {
"id": "cjyz7ocx10mwp0171660kf1gm",
"name": "Alice",
"age": 36
}
}
}
訂閱(Subscription)的實時更新
當今許多應(yīng)用程序的另一個重要要求是與服務(wù)器建立實時連接,以便立即了解重要事件卷仑。 對于此用例峻村,GraphQL 提供了訂閱(subscriptions)的概念。
當客戶端訂閱某個事件時锡凝,它將啟動并保持與服務(wù)器的穩(wěn)定連接粘昨。 每當該特定事件實際發(fā)生時,服務(wù)器就將相應(yīng)的數(shù)據(jù)推送到客戶端窜锯。 與典型的“request-response-cycle”之后的查詢和更改不同张肾,訂閱表示發(fā)送到客戶端的數(shù)據(jù)流。
訂閱使用與查詢和突變相同的語法編寫锚扎。 這是我們訂閱Person
類型上發(fā)生的事件的示例:
subscription {
newPerson {
name
age
}
}
客戶端將此訂閱(subscription)發(fā)送到服務(wù)器后吞瞪,將在它們之間建立長連接。 然后工秩,每當執(zhí)行創(chuàng)建新 Person
的變更時尸饺,服務(wù)器都會將有關(guān)此人的信息發(fā)送給客戶端:
{
"newPerson": {
"name": "Jane",
"age": 23
}
}
定義 Schema
至此,您已經(jīng)基本了解了query
助币,mutation
和subscription
,讓我們將它們結(jié)合起來螟碎,并了解如何編寫一個 schema眉菱,使您能夠執(zhí)行到目前為止看到的示例。
在使用 GraphQL API 時掉分,schema 是最重要的概念之一俭缓。
Schema 指定 API 的功能并定義客戶端如何請求數(shù)據(jù),其通常被視為服務(wù)端和客戶端之間的契約酥郭。
通常华坦,schema 只是 GraphQL 類型的集合。 但是不从,在為 API 編寫 schema 時惜姐,有一些特殊的根(root)類型:
type Query { ... }
type Mutation { ... }
type Subscription { ... }
Query
,Mutation
和Subscription
類型是客戶端發(fā)送的請求的入口點。 要使我們之前看到的allPersons
查詢可用歹袁,必須按如下方式編寫Query
類型:
type Query {
allPersons: [Person!]!
}
allPersons
被稱為 API 的根(root)字段坷衍。 再次考慮我們在 allPersons
字段中添加最后一個參數(shù)的示例,我們必須按如下方式編寫 Query
:
type Query {
allPersons(last: Int): [Person!]!
}
同樣条舔,對于createPerson
變更枫耳,我們必須在Mutation
類型中添加一個根字段:
type Mutation {
createPerson(name: String!, age: Int!): Person!
}
請注意,此根字段也有兩個參數(shù)孟抗,即新Person
的name
和age
迁杨。
最后,對于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ù),因為它使客戶能夠以比以前更好的方式獲取數(shù)據(jù)帆喇。 但是警医,API 本身當然是在服務(wù)器端實現(xiàn)的。 在服務(wù)端提供 GraphQL 服務(wù)還有許多好處坯钦,因為GraphQL 使服務(wù)器開發(fā)人員能夠?qū)W⒂诿枋隹捎脭?shù)據(jù)预皇,而不是實現(xiàn)和優(yōu)化特定端點。
GraphQL不僅指定了描述Schema和從Schema中檢索數(shù)據(jù)的方法婉刀,而且還指定了將查詢轉(zhuǎn)換為結(jié)果的實際執(zhí)行算法吟温。 該算法的核心非常簡單:逐個遍歷查詢,為每個字段執(zhí)行“解析器”突颊,然后返回與請求的結(jié)構(gòu)相對應(yīng)的結(jié)果鲁豪,該結(jié)果通常會是 JSON 的格式。
本節(jié)闡述如下內(nèi)容:
- GraphQL查詢
- Schema和解析器
- GraphQL查詢執(zhí)行步驟律秃。
GraphQL查詢
GraphQL查詢結(jié)構(gòu)非常簡單爬橡,易于理解。如下:
{
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的客戶端非常簡單糙申,它實際上是非常明顯的!
但服務(wù)端如何船惨?是不是超級復(fù)雜柜裸?
事實上,GraphQL服務(wù)端也非常簡單粱锐。繼續(xù)往下看疙挺。
Schema與解析器(Resolve Functions)
每個GraphQL服務(wù)端都有兩個核心部分:Schema和解析器(Resolve Functions)。
Schema是可以通過GraphQL服務(wù)端獲取的數(shù)據(jù)模型怜浅。 它定義了允許客戶端進行的查詢拳亿,及可以從服務(wù)端獲取的數(shù)據(jù)類型以及這些類型中的字段和之間的關(guān)系。
下圖表示一個簡單的GraphQL Schema施敢,有三種類型:Author
,Post
和Query
使用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
}
這個Schema非常簡單:它聲明應(yīng)用程序有三種類型 - Author
,Post
和Query
险掀。
第三種類型——Query
——就是標記Schema的入口沪袭。每個查詢都必須從其中一個字段開始:getAuthor
或getPostsByTitle
≌燎猓可以將它們看作有點像RESTful的斷電(endpoints)冈绊,不過更強大。
Author
埠啃,Post
互相引用:
- 可以通過
Author
的posts
字段獲取Author
發(fā)布的Post
死宣,即從作者獲取作者發(fā)布的帖子; - 也可以通過
Post
的author
字段獲取Post
的Author
碴开,即從帖子獲取帖子的作者毅该。
Schema告訴服務(wù)端允許客戶端進行哪些查詢,以及不同類型的關(guān)聯(lián)方式潦牛,但是它不包含一條關(guān)鍵信息:每種類型的數(shù)據(jù)來自何處眶掌!這是解析器的作用所在,請繼續(xù)往下看巴碗。
解析器(Resolve Functions)
解析函數(shù)就像微型“路由器”朴爬。
解析器指定了Schema中的類型和字段如何連接到各種后端,回答了“如何獲取作者的數(shù)據(jù)橡淆?”和“需要使用哪些后端調(diào)用以獲取帖子的數(shù)據(jù)召噩?”等問題。
GraphQL解析器可以包含任意代碼逸爵,這意味著GraphQL服務(wù)端可以連接任何類型的后端具滴,甚至與其它GraphQL服務(wù)端進行通信。 例如痊银,Author
類型可以存儲在SQL數(shù)據(jù)庫中抵蚊,而Posts
存儲在MongoDB中,甚至可以由微服務(wù)處理溯革。
也許GraphQL的最大特點是它隱藏了客戶端的所有后端復(fù)雜性。 無論您的應(yīng)用程序使用多少個后端谷醉,所有客戶端都會看到一個GraphQL端點(endpoint)致稀,并為應(yīng)用程序提供了一個簡單的自帶文檔的API。
以下是兩個解析函數(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}`);
}
當然俱尼,您不會直接在解析函數(shù)中編寫查詢或URL抖单,而會將其放在單獨的模塊中,你懂的??!矛绘!
GraphQL查詢執(zhí)行過程
Alright,既然已了解Schema和解析器(Resolve Functions)耍休,那么讓我們看一下實際查詢的執(zhí)行。
附注:下面的代碼是GraphQL-JS——GraphQL的JavaScript參考實現(xiàn)货矮,但執(zhí)行模式在所有GraphQL服務(wù)端中都是相同的羊精。
本小節(jié)來說說GraphQL服務(wù)端如何使用Schema和解析器以執(zhí)行查詢并生成所需結(jié)果。
這是一個與前面介紹的Schema一起使用的查詢囚玫。它獲取作者的姓名喧锦,該作者的所有帖子以及每個帖子的作者姓名。
{
getAuthor(id: 5){
name
posts {
title
author {
name # this will be the same as the name above
}
}
}
}
您會注意到此查詢兩次獲取同一作者的名稱抓督。在這里用來說明GraphQL燃少,同時保持Schema盡可能簡單。不要慌??A逶凇阵具!
以下是服務(wù)器響應(yīng)查詢所需的三個主要步驟:
- 解析(Parse)
- 驗證(Validate)
- 執(zhí)行(Execute)
第一步:解析查詢(Parsing the query)
首先,服務(wù)器解析字符串并將其轉(zhuǎn)換為AST - 一種抽象語法樹(Abstract Syntax Tree)定铜。如果存在任何語法錯誤阳液,服務(wù)器將停止執(zhí)行并將語法錯誤返回給客戶端。
第二步:驗證(Validate)
查詢可以在語法上正確宿稀,但仍然沒有意義趁舀,就像下面的英語句子在語法上是正確的,但沒有任何意義:“The sand left through the idea”祝沸。
驗證階段確保在執(zhí)行開始之前查詢在給定Schema中是有效的矮烹。它會檢查以下內(nèi)容:
-
getAuthor
是Query
類型的一個字段? -
getAuthor
接受名為id
的參數(shù)嗎罩锐? -
getAuthor
返回的類型上的名稱和帖子字段是什么奉狈? - 更多…
作為應(yīng)用程序開發(fā)人員,您無需擔(dān)心此部分涩惑,因為GraphQL服務(wù)器會自動執(zhí)行驗證操作仁期。
與RESTful API形成對比,RESTful API中取決于開發(fā)人員來確保所有參數(shù)都有效竭恬。
第三步:執(zhí)行(Execute)
如果驗證通過跛蛋,GraphQL服務(wù)端將執(zhí)行查詢。
每個GraphQL查詢都樹形的痊硕,從查詢的根(root)開始執(zhí)行赊级。 首先,執(zhí)行程序使用提供的參數(shù)調(diào)用頂級字段的解析器 - 在本例中為getAuthor
岔绸。 它等待所有解析器返回一個值理逊,然后以樹形方式繼續(xù)向下進行橡伞。 如果一個解析器返回promise,則執(zhí)行程序?qū)⒌却唬钡皆損romise被解析兑徘。
執(zhí)行從頂部開始。同時執(zhí)行同一級別的解析器羡洛,如下圖所示:
如果對如何構(gòu)建GraphQL服務(wù)感興趣可參見Apollo挂脑。
更多參見GraphQL explained-How GraphQL turns a query into a response。
架構(gòu)
GraphQL 僅作為規(guī)范發(fā)布翘县。 這意味著 GraphQL 實際上只不過是一個詳細描述 GraphQL 服務(wù)器行為的長文檔最域。
用例
在本節(jié)中,我們將向您介紹包含 GraphQL 服務(wù)器的 3 種不同架構(gòu):
- 連接數(shù)據(jù)庫的 GraphQL 服務(wù)
- GraphQL 服務(wù)是許多第三方或遺留系統(tǒng)前面的一層锈麸,并通過單個 GraphQL API 集成它們
- 連接數(shù)據(jù)庫和第三方或遺留系統(tǒng)的混合方法镀脂,可以通過相同的 GraphQL API 進行訪問
所有這三種體系結(jié)構(gòu)都代表了 GraphQL 的主要用例,并展示了可以使用它的上下文的靈活性忘伞。
連接數(shù)據(jù)庫的 GraphQL 服務(wù)
這種架構(gòu)將是綠地項目最常見的薄翅。 在設(shè)置中,您有一個實現(xiàn) GraphQL 規(guī)范的單個(Web)服務(wù)器氓奈。 當查詢到達 GraphQL 服務(wù)器時翘魄,服務(wù)器將讀取查詢的有效內(nèi)容并從數(shù)據(jù)庫中獲取所需的信息。 這稱為解析查詢舀奶。 然后暑竟,它按照官方規(guī)范中的描述構(gòu)造響應(yīng)對象,并將其返回給客戶端育勺。
GraphQL 實際上與傳輸層無關(guān)但荤。 它可以與任何可用的網(wǎng)絡(luò)協(xié)議一起使用。 因此涧至,有可能實現(xiàn)基于 TCP腹躁,WebSockets 等的 GraphQL 服務(wù)器。
GraphQL 也不關(guān)心數(shù)據(jù)庫或用于存儲數(shù)據(jù)的格式南蓬。 您可以使用 AWS Aurora 等 SQL 數(shù)據(jù)庫或 MongoDB 等 NoSQL 數(shù)據(jù)庫纺非。
GraphQL 層集成現(xiàn)有系統(tǒng)
GraphQL 的另一個主要用例是將多個現(xiàn)有系統(tǒng)集成在一個統(tǒng)一的 GraphQL API 之后赘方。 對于擁有傳統(tǒng)基礎(chǔ)架構(gòu)和許多不同 API 的公司來說烧颖,這尤其具有吸引力,這些 API 已經(jīng)發(fā)展多年窄陡,現(xiàn)在帶來了很高的維護負擔(dān)倒信。 這些遺留系統(tǒng)的一個主要問題是,它們幾乎不可能構(gòu)建需要訪問多個系統(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ù)器負責(zé)從現(xiàn)有系統(tǒng)中獲取數(shù)據(jù)并以 GraphQL 響應(yīng)格式對其進行打包。
就像之前的架構(gòu)一樣邪意,GraphQL 服務(wù)器并不關(guān)心所使用的數(shù)據(jù)庫類型九妈,在這里它并不關(guān)心獲取解析(resolve) 查詢獲取數(shù)據(jù)所需的數(shù)據(jù)源。
混合方式:連接數(shù)據(jù)庫并與現(xiàn)有系統(tǒng)集成
最后策菜,可以將這兩種方法結(jié)合起來構(gòu)建一個 GraphQL 服務(wù)器晶疼,該服務(wù)器具有連接的數(shù)據(jù)庫,但仍然可以與傳統(tǒng)或第三方系統(tǒng)進行通信又憨。
當服務(wù)器收到查詢時翠霍,它將解析(resolve)它并從連接的數(shù)據(jù)庫或某些集成的 API 中檢索所需的數(shù)據(jù)。
解析(Resolver)器
但是我們?nèi)绾瓮ㄟ^ GraphQL 獲得這種靈活性? 它是如何非常適合這些非常不同的用例躏将?
正如您在前面所了解到的锄弱,GraphQL 查詢(query
)或變更(mutation
)的有效負載(payload)由一組字段組成。 在 GraphQL 服務(wù)器實現(xiàn)中耸携,這些字段中的每一個實際上都對應(yīng)于一個稱為解析器的函數(shù)棵癣。 解析器功能的唯一目的是獲取其字段的數(shù)據(jù)。
當服務(wù)器收到查詢時夺衍,它將調(diào)用查詢有效負載中指定的字段的所有函數(shù)狈谊。 因此,它解析了查詢沟沙,并能夠為每個字段檢索正確的數(shù)據(jù)河劝。 一旦所有解析器返回,服務(wù)器將以查詢描述的格式打包數(shù)據(jù)并將其發(fā)送回客戶端矛紫。
GraphQL 客戶端庫
GraphQL 對于前端開發(fā)人員來說特別棒牡辽,因為它完全消除了 REST API 遇到的許多不便和缺點,例如上面提到的過度獲取和不足獲取敞临。 復(fù)雜性被推向服務(wù)器端态辛,強大的機器可以處理繁重的計算工作。 客戶端不必知道它所獲取的數(shù)據(jù)實際來自何處挺尿,并且可以使用單個奏黑,連貫且靈活的 API。
考慮一下 GraphQL 引入的主要變化编矾,從一個相當命令式的數(shù)據(jù)獲取方法轉(zhuǎn)變?yōu)榧兇獾穆暶鞣绞健?從 REST API 獲取數(shù)據(jù)時熟史,大多數(shù)應(yīng)用程序必須執(zhí)行以下步驟:
- 構(gòu)造并發(fā)送 HTTP 請求(例如,使用 Javascript 中的 fetch)
- 接收和解析服務(wù)器響應(yīng)
- 在本地存儲數(shù)據(jù)(簡單地在內(nèi)存中或持久存儲)
- 在 UI 中顯示數(shù)據(jù)
使用理想的聲明性數(shù)據(jù)獲取方法窄俏,客戶不應(yīng)該超過以下兩個步驟:
- 描述數(shù)據(jù)要求
- 在 UI 中顯示數(shù)據(jù)
應(yīng)該抽象出所有較低級別的網(wǎng)絡(luò)任務(wù)以及數(shù)據(jù)存儲蹂匹,并且數(shù)據(jù)依賴性的聲明應(yīng)該是主要部分。
這正是像 Relay 或 Apollo Client 這樣的 GraphQL 客戶端庫將使您能夠做到的事情裆操。 它們提供了所需的抽象怒详,使您能夠?qū)W⒂趹?yīng)用程序的重要部分,而不必處理重復(fù)的基礎(chǔ)結(jié)構(gòu)實現(xiàn)踪区。
查詢和變更
字段(Fields)
{
hero {
name
# 這是備注
friends {
name
}
}
}
返回結(jié)果
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
針對hero
的查詢包含兩個字段:
-
name
字段返回String
類型昆烁, -
friends
字段返回數(shù)組,其中包含的字段有:-
name
字段返回String
類型
-
GraphQL 查詢能夠遍歷相關(guān)對象及其字段缎岗,使得客戶端可以一次請求查詢大量相關(guān)數(shù)據(jù)静尼,而不像傳統(tǒng) REST 架構(gòu)中那樣需要多次往返查詢。
參數(shù)(Arguments)
GraphQL 中每一個字段和嵌套對象都可以有的一組參數(shù)传泊,從而使得 GraphQL 可以替代多次 API 請求鼠渺。
{
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"
}
}
}
如果兩個hero
字段會存在沖突拦盹,通過別名以避免.
片段(Fragments)
從這里開始,為了精簡本文內(nèi)容溪椎,不再列出返回結(jié)果普舆。
::: tip
片段定義了一段可重復(fù)使用的查詢代碼,通常用在復(fù)雜的查詢場景校读,如多級嵌套查詢沼侣。
:::
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
上面查詢在第 10-16 行定義了一個名稱為comparisonFields
的片段,且其類型為Character
歉秫,在第 3 和第 6 行使用蛾洛。
注意,片段不能引用其自身雁芙,因為這會導(dǎo)致結(jié)果無限循環(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
}
}
}
}
使用片段時,片段名稱前面的...
可以看作 ES6 中的展開運算符類似扶供。
操作名稱(Operation name)
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
示例代碼包含了操作類型(query
)和操作名稱(HeroNameAndFriends
)筛圆。
這之前,我們使用了簡寫句法椿浓,省略了
query
關(guān)鍵字和操作名稱,實際使用其可以代碼減少歧義闽晦。
-
操作類型
可以是
query
扳碍、mutation
、subscription
仙蛉,用以描述什么類型的操作笋敞,簡寫語法可省略query
。 -
操作名稱
一個有意義的名稱荠瘪,可以理解為 js 中的函數(shù)名稱夯巷,方便調(diào)試和維護。
變量(Variables)
GraphQL 可以將查詢或變更的參數(shù)分離出來哀墓,作為變量傳遞給查詢或變更趁餐。
query HeroNameAndFriends($episode: Episode = "JEDI") {
hero(episode: $episode) {
name
friends {
name
}
}
}
可以簡單的理解為 js 中的函數(shù)定義,不同類型的參數(shù)篮绰,調(diào)用時給參數(shù)提供不同的變量值后雷。
變量格式為:$variableName : variableType = defaultValue
,其中= defaultValue
為默認值吠各,可選臀突。
- 變量名前綴必須為
$
,后跟其類型贾漏,本例中為Episode
- 所有聲明的變量都必須是標量候学、枚舉型或者輸入對象類型
- 變量定義可以是可選的或者必要的,本例中,
Episode
后并沒有!
纵散,因此其是可選的 - 變量可以帶默認值,本例中默認值為
"JEDI"
梳码。
指令(Directives)
假設(shè)我們需要使用變量,來動態(tài)改變查詢結(jié)果的結(jié)構(gòu)困食。
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
1.當參數(shù)為"withFriends": false
{
"episode": "JEDI",
"withFriends": false
}
返回結(jié)果如下:
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
2.當參數(shù)為"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ī)范包含兩個指令硕盹,其必須被任何規(guī)范兼容的 GraphQL 服務(wù)器實現(xiàn)所支持:
-
@include(if: Boolean)
僅在參數(shù)為 true 時符匾,包含此字段。 -
@skip(if: Boolean)
如果參數(shù)為 true瘩例,跳過此字段啊胶。
需要動態(tài)改變查詢結(jié)果結(jié)構(gòu)時記得有指令來幫你搞定甸各。
當然,服務(wù)端實現(xiàn)也可以定義新的指令來添加新的特性焰坪。
變更(Mutations)
RESTful 中不建議使用GET
請求修改數(shù)據(jù)趣倾,建議使用PUT
、POST
HTTP 方法來修改數(shù)據(jù)某饰。
GraphQL 中 約定采用變更(mutaion
)來發(fā)送所有會導(dǎo)致數(shù)據(jù)修改的請求儒恋。
變更請求如下例,注意操作類型變成了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
類型的starts
和commentary
字段诫尽,返回結(jié)果與 mutation 中定義的結(jié)構(gòu)依然相同;同查詢(query)一樣炬守,也可以使用嵌套結(jié)構(gòu)的結(jié)果牧嫉。
參數(shù)中review
并非標量,而是一個對象類型减途,GraphQL 中稱其為輸入對象類型(nput object type)酣藻,詳情見 Schema - 輸入類型(Input Types)。
變更中的多個操作字段(Multiple fields in mutations)
一個變更中也能包含多個操作字段鳍置,和查詢一樣(參見別名)辽剧。
查詢(query)是并行執(zhí)行,而變更(mutation)是線性執(zhí)行墓捻,即一個成功后再接著執(zhí)行下一個抖仅。
在一個請求中發(fā)送兩個incrementCredits
變更,第一個會確保在第二個之前執(zhí)行砖第。
內(nèi)聯(lián)片段(Inline Fragments)
GraphQL schema 也具備定義接口和聯(lián)合類型的能力(更多參見 Schema-接口(Interface))撤卢。
如果查詢返回的字段是接口或者聯(lián)合類型,那么需要使用內(nèi)聯(lián)片段來取出下層具體類型的數(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
}
}
}
這個查詢中放吩,hero
字段返回 Character
接口類型,取決于 episode
參數(shù)實際可能是 Human
或者 Droid
類型羽杰。在直接選擇的情況下渡紫,你只能請求 Character
接口中存在的字段,譬如 name
考赛。
如果要請求具體類型上的字段惕澎,你需要使用類型條件內(nèi)聯(lián)片段。因為第一個片段標注為 ... on Droid
颜骤,primaryFunction
字段僅在 hero
返回的 Character
為 Droid
類型時才會執(zhí)行唧喉。 Human
類型的 height
字段也是這個道理。
具名片段也可以用于同樣的情況,因為具名片段總是附帶了一個類型八孝。
1.當參數(shù)為"ep":"EMPIRE"
時
{
"ep": "EMPIRE"
}
返回結(jié)果如下董朝,包含 Human
類型的height
字段:
{
"data": {
"hero": {
"name": "Luke Skywalker",
"height": 1.72
}
}
}
2.當參數(shù)為"ep":"JEDI"
時
{
"ep": "JEDI"
}
返回結(jié)果如下,包含 Droid
類型的primaryFunction
字段:
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
元字段(Meta fields)
有時干跛,我們并不知道從 GraphQL 服務(wù)端獲取什么類型子姜,這時需要一些方法在客戶端處理數(shù)據(jù)。
GraphQL 允許你在查詢的任何位置請求
__typename
元字段楼入,以獲得其對象類型名稱哥捕。
{
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 和類型
類型系統(tǒng)(Type System)
每一個 GraphQL 服務(wù)都會定義一套類型即 schema,用以描述服務(wù)端提供什么樣的數(shù)據(jù)呼巴。每當查詢請求到來泽腮,服務(wù)端會根據(jù) schema 驗證并執(zhí)行查詢。
schema 是對服務(wù)端提供數(shù)據(jù)的準確描述衣赶。
類型語言(Type Language)
GraphQL 服務(wù)端可以用任何語言編寫诊赊,其并不依賴于任何特定語言(譬如 JavaScript)來與 GraphQL schema 溝通。
GraphQL schema language —— 它和 GraphQL 的查詢語言很相似府瞄。
對象類型和字段(Object Types and Fields)
GraphQL schema 中的最基本的組件是對象類型碧磅,它就表示你可以從服務(wù)端獲取到什么類型的對象,以及對象包含哪些字段遵馆。
使用 GraphQL schema language鲸郊,可以這樣表示:
type Character {
name: String!
appearsIn: [Episode!]!
}
-
Character
是一個 GraphQL 對象類型,表示其是一個擁有一些字段的類型货邓。你的 schema 中的大多數(shù)類型都會是對象類型秆撮。
name
和appearsIn
是Character
類型上的字段。這意味著在一個操作Character
類型的 GraphQL 查詢中的任何部分换况,都只能出現(xiàn)name
和appearsIn
字段职辨。 -
String
是內(nèi)置的標量類型之一 —— 標量類型是解析到單個標量對象的類型,無法在查詢中對它進行次級選擇戈二。后面我們將細述標量類型舒裤。 -
String!
表示這個字段是非空的,GraphQL 服務(wù)保證當你查詢這個字段后總會給你返回一個值觉吭。在類型語言里面盏缤,我們用一個感嘆號來表示這個特性。 -
[Episode!]!
表示一個Episode
類型的對象數(shù)組绳匀。因為它也是非空的,所以當查詢appearsIn
字段的時候宏赘,總能得到一個數(shù)組(零個或者多個元素)。且由于Episode!
也是非空的黎侈,你總是可以預(yù)期到數(shù)組中的每個項目都是一個Episode
對象察署,而不會為NULL
。
Schema 中的參數(shù)(Arguments)
GraphQL 對象類型上的每一個字段都可能有零個或者多個參數(shù).
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
GraphQL 中峻汉,所有參數(shù)必須具名傳遞贴汪。
參數(shù)可能是必選或者可選的,當一個參數(shù)是可選的休吠,可以定義一個默認值扳埂。
查詢和變更類型(The Query and Mutation Types)
schema 中大部分的類型都是普通對象類型,但是一個 schema 內(nèi)有兩個特殊類型Query
和Mutation
:
schema {
query: Query
mutation: Mutation
}
Query
和Mutation
類型定義了每一個 GraphQL 查詢的入口瘤礁。
每一個 GraphQL 服務(wù)都有一個 query
類型阳懂,可能(也可能沒有)有一個 mutation
類型。
如果看到下面的查詢:
query {
hero {
name
}
droid(id: "2000") {
name
}
}
則 GraphQL 服務(wù)端的 schema 必然有:
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
標量類型(Scalar Types)
標量類型字段沒有任何次級字段,是 GraphQL 查詢的葉子節(jié)點柜思。
GraphQL 自帶一組默認標量類型:
- Int:有符號 32 位整數(shù)岩调。
- Float:有符號雙精度浮點值。
- String:UTF‐8 字符序列赡盘。
- Boolean:true 或者 false号枕。
- ID:ID 標量類型表示一個唯一標識符,通常用以重新獲取對象或者作為緩存中的鍵陨享。ID 類型使用和 String 一樣的方式序列化葱淳;然而將其定義為 ID 意味著并不需要人類可讀型。
大部分的 GraphQL 服務(wù)實現(xiàn)中抛姑,都有自定義標量類型赞厕。例如,我們可以定義一個 Date 類型:
scalar Date
自定義標量就取決于服務(wù)端的實現(xiàn)時如何定義將其序列化途戒、反序列化和驗證坑傅。例如,你可以指定 Date 類型應(yīng)該總是被序列化成整型時間戳喷斋,而客戶端應(yīng)該知道去要求任何 Date 字段都是這個格式唁毒。
枚舉類型(Enumeration Types)
枚舉類型是一種特殊的標量,它限制在一個特殊的可選值集合內(nèi)星爪。
定義一個Episode
枚舉
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
列表和非空(Lists and Non-Null)
可以給類型附加類型修飾符浆西。
type Character {
name: String!
appearsIn: [Episode]!
}
非空
類型名后面添加一個感嘆號
!
將其標注為非空。
有點像數(shù)據(jù)庫表結(jié)構(gòu)定義時字段不允許為空顽腾。即服務(wù)端對于這個字段近零,總是會返回一個非空值诺核。
如果它結(jié)果得到了一個空值,那么事實上將會觸發(fā)一個 GraphQL 執(zhí)行錯誤久信,以讓客戶端知道發(fā)生了錯誤窖杀。
非空類型修飾符也可以用于參數(shù),如果參數(shù)上傳遞了一個空值(不管通過 GraphQL 字符串還是變量)裙士,那么會導(dǎo)致服務(wù)器返回一個驗證錯誤入客。
如:
query DroidById($id: ID!) {
droid(id: $id) {
name
}
}
參數(shù)為"id": null
{
"id": null
}
服務(wù)端返回驗證錯誤
{
"errors": [
{
"message": "Variable \"$id\" of required type \"ID!\" was not provided.",
"locations": [
{
"line": 1,
"column": 17
}
]
}
]
}
列表
通過方括號
[
和]
將類型包裹起來以表示列表。
即服務(wù)端會返回列表類型腿椎,列表用于參數(shù)也類似桌硫。
非空和列表修飾符可以組合使用。例如你可以要求一個非空字符串的數(shù)組:
myField: [String!]
這表示數(shù)組本身可以為空啃炸,但是其不能有任何空值成員铆隘。用 JSON 舉例如下:
myField: null // 錯誤
myField: [] // 有效
myField: ['a', 'b'] // 有效
myField: ['a', null, 'b'] // 有效
可以根據(jù)需求嵌套任意層非空和列表修飾符。
接口(Interfaces)
接口是一個抽象類型南用,它包含某些字段膀钠,而接口的實現(xiàn)必須包含這些字段。
例如裹虫,Character 接口用以表示《星球大戰(zhàn)》三部曲中的任何角色:
interface Character {
id: ID!
name: String!
}
注意托修,上面代碼中的interface
,其表示定義一個接口恒界。
一旦定義了接口,那么任何實現(xiàn) Character
接口的類型都要必須具有這些字段砚嘴,并有對應(yīng)參數(shù)和返回類型十酣。例如:
type Human implements Character {
id: ID!
name: String!
height: Float
}
type Droid implements Character {
id: ID!
name: String!
primaryFunction: String
}
如上代碼所示,Human
和Droid
類型是對Character
接口的實現(xiàn)际长,同時也具有自己的字段height
和primaryFunction
耸采。
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Human {
height
}
... on Droid {
primaryFunction
}
}
}
當返回一個對象或者一組對象,特別是一組不同的類型時工育,接口就顯得特別有用虾宇。
可參見內(nèi)聯(lián)片段了解更多相關(guān)信息。
聯(lián)合類型(Union Types)
聯(lián)合類型和接口十分相似如绸,但是它并不指定類型之間的任何共同字段嘱朽。
union SearchResult = Human | Droid | Starship
上面的 schema 定義中,任何返回一個 SearchResult
類型的地方怔接,都可能得到一個 Human
搪泳、Droid
或者 Starship
類型。注意扼脐,聯(lián)合類型的成員需要是具體對象類型岸军;你不能使用接口或者其他聯(lián)合類型來創(chuàng)造一個聯(lián)合類型。
如果你需要查詢一個返回 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
}
]
}
}
前面說過佣谐,_typename
元字段解析為 String
類型,可以在客戶端區(qū)分不同的數(shù)據(jù)類型方妖。
由于 Human
和 Droid
共享一個公共接口(Character
)狭魂,我們可以在一個地方查詢它們的公共字段,而不必在多個類型中重復(fù)相同的字段:
{
search(text: "an") {
__typename
... on Character {
name
}
... on Human {
height
}
... on Droid {
primaryFunction
}
... on Starship {
name
length
}
}
}
輸入類型(Input Types)
目前為止吁断,我們只討論過將例如枚舉和字符串等標量值作為參數(shù)傳遞給字段趁蕊,但是你也能很容易地傳遞復(fù)雜對象。這在變更(mutation)中特別有用仔役,因為有時候你需要傳遞一整個對象作為新建對象掷伙。在 GraphQL schema language 中,輸入對象看上去和常規(guī)對象一模一樣又兵,除了關(guān)鍵字是 input
而不是 type
:
input ReviewInput {
stars: Int!
commentary: String
}
可以像下面代碼這樣在變更(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!"
}
}
輸入對象類型上的字段本身也可以為輸入對象類型(即輸入對象類型可嵌套)任柜。
不能在 schema 中混淆輸入和輸出類型。輸入對象類型的字段不能擁有參數(shù)沛厨。
內(nèi)省(Introspection)
GraphQL 通過內(nèi)省機制告訴客戶端宙地,服務(wù)端都提供哪些查詢、變更或訂閱逆皮,以及服務(wù)端定義的類型和字段宅粥。
如果是自己設(shè)計了類型系統(tǒng),那自己當然知道哪些類型是可用的电谣。但如果類型不是自己設(shè)計的秽梅,可以通過查詢 __schema
字段來向 GraphQL 服務(wù)端詢問哪些類型是可用的。一個查詢的根類型總是有 __schema
這個字段剿牺。如下代碼所示:
{
__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
等 - 這些是我們在類型系統(tǒng)中定義的類型。 - 標量類型:
String
湃崩、Int
荧降、FLoat
、Boolean
竹习、ID
為標量類型. - 內(nèi)省系統(tǒng)類型:
__Schema
誊抛、__Type
、__TypeKind
整陌、__Field
拗窃、__InputValue
瞎领、__EnumValue
、__Directive
随夸、__DirectiveLocation
這些以兩個下劃線__
開頭的類型屬于 GraphQL Schema 內(nèi)省系統(tǒng)九默。
GraphQL Schema 的內(nèi)省系統(tǒng)可通過查詢操作的根級類型上的元字段__schema
和__type
來進行。
可以通過內(nèi)省系統(tǒng)接觸到類型系統(tǒng)的文檔宾毒,并做出文檔瀏覽器驼修,或是提供豐富的 IDE 體驗。
更多關(guān)于內(nèi)省的說明參見GraphQL 規(guī)范-內(nèi)省诈铛。
參考 & 引用
- GraphQL 文檔(官方英文)
- GraphQL 規(guī)范(2019-08-07官方預(yù)發(fā)行草案)
- How do I GraphQL?(離線pdf)
- How to GraphQL
- How to build a GraphQL Server with graphql-yoga
- How to GraphQL : The fullstack GraphQL tutorial
- GraphQL boilerplates: Starter kits for GraphQL projects with Node, TypeScript, React, Vue,…
- 360linker-GraphQL 入門介紹
- 使用 GraphQL 的 5 個理由
- awesome-graphql Awesome
- GraphQL explained-How GraphQL turns a query into a response(離線pdf)