深入GraphQL 的使用語法
對(duì)于GraphQL 的使用語法在上一節(jié)中已經(jīng)大概介紹了基本的使用方式了账胧,這一篇將會(huì)對(duì)上一篇入門做拓展吹截,努力將所有的使用語法都覆蓋到。
1. 終端語法
首先是介紹在前端查詢時(shí)用的語法甫窟,分成Query 和Mutation 兩部分,Subscription 的和Query 是類似的就不特別說明了捌木。
1.1 Query
假設(shè)我們現(xiàn)在有一個(gè)數(shù)據(jù)集,結(jié)構(gòu)是這樣的:
- 學(xué)生和老師各有各自的特有字段职辅;
- 學(xué)生中有個(gè)獲取所有相關(guān)老師的方法,老師中也有一個(gè)獲取所有學(xué)生的方法聂示;
- 學(xué)生和老師互為多對(duì)多關(guān)系域携;
首先最簡單的使用方式我們可查:
query {
student {
id
name
teacher {
id
name
}
}
}
# 結(jié)果:
{
data: {
student: [
{
id: 1,
name: "張三",
teacher: [
{
id: 1,
name: "李老師"
},
{
id: 2,
name: "吳老師"
}
]
},
{
id: 2,
name: "李四",
teacher: [
{
id: 1,
name: "李老師"
}
]
},
{
id: 3,
name: "王三",
teacher: [
{
id: 2,
name: "吳老師"
}
]
}
]
}
}
我們通過上面的查詢可以得到總共有兩個(gè)學(xué)生,其中李老師同時(shí)教了他們兩人鱼喉。這是最最基本的用法秀鞭。下面我們慢慢加查詢需求去改變結(jié)果。
1.1.1 參數(shù) Arguments
我們可以通過添加參數(shù)的方式限制返回集的內(nèi)容
query {
student(name_contains: "三") { # <-- 這里限制了只要名字中包含了“三”的學(xué)生
id
name
teacher(id_eq: 2) { # <-- 這里限制了只要id=2 的老師
id
name
}
}
}
# 結(jié)果:
{
data: {
student: [
{
id: 1,
name: "張三",
teacher: [
{
id: 2,
name: "吳老師"
}
]
},
{
id: 3,
name: "王三",
teacher: [
{
id: 2,
name: "吳老師"
}
]
}
]
}
}
這時(shí)扛禽,因?yàn)槲覀冞^濾了只要id 為2 的老師锋边,所以李老師就給過濾掉了;因?yàn)樵O(shè)置了過濾只要名字中帶“三”字的學(xué)生编曼,所以李四也給過濾掉了豆巨。
同理,也可以用參數(shù)去對(duì)內(nèi)容進(jìn)行分頁灵巧、跳過等操作抹沪,操作相同就不寫例子了刻肄。
1.1.2 別名 Aliases
因?yàn)樵诓樵冎校煌臄?shù)據(jù)實(shí)體在Graphql 語句中本身是類似于直接請(qǐng)求這個(gè)單元的存在融欧,所以如果你同時(shí)請(qǐng)求兩個(gè)相同的集時(shí)它就會(huì)報(bào)錯(cuò)敏弃;因?yàn)樗鼈兌紤?yīng)該是一一對(duì)應(yīng)的。這時(shí)你就可以用別名來解決這個(gè)問題:
query {
san: student(name_contains: "三") {
id
name
}
wang: student(name_contains: "王") {
id
name
}
}
# 結(jié)果:
{
data: {
san: [
{
id: 1,
name: "張三"
}
]
wang: [
{
id: 3,
name: "王三"
}
]
}
}
處理請(qǐng)求結(jié)果的時(shí)候要注意用了別名的內(nèi)容是以別名為key 返回的噪馏,不是原來的名字了麦到。
1.1.3 片段 Fragments
看上面的查詢語句,我們可以看到當(dāng)用了不同的別名時(shí)我們難免會(huì)產(chǎn)生這種寫了一堆重復(fù)的字段名的情況欠肾。我們這個(gè)例子字段少還好瓶颠,但正常的業(yè)務(wù)要有個(gè)幾十個(gè)字段都是挺常見的,這樣寫可就太費(fèi)勁了刺桃,這時(shí)就可以祭出Fragments 來處理了:
fragment studentFields on Student {
id
name
}
query {
san: student(name_contains: "三") {
...studentFields
}
wang: student(name_contains: "王") {
...studentFields
}
}
# 結(jié)果:
{
data: {
san: [
{
id: 1,
name: "張三"
}
]
wang: [
{
id: 3,
name: "王三"
}
]
}
}
1.1.4 操作名 Operation name
這個(gè)相對(duì)來說是比較少用的粹淋,起碼我個(gè)人的使用情況來看,我基本更傾向于“能省就省”的原則瑟慈;但寫教程的話就還是介紹下吧桃移。主要出現(xiàn)在同時(shí)有多個(gè)操作的情況下,用于區(qū)分操作數(shù)據(jù)葛碧。
query op1 {
student(name_contains: "三") {
id
}
}
query op2 {
student(name_contains: "王") {
id
}
}
# op1, op2 就是操作名借杰。
# 但日常寫query 你甚至可以將操作符("query")也省了像下面這樣寫就行
{
student {
id
}
}
1.1.5 操作參數(shù) Variables
這個(gè)參數(shù)有別于上面提到的Arguments, Arguments 是用于具體數(shù)據(jù)結(jié)點(diǎn)操作用的。Variables 指的是面向操作符時(shí)进泼,可以讓Query 變得可復(fù)用蔗衡,也方便在不同地方使用纤虽。
假設(shè)我們有兩個(gè)不同的頁面,都要查詢學(xué)生表但過濾不同绞惦,這時(shí)如果我們寫兩個(gè)查詢像下面這樣就很浪費(fèi)廓推,代碼也很丑,也不能復(fù)用翩隧。
# 頁面一用的
query {
student(name_contains: "三") {
id
}
}
# 頁面二用的
query {
student(name_contains: "王") {
id
}
}
因?yàn)楸举|(zhì)上說樊展,它們查詢的內(nèi)容是相同的,只是參數(shù)有點(diǎn)不一樣堆生,這里我們可以把參數(shù)給提取出來妇穴,通過在實(shí)際使用時(shí)再由不同情況傳參就好:
# 頁面一阎曹、二都用同一個(gè)Query
query($name: String) {
student(name_contains: $name) {
id
}
}
使用時(shí)改變傳進(jìn)去的Variables, 例:
const query = gql`
query($name: String) {
student(name_contains: $name) {
id
}
}
`
const page1 = post(URL, query=query, variables={name: "三"})
const page2 = post(URL, query=query, variables={name: "王"})
這樣出來的結(jié)果就和上面寫兩個(gè)不同的Query 是一樣的,但代碼會(huì)優(yōu)雅很多,Query 也得到了合理復(fù)用财忽。如果有一天需要修改請(qǐng)求的返回結(jié)果,也不用跑到各個(gè)地方一個(gè)一個(gè)地修改請(qǐng)求的Query.
注意定義參數(shù)有幾個(gè)硬性規(guī)定:
- 參數(shù)名要以 $ 開頭厢拭。沒得商量不以美元符開頭它不認(rèn)的它匕。
- 參數(shù)的類型必須和它將會(huì)用到的地方的類型一樣,否則會(huì)出錯(cuò)寞射。因?yàn)镚raphql 是靜態(tài)類型的語言渔工。
- 可以以類似TS 的方式給參數(shù)默認(rèn)值,如下桥温。
# 這樣如果沒有給任何參數(shù)則$name 會(huì)默認(rèn)等于“三”
query($name: String = "三") {
student(name_contains: $name) {
id
}
}
1.1.6 指示符 Directives
Directives 可以翻譯成指示符引矩,但我覺得不太直觀,它的功能主要是類似一個(gè)條件修飾侵浸,類似代碼中的if-else 塊差不多的功能旺韭。讓你可以在外面指定要怎么請(qǐng)求的細(xì)節(jié)。
query($name: String, $withTeacher: Boolean!) {
student(name_contains: $name) {
id
teacher @include(if: $withTeacher) {
id
}
}
}
它的主要作用就是說掏觉,如果你在外面的variables 中給定withTeacher=true 那它就會(huì)請(qǐng)求teacher 節(jié)點(diǎn)区端,等同于:
query($name: String) {
student(name_contains: $name) {
id
teacher {
id
}
}
}
反之,如果指定withTeacher=false 那它就會(huì)省略teacher 節(jié)點(diǎn)澳腹,等同于:
query($name: String) {
student(name_contains: $name) {
id
}
}
Directives 主要有兩個(gè)操作符:@include(if: Boolean)
和 @skip(if: Boolean)
這兩個(gè)的作用相反织盼。另外Directives 這個(gè)功能需要服務(wù)端有相關(guān)支持才能用。但同時(shí)遵湖,如果需要服務(wù)端也可以自已實(shí)現(xiàn)完全自定義的Directives.
1.2 Mutation
1.2.1 操作參數(shù) Variables
這個(gè)和Query 那邊的規(guī)則完全一樣悔政,參見上面的內(nèi)容即可,給個(gè)小例子:
# 無參寫法
mutation create {
createStudent(name: "王五", age: 18) {
id
}
}
# 有參寫法
mutation create($name: String, $age: Int) {
createStudent(name: $name, age: $age) {
id
}
}
# 另一種有參寫法
# 假設(shè)createStudent 函數(shù)的參數(shù)的類型叫createStudentInput
mutation create($input: createStudentInput!) {
createStudent($input) {
id
}
}
1.2.2 行內(nèi)片段 Inline Fragments
這里的使用情景主要是針對(duì)聯(lián)合(Union) 類型的延旧,類似于接口(interface) 與類(class)的關(guān)系谋国。
假設(shè)我們有個(gè)接口叫動(dòng)物(Animal), 有兩個(gè)類分別是狗(Dog) 和鳥(Bird). 并且我們將這兩個(gè)類由一個(gè)GraphQL 節(jié)點(diǎn)給出去:
{
animal {
name
kind
... on Dog {
breed
}
... on Bird {
wings
}
}
}
# 結(jié)果
{
data: {
animal: [
{
name: "Pepe",
kind: "Dog",
breed: "Husky"
},
{
name: "Pipi",
kind: "Bird",
wings: 2
}
]
}
}
從上面的結(jié)果可以看出,它可以由不同的類型去查不同的“類”迁沫,但返回時(shí)可以合并返回芦瘾。就類似于是從一個(gè)“接口” 上直接獲取到實(shí)現(xiàn)類的數(shù)據(jù)了捌蚊,非常具體。但大部分情況下我們可能不會(huì)合并著查兩個(gè)不同結(jié)構(gòu)的數(shù)據(jù)以一個(gè)數(shù)組返回近弟,我們更多可能是用在于用同一個(gè)節(jié)點(diǎn)名(animal)就可以查不同的東西但先以他們的類型作了過濾缅糟。
1.2.3 元字段 Meta fields
配合上面的例子食用,如果我們沒有kind 那個(gè)字段時(shí)祷愉,我們要怎么知道哪個(gè)元素是哪個(gè)類型呢窗宦?我們可以用元字段去知道我們當(dāng)前操作的是哪個(gè)數(shù)據(jù)實(shí)體,主要的元字段有 __typename
.
我們可以這樣查:
{
animal {
name
__typename
... on Dog {
breed
}
... on Bird {
wings
}
}
}
# 結(jié)果
{
data: {
animal: [
{
name: "Pepe",
__typename: "Animal__Dog",
breed: "Husky"
},
{
name: "Pipi",
__typename: "Animal__Bird",
wings: 2
}
]
}
}
__typename
是內(nèi)置的二鳄,你可以在任何節(jié)點(diǎn)上查赴涵,它都會(huì)給你一個(gè)類型。
2. 類型定義
2.1 基礎(chǔ)
我們知道GraphQL 是一個(gè)靜態(tài)類型的語法系統(tǒng)订讼,那么我們?cè)谡嬲褂们熬捅仨毾榷x好它的類型髓窜。
GraphQL 的類型定義叫做Schemas. 有它自已獨(dú)立的語法。里面有各個(gè)基本類型Scalar欺殿,可以定義成不同對(duì)象的Type. 也可以自已用基本類型定義成新的類型寄纵。
所有的不同的對(duì)象最終會(huì)組成一個(gè)樹狀的結(jié)構(gòu),根由schema 組成:
schema {
query: Query
mutation: Mutation
}
然后再定義里面一層一層的子對(duì)象脖苏,比如我們上面那個(gè)模型大概可以寫成:
type Query {
student(name_contains: String): Student
teacher(id_eq: ID): Teacher
}
type Student {
id: ID!
name: String!
age: Int
teachers: [Teacher!]!
}
type Teacher {
id: ID!
name: String!
gender: Boolean
}
像上面這樣我們就定義了兩個(gè)不同的對(duì)象及他們的屬性程拭。其中,如果是必填或者說非空的字段則帶有"!" 在它的類型后面帆阳,比如id: ID!
就表明id 是個(gè)非空的字段哺壶。非空的字段如果在操作中給它傳null
會(huì)報(bào)錯(cuò)屋吨。另外某種類型組成的數(shù)組可以用類型加中括號(hào)組成蜒谤,比如上面的Student 里面的Teacher.
定義一個(gè)字段為數(shù)組:
myField: [String!]
這樣定義呢,表明了這個(gè)字段本身是可以為 null
的至扰,但它不能有 null
的成員鳍徽。比如說:
const myField: null // valid
const myField: [] // valid
const myField: ['a', 'b'] // valid
const myField: ['a', null, 'b'] // error
但如果,是這樣定義的:
myField: [String]!
則代表它本身不能為 null
但它的組成成員中可以包含 null
.
const myField: null // error
const myField: [] // valid
const myField: ['a', 'b'] // valid
const myField: ['a', null, 'b'] // valid
2.2 自帶類型
GraphQL 默認(rèn)的自帶類型只有5 種敢课。分別是:
ID: 就類似傳統(tǒng)數(shù)據(jù)庫中的ID 字段阶祭,主要用于區(qū)別不同的對(duì)象≈备眩可以直接是一個(gè)Int, 也可能是一個(gè)編碼過的唯一值濒募,比如常見的relay 中使用的是“類名:ID” 的字符串再經(jīng)base64 轉(zhuǎn)碼后的結(jié)果作為ID. 要注意的是這個(gè)ID 只是存在于Graphql 中的。它不一定和數(shù)據(jù)庫中的是對(duì)應(yīng)的圾结。比如relay 這個(gè)情況瑰剃,數(shù)據(jù)庫中存的可能還是一個(gè)整數(shù)并不是那個(gè)字符串。
Int: 整數(shù)筝野,可正負(fù)晌姚。
Float: 雙精度浮點(diǎn)數(shù)粤剧,可正負(fù)。
String: UTF-8 字符的字符串挥唠。
Boolean: true / false.
如果不能滿足你的業(yè)務(wù)場(chǎng)景你就可以自定義新的類型抵恋,或者是找第三方做好的拓展類型。
定義一個(gè)類型的Graphql 寫法很簡單宝磨,比如我們新增一個(gè)Date 類型弧关。
scalar Date
就這樣就可以了,但是你還需要在你的代碼中實(shí)現(xiàn)它的具體功能唤锉,怎么轉(zhuǎn)換出入運(yùn)行時(shí)等等梯醒。
另外,Graphql 中支持枚舉類型腌紧,可以這樣定義:
enum GenderTypes {
MALE
FEMALE
OTHERS
}
2.3 接口(Interface) 和聯(lián)合類型(Union)
Interface 和 Union 很像茸习,所以我就合在一起講了。
Interface 和其他語言的類似壁肋,都是為了給一個(gè)通用的父類型定義用的号胚。可以像這樣定義及使用:
interface Animal {
id: ID!
name: String
}
type Dog implements Animal {
id: ID!
name: String
breed: String
}
type Cat implements Animal {
id: ID!
name: String
color: String
}
可以看到浸遗,接口定義的每個(gè)字段在實(shí)現(xiàn)時(shí)都會(huì)帶上猫胁,但它也可以有自已的字段。查詢時(shí)跛锌,需要注意的是:你不可以直接在Animal 上查到各個(gè)獨(dú)有的字段弃秆,因?yàn)楫?dāng)你在Animal 上做查詢時(shí)系統(tǒng)并不知道你當(dāng)前查詢的對(duì)象是Dog 還是Cat. 你需要用inline fragment 去指定。
# 這樣查直接報(bào)錯(cuò):
# "Cannot query field \"color\" on type \"Animal\". Did you mean to use an inline fragment on \"Cat\"?"
query {
animal {
id
name
color
}
}
# 正確的打開方式:
query {
animal {
id
name
... on Cat {
color
}
}
}
講完Interface, 我們?cè)倏纯碪nion.
Union 你可以直接理解成是沒有共同字段的Interface.
union Plant = Lily | Rose | Daisy
查詢時(shí)和接口一樣得用inline fragments 去指定類型髓帽。
2.4 輸入類型 Input types
上面在那個(gè)Mutation 的Variables 舉例子時(shí)稍微提到過菠赚,就是給某個(gè)操作的輸入的所有參數(shù)指定成一個(gè)類型,這樣可以更方便地添加內(nèi)容也增加了代碼可復(fù)用的程度郑藏。
假設(shè)我們有一個(gè)Mutation 的定義是這樣的:
type Mutaion {
createSomething(foo: Int, bar: Float): Something
}
使用Input types:
input CreateSomethingInput {
foo: Int
bar: Float
}
type Mutaion {
createSomething(input: CreateSomethingInput): Something
}