深刻 GraphQL 的应用语法
对于 GraphQL 的应用语法在上一节中曾经大略介绍了根本的应用形式了,这一篇将会对上一篇入门做拓展,致力将所有的应用语法都笼罩到。
1. 终端语法
首先是介绍在前端查问时用的语法,分成 Query 和 Mutation 两局部,Subscription 的和 Query 是相似的就不特地阐明了。
1.1 Query
假如咱们当初有一个数据集,构造是这样的:
- 学生和老师各有各自的特有字段;
- 学生中有个获取所有相干老师的办法,老师中也有一个获取所有学生的办法;
- 学生和老师互为多对多关系;
classDiagram
Student <|-- Teacher2Student
Teacher <|-- Teacher2Student
class Student {
+Int id
+String name
+Int age
+teachers(id_eq: number) get_all_teachers
}
class Teacher {
+Int id
+String name
+Boolean gender
+students(name_contains: string) get_all_students
}
class Teacher2Student {
+Int id
+Int student_id
+Int teacher_id
+student() get_student
+teacher() get_teacher}
首先最简略的应用形式咱们可查:
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
}