一、痛点
RESTful API是目前常见的接口设计方式,客户端调用接口来进行前后端的交互, 但是调用RESTful API会有下面一些常见的问题:
- 调用多个API加载资源
- 后端接口返回大量无用数据
这些问题会对性能造成一定的影响, 因为http是基于tcp/ip协议的,每个hppt请求建立连接需要一定的开销,另外如果接口中涉及数据库的操作,数据库打开关闭连接也会有一部分的开销,所以通过一次接口调用获取数据比调用多个接口获取数据在性能上更优。另外,如果接口返回大量的无用字段,在数据传输上会造成浪费。
GraphQL能够解决上述两种问题,下面通过一个例子直观的感受下两者的区别。
假如要开发一个新增/修改用户信息的页面,包含姓名、年龄、性别、所属省份,所属省份是下拉框。
- RESTful API
服务端提供三个接口:
- 根据id查询患者信息
- 查询所有省份
- 患者保存
前端:调用接口查询患者信息,调用接口查询所有省份。
- GraphQL
后端定义schema
type Query {
getUser(id: String): User,
getProvince() : [Province];
}
type User {
id: ID,
name: String,
age: Int,
gender: String,
phone: String,
address: String
}
type Province {
id: ID,
name: String
}
前端构建下面查询,通过一次查询得到想要的结果。
query {
getUser('1') {
id,
name,
age
},
getProvince {
id,
name
},
}
返回结果:
{
data: {
getUser: {
id:'1',
name:'张三',
age:22,
gender:'女'
},
getProvince: [{
id:1
name: '北京'
}, {
id: 2
name: '上海'
}]
}
}
下面详细的介绍下GraphQL的基础语法
二、Graphql介绍
GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。
一个 GraphQL 服务是通过定义类型和类型上的字段来创建的,然后给每个类型上的每个字段提供解析函数。
2.1 对象类型
2.1.1 GraphQL如何定义一个对象类型?
type typeName {
/**字段名称 :字段类型*/
fieldName : String
}
- 字段类型可以是:
- 标量类型:Int、Float、String、Boolean、ID。标量类型表明该字段必定能解析到具体的数据,表示对应 GraphQL 查询的叶子节点。
可以自定义标量类型
scalar Date
- 枚举类型:是一种特殊的标量
enum status {
Enable
Disable
}
- 对象类型
例如职场类型的meetingRooms字段是MeetingRoom数组类型,
type Workplace {
id: ID,
name: String!,
city: String!,
state: status,
meetingRooms: [MeetingRoom]
}
type MeetingRoom {
name: String,
desc: String!
logo: String!
}
- 类型名后面添加感叹号!表示字段不能为空, 中括号[]表示一个数组
2.1.2 两个特殊的类型:Query、Mutation。
每个GraphQL服务都有一个 query 类型,可能有一个 mutation 类型。通常情况下Query对象类型定义了GraphQL服务所支持的查询操作,Mutation对象类型定义了服务所支持的修改操作。
schema {
query: Query
mutation: Mutation
}
假设我们要做一个职场管理的系统,可以新增,修改职场,可以查询所有职场,可以根据id查询单个职场的详情,那么系统的Query类型和Mutation可以这样定义:
Query 类型
Query类型定义了两个字段,字段GetWorkplaceList的类型是[Workplace]即返回所有职场, 字段GetWorkplaceDetail的返回类型是Workplace即返回单个职场信息。
type Query {
GetWorkplaceList: [Workplace],
GetWorkplaceDetail(id: String): Workplace
}
Mutation 类型
Mutation类型定义了一个字段upsertWorkplace,字段的类型是Workplace,
type Mutation {
upsertWorkplace(id: String, name: String, city: String): Workplace
}
2.1.3 字段参数
上面在定义Query类型和Mutation类型的时候已经使用了参数,GetWorkplaceDetail字段有个参数id,它是String类型,Mutation对象类型的upsertWorkplace字段有3个参数id, name, city。
语法:字段名(参数名:参数类型),参数可以设置默认值 (参数名:参数类型 = 默认值)
假如查询职场列表可以根据名称进行筛选, 那么字段GetWorkplaceList可以这样改造
type Query {
GetWorkplaceList(condition : String): [Workplace],
GetWorkplaceDetail(id: String): Workplace
}
2.1.4 接口
接口相当于对象类型的抽象,接口中包含一些字段,对象类型要实现这个接口,就必须也包含这些字段。
还以职场为例,公司的诊所也属于一种职场,他是医生工作的地方,他与普通职场的区别是除了有会议室还有诊室。
interface Workplace {
name: String!,
city: String!,
state: status,
meetingRooms: [MeetingRoom]
}
type ClinicWorkplace implements Workplace{
clinicRooms: [String]
}
当你要返回一个对象或者一组对象,特别是一组不同的类型时,接口就显得特别有用。
2.1.5 联合类型
联合类型和接口十分相似,但是它并不指定类型之间的任何共同字段。如果想返回不止一种对象类型,可以选则使用联合类型
type Query {
GetWorkplaceDetail(id: String): Workplace | ClinicWorkplace
}
联合类型的成员需要是具体对象类型;你不能使用接口或者其他联合类型来创造一个联合类型。
如果你需要查询一个返回类型是 联合类型的字段,那么你得使用内连片段才能查询任意字段。内连片段... on ClinicWorkplace意思就是,如果查询结果是ClinicWorkplace返回clinicRooms字段。
客户端请求
{
GetWorkplaceDetail(id: "1") {
name
... on ClinicWorkplace {
clinicRooms
}
}
}
2.1.6 输入类型
如果要给字段传递复杂的对象,可以定义输入类型。例如我们要upser一个职场时,可以传递一个form信息。
type Mutation {
upsertWorkplace(form: inputForm): Workplace
}
input inputForm {
id: String,
name: String!,
city: String!,
}
2.2 客户端查询、变更
继续以职场管理为例,现在服务端定义的schema如下:
type Query {
WorkplaceList(condition : queryCondition): [Workplace],
WorkplaceDetail(id: String): Workplace
}
type Mutation {
upsertWorkplace(from: workplaceForm): Workplace
}
type Workplace {
id: ID!,
name: String!,
city: String!,
address: String,
logo: String,
state: Int,
}
input queryCondition {
city: String,
name: String,
}
input workplaceForm {
id: ID!,
name: String!,
city: String!,
address: String,
}
2.2.1 查询
客户端要查询id为1的职场名称、所在城市,
请求:
query {
WorkplaceDetail("1") {
name,
city
}
}
返回结果:
{
data: {
WorkplaceDetail: {
name: '北京总部',
city:'北京市'
}
}
}
2.2.2 别名
假如要查询id为1和2的职场名称和所在城市, 如果按照下面的写法,返回结果有有两个WorkplaceDetail,会有冲突,这个时候可以使用别名。
query {
WorkplaceDetail("1") {
name,
city
},
WorkplaceDetail("2") {
name,
city
}
}
使用别名查询,id为1的别名为bj, id为2的别名为sh, 请求代码如下:
query {
bj:WorkplaceDetail("1") {
name,
city
},
sh: WorkplaceDetail("2") {
name,
city
}
}
此时返回结果是:
{
data: {
bj: {
name: '北京总部',
city:'北京市'
},
sh: {
{
name: '上海总部',
city:'上海市'
},
}
}
}
2.2.3片段
片段使你能够组织一组字段,然后在需要它们的的地方引入(可以理解为一段代码的复用)。刚才的例子,查询id为1和2的职场的name和city,每个返回结果都要写一遍,有些重复,使用片段的话可以这样:
fragment comparisonFields on Workplace {
name,
city
}
query {
bj: WorkplaceDetail("1") {
...comparisonFields
},
sh: WorkplaceDetail("2") {
...comparisonFields
}
}
返回结果
{
data: {
bj: {
name: '北京总部',
city:'北京市'
},
sh: {
{
name: '上海总部',
city:'上海市'
},
}
}
}
操作名称
操作类型可以是 query、mutation 或 subscription,描述你打算做什么类型的操作,当操作类型是query时,可以不写;
操作名称是你的操作的有意义和明确的名称。它仅在有多个操作的文档中是必需的,但我们鼓励使用它,因为它对于调试和服务器端日志记录非常有用。
query GetWorkplaceDetail {
WorkplaceDetail("1") {
name,
city
}
}
变量
指令
- @include(if: Boolean) 仅在参数为 true 时,包含此字段。
- @skip(if: Boolean) 如果参数为 true,跳过此字段。
原字段
某些情况下,你并不知道你将从 GraphQL 服务获得什么类型,这时候你就需要一些方法在客户端来决定如何处理这些数据。GraphQL 允许你在查询的任何位置请求 __typename,一个元字段,以获得那个位置的对象类型名称。
GraphQL 库可以让你省略这些简单的解析器,假定一个字段没有提供解析器时,那么应该从上层返回对象中读取和返回和这个字段同名的属性。
2.3 解析器
以下面查询为例
query {
WorkplaceDetail("1") {
name,
city
}
}
返回结果:
{
data: {
WorkplaceDetail: {
name: '北京总部',
city:'北京市'
}
}
}
WorkplaceDetail字段调用WorkplaceDetail的解析器
解析器有四个参数:
- obj :上一级解析器返回的对象
- args:在 GraphQL 查询中传入的参数
- context:请求的上下文,
- info:保存与当前查询相关的字段特定信息以及 schema 详细信息的值
WorkplaceDetail(obj, args, context, info){
return ctx.service.workplace.getWorkplace(args.id);
}
调用workplace服务的getWorkplace方法,返回一个职场对象
{
id: '1',
name: '北京职场',
city: '北京',
logo: '',
description: '',
created: '2020-6-1',
updated: '2020-8-1',
...
}
WorkplaceDetail解析完,GraphQL 继续递归执行下解析name,city。
name解析器:
name(obj, args, context, info) {
return obj.name;
}
通常name,city解析器不用提供,GraphQL库发现一个字段没有提供解析器时,会从上层返回对象中读取和返回和这个字段同名的属性。
三、使用Egg框架搭建一个GraphQL服务
未完..