1. 程式人生 > 實用技巧 >Graphql基礎知識整理

Graphql基礎知識整理

一、痛點

RESTful API是目前常見的介面設計方式,客戶端呼叫介面來進行前後端的互動, 但是呼叫RESTful API會有下面一些常見的問題:

  1. 呼叫多個API載入資源

  1. 後端介面返回大量無用資料

這些問題會對效能造成一定的影響, 因為http是基於tcp/ip協議的,每個hppt請求建立連線需要一定的開銷,另外如果介面中涉及資料庫的操作,資料庫開啟關閉連線也會有一部分的開銷,所以通過一次介面呼叫獲取資料比呼叫多個介面獲取資料在效能上更優。另外,如果介面返回大量的無用欄位,在資料傳輸上會造成浪費。

GraphQL能夠解決上述兩種問題,下面通過一個例子直觀的感受下兩者的區別。

假如要開發一個新增/修改使用者資訊的頁面,包含姓名、年齡、性別、所屬省份,所屬省份是下拉框。

  • RESTful API

服務端提供三個介面:

  1. 根據id查詢患者資訊
  2. 查詢所有省份
  3. 患者儲存

前端:呼叫介面查詢患者資訊,呼叫介面查詢所有省份。

  • 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
}
  • 欄位型別可以是:
  1. 標量型別:Int、Float、String、Boolean、ID。標量型別表明該欄位必定能解析到具體的資料,表示對應 GraphQL 查詢的葉子節點。

可以自定義標量型別

scalar Date
  1. 列舉型別:是一種特殊的標量
enum status {
  Enable
  Disable
}
  1. 物件型別

例如職場型別的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服務

未完..