深入GraphQL 的使用語法
對於GraphQL 的使用語法在上一節中已經大概介紹了基本的使用方式了,這一篇將會對上一篇入門做拓展,努力將所有的使用語法都覆蓋到。
1. 終端語法
首先是介紹在前端查詢時用的語法,分成Query 和Mutation 兩部分,Subscription 的和Query 是類似的就不特別説明了。
1.1 Query
假設我們現在有一個數據集,結構是這樣的:
- 學生和老師各有各自的特有字段;
- 學生中有個獲取所有相關老師的方法,老師中也有一個獲取所有學生的方法;
- 學生和老師互為多對多關係;
首先最簡單的使用方式我們可查:
query {
student {
id
name
teacher {
id
name
}
}
}
# 結果:
{
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: "吳老師"
}
]
}
]
}
}
我們通過上面的查詢可以得到總共有兩個學生,其中李老師同時教了他們兩人。這是最最基本的用法。下面我們慢慢加查詢需求去改變結果。
1.1.1 參數 Arguments
我們可以通過添加參數的方式限制返回集的內容
query {
student(name_contains: "三") { # <-- 這裏限制了只要名字中包含了“三”的學生
id
name
teacher(id_eq: 2) { # <-- 這裏限制了只要id=2 的老師
id
name
}
}
}
# 結果:
{
data: {
student: [
{
id: 1,
name: "張三",
teacher: [
{
id: 2,
name: "吳老師"
}
]
},
{
id: 3,
name: "王三",
teacher: [
{
id: 2,
name: "吳老師"
}
]
}
]
}
}
這時,因為我們過濾了只要id 為2 的老師,所以李老師就給過濾掉了;因為設置了過濾只要名字中帶“三”字的學生,所以李四也給過濾掉了。
同理,也可以用參數去對內容進行分頁、跳過等操作,操作相同就不寫例子了。
1.1.2 別名 Aliases
因為在查詢中,不同的數據實體在Graphql 語句中本身是類似於直接請求這個單元的存在,所以如果你同時請求兩個相同的集時它就會報錯;因為它們都應該是一一對應的。這時你就可以用別名來解決這個問題:
query {
san: student(name_contains: "三") {
id
name
}
wang: student(name_contains: "王") {
id
name
}
}
# 結果:
{
data: {
san: [
{
id: 1,
name: "張三"
}
]
wang: [
{
id: 3,
name: "王三"
}
]
}
}
處理請求結果的時候要注意用了別名的內容是以別名為key 返回的,不是原來的名字了。
1.1.3 片段 Fragments
看上面的查詢語句,我們可以看到當用了不同的別名時我們難免會產生這種寫了一堆重複的字段名的情況。我們這個例子字段少還好,但正常的業務要有個幾十個字段都是挺常見的,這樣寫可就太費勁了,這時就可以祭出Fragments 來處理了:
fragment studentFields on Student {
id
name
}
query {
san: student(name_contains: "三") {
...studentFields
}
wang: student(name_contains: "王") {
...studentFields
}
}
# 結果:
{
data: {
san: [
{
id: 1,
name: "張三"
}
]
wang: [
{
id: 3,
name: "王三"
}
]
}
}
1.1.4 操作名 Operation name
這個相對來説是比較少用的,起碼我個人的使用情況來看,我基本更傾向於“能省就省”的原則;但寫教程的話就還是介紹下吧。主要出現在同時有多個操作的情況下,用於區分操作數據。
query op1 {
student(name_contains: "三") {
id
}
}
query op2 {
student(name_contains: "王") {
id
}
}
# op1, op2 就是操作名。
# 但日常寫query 你甚至可以將操作符("query")也省了像下面這樣寫就行
{
student {
id
}
}
1.1.5 操作參數 Variables
這個參數有別於上面提到的Arguments, Arguments 是用於具體數據結點操作用的。Variables 指的是面向操作符時,可以讓Query 變得可複用,也方便在不同地方使用。
假設我們有兩個不同的頁面,都要查詢學生表但過濾不同,這時如果我們寫兩個查詢像下面這樣就很浪費,代碼也很醜,也不能複用。
# 頁面一用的
query {
student(name_contains: "三") {
id
}
}
# 頁面二用的
query {
student(name_contains: "王") {
id
}
}
因為本質上説,它們查詢的內容是相同的,只是參數有點不一樣,這裏我們可以把參數給提取出來,通過在實際使用時再由不同情況傳參就好:
# 頁面一、二都用同一個Query
query($name: String) {
student(name_contains: $name) {
id
}
}
使用時改變傳進去的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: "王"})
這樣出來的結果就和上面寫兩個不同的Query 是一樣的,但代碼會優雅很多,Query 也得到了合理複用。如果有一天需要修改請求的返回結果,也不用跑到各個地方一個一個地修改請求的Query.
注意定義參數有幾個硬性規定:
- 參數名要以 $ 開頭。沒得商量不以美元符開頭它不認的。
- 參數的類型必須和它將會用到的地方的類型一樣,否則會出錯。因為Graphql 是靜態類型的語言。
- 可以以類似TS 的方式給參數默認值,如下。
# 這樣如果沒有給任何參數則$name 會默認等於“三”
query($name: String = "三") {
student(name_contains: $name) {
id
}
}
1.1.6 指示符 Directives
Directives 可以翻譯成指示符,但我覺得不太直觀,它的功能主要是類似一個條件修飾,類似代碼中的if-else 塊差不多的功能。讓你可以在外面指定要怎麼請求的細節。
query($name: String, $withTeacher: Boolean!) {
student(name_contains: $name) {
id
teacher @include(if: $withTeacher) {
id
}
}
}
它的主要作用就是説,如果你在外面的variables 中給定withTeacher=true 那它就會請求teacher 節點,等同於:
query($name: String) {
student(name_contains: $name) {
id
teacher {
id
}
}
}
反之,如果指定withTeacher=false 那它就會省略teacher 節點,等同於:
query($name: String) {
student(name_contains: $name) {
id
}
}
Directives 主要有兩個操作符:@include(if: Boolean) 和 @skip(if: Boolean)
這兩個的作用相反。另外Directives 這個功能需要服務端有相關支持才能用。但同時,如果需要服務端也可以自已實現完全自定義的Directives.
1.2 Mutation
1.2.1 操作參數 Variables
這個和Query 那邊的規則完全一樣,參見上面的內容即可,給個小例子:
# 無參寫法
mutation create {
createStudent(name: "王五", age: 18) {
id
}
}
# 有參寫法
mutation create($name: String, $age: Int) {
createStudent(name: $name, age: $age) {
id
}
}
# 另一種有參寫法
# 假設createStudent 函數的參數的類型叫createStudentInput
mutation create($input: createStudentInput!) {
createStudent($input) {
id
}
}
1.2.2 行內片段 Inline Fragments
這裏的使用情景主要是針對聯合(Union) 類型的,類似於接口(interface) 與類(class)的關係。
假設我們有個接口叫動物(Animal), 有兩個類分別是狗(Dog) 和鳥(Bird). 並且我們將這兩個類由一個GraphQL 節點給出去:
{
animal {
name
kind
... on Dog {
breed
}
... on Bird {
wings
}
}
}
# 結果
{
data: {
animal: [
{
name: "Pepe",
kind: "Dog",
breed: "Husky"
},
{
name: "Pipi",
kind: "Bird",
wings: 2
}
]
}
}
從上面的結果可以看出,它可以由不同的類型去查不同的“類”,但返回時可以合併返回。就類似於是從一個“接口” 上直接獲取到實現類的數據了,非常具體。但大部分情況下我們可能不會合並着查兩個不同結構的數據以一個數組返回,我們更多可能是用在於用同一個節點名(animal)就可以查不同的東西但先以他們的類型作了過濾。
1.2.3 元字段 Meta fields
配合上面的例子食用,如果我們沒有kind 那個字段時,我們要怎麼知道哪個元素是哪個類型呢?我們可以用元字段去知道我們當前操作的是哪個數據實體,主要的元字段有 __typename.
我們可以這樣查:
{
animal {
name
__typename
... on Dog {
breed
}
... on Bird {
wings
}
}
}
# 結果
{
data: {
animal: [
{
name: "Pepe",
__typename: "Animal__Dog",
breed: "Husky"
},
{
name: "Pipi",
__typename: "Animal__Bird",
wings: 2
}
]
}
}
__typename 是內置的,你可以在任何節點上查,它都會給你一個類型。
2. 類型定義
2.1 基礎
我們知道GraphQL 是一個靜態類型的語法系統,那麼我們在真正使用前就必須先定義好它的類型。
GraphQL 的類型定義叫做Schemas. 有它自已獨立的語法。裏面有各個基本類型Scalar,可以定義成不同對象的Type. 也可以自已用基本類型定義成新的類型。
所有的不同的對象最終會組成一個樹狀的結構,根由schema 組成:
schema {
query: Query
mutation: Mutation
}
然後再定義裏面一層一層的子對象,比如我們上面那個模型大概可以寫成:
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
}
像上面這樣我們就定義了兩個不同的對象及他們的屬性。其中,如果是必填或者説非空的字段則帶有"!" 在它的類型後面,比如id: ID! 就表明id 是個非空的字段。非空的字段如果在操作中給它傳null 會報錯。另外某種類型組成的數組可以用類型加中括號組成,比如上面的Student 裏面的Teacher.
定義一個字段為數組:
myField: [String!]
這樣定義呢,表明了這個字段本身是可以為 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 默認的自帶類型只有5 種。分別是:
ID: 就類似傳統數據庫中的ID 字段,主要用於區別不同的對象。可以直接是一個Int, 也可能是一個編碼過的唯一值,比如常見的relay 中使用的是“類名:ID” 的字符串再經base64 轉碼後的結果作為ID. 要注意的是這個ID 只是存在於Graphql 中的。它不一定和數據庫中的是對應的。比如relay 這個情況,數據庫中存的可能還是一個整數並不是那個字符串。
Int: 整數,可正負。
Float: 雙精度浮點數,可正負。
String: UTF-8 字符的字符串。
Boolean: true / false.
如果不能滿足你的業務場景你就可以自定義新的類型,或者是找第三方做好的拓展類型。
定義一個類型的Graphql 寫法很簡單,比如我們新增一個Date 類型。
scalar Date
就這樣就可以了,但是你還需要在你的代碼中實現它的具體功能,怎麼轉換出入運行時等等。
另外,Graphql 中支持枚舉類型,可以這樣定義:
enum GenderTypes {
MALE
FEMALE
OTHERS
}
2.3 接口(Interface) 和聯合類型(Union)
Interface 和 Union 很像,所以我就合在一起講了。
Interface 和其他語言的類似,都是為了給一個通用的父類型定義用的。可以像這樣定義及使用:
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
}
可以看到,接口定義的每個字段在實現時都會帶上,但它也可以有自已的字段。查詢時,需要注意的是:你不可以直接在Animal 上查到各個獨有的字段,因為當你在Animal 上做查詢時系統並不知道你當前查詢的對象是Dog 還是Cat. 你需要用inline fragment 去指定。
# 這樣查直接報錯:
# "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, 我們再看看Union.
Union 你可以直接理解成是沒有共同字段的Interface.
union Plant = Lily | Rose | Daisy
查詢時和接口一樣得用inline fragments 去指定類型。
2.4 輸入類型 Input types
上面在那個Mutation 的Variables 舉例子時稍微提到過,就是給某個操作的輸入的所有參數指定成一個類型,這樣可以更方便地添加內容也增加了代碼可複用的程度。
假設我們有一個Mutation 的定義是這樣的:
type Mutaion {
createSomething(foo: Int, bar: Float): Something
}
使用Input types:
input CreateSomethingInput {
foo: Int
bar: Float
}
type Mutaion {
createSomething(input: CreateSomethingInput): Something
}