一、痛點
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服務
未完..