33

Go GraphQL 教程

 4 years ago
source link: https://www.tuicool.com/articles/QBjiq2z
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一般的 Web 开发都是使用 RESTful 风格进行API的开发,这种 RESTful 风格的 API 开发的一般流程是:

  • 需求分析
  • 模型设计
  • 编码实现
    • 路由设计:
    • 参数操作:校验、请求
    • 响应:JSON 格式、状态码

一种资源一般都可以抽象出 4 类路由,比如投票接口:

# 获取所有投票信息
GET /v1/api/votes

# 获取单个投票信息
GET /v1/api/vote/{vote_id}

# 创建投票
POST /v1/api/vote

# 更新投票
PATCH  /v1/api/vote/{vote_id}

# 删除投票
DELETE /v1/api/vote/{vote_id}

复制代码

分别对应资源的获取、创建、更新、删除。

对于后端开发人员而言,重要的是在满足需求的前提下设计这类 API。

设计这类 API 一般需要处理这些具体的问题:

  • 根据需求进行模型设计:即 model 层,模型设计核心对应数据库表,所以又需要根据需求,设计字段、字段类型、表的多对多等关系
  • 抽象出资源实体,进行资源的增删改查操作
  • 返回JSON 格式的响应、状态码、或者错误信息

前端或者客户端,根据具体的需求,调用接口,对接口返回的字段进行处理。尽管有时候需求并不需要所有字段,又或者有时候需求需要 调用多个接口,组装成一个大的格式,以完成需求。

后端抽象出多少实体,对应就会设计各种资源实体的接口。后续需求变更,为了兼容,需要维护越来越多的接口。

看到没,这类的接口设计:

  • 需要维护多类接口,需求不断变更,维护的接口越来越多
  • 字段的获取,前端或者客户端不能决定,而是一股脑的返回,再由相应开发人员处理
  • 需要考虑接口版本 ...

GraphQL API

GraphQL 是一种专门用于API 的查询语言,由大厂 Facebook 推出,但是至今 GraphQL 并没有引起广泛的使用, 绝大多少还是采用 RESTful API 风格的形式开发。

GraphQL 尝试解决这些问题:

  • 查询语法和查询结果高度相似
  • 根据需求获取字段
  • 一个路由能获取多个请求的结果
  • 无需接口版本管理

1

既然是一种专门用于 API 的查询语言,其必定有一些规范或者语法约束。具体 GraphQL 包含哪些知识呢?

  • Schema 是类型语言的合集,定义了具体的操作(比如:请求、更改),和对象信息(比如:响应的字段)

schema.graphql

type Query {
    ping(data: String): Pong
}

type Mutation {
    createVote(name: String!): Vote
}

type Pong{
    data: String
    code: Int
}

type Vote {
    id: ID!
    name: String!
}
复制代码

具体定义了请求合集:Query, 更改或者创建合集:Mutation,定义了两个对象类型:Pong, Vote , 对象内包含字段和类型。

这个schema 文件,是后端开发人员的开发文档,也是前端或者客户端人员的 API 文档。

假设,后端开发人员依据 schema 文件,已经开发完毕,那么如何调用 API 呢?

推荐使用:PostMan

# ping 请求动作
query {
    ping{
        data
        code
    }
}
复制代码
# mutation 更改动作
mutation {
    createVote(name:"have a lunch") {
        id
        name 
    }
}
复制代码

能发现一些规律么?

  • schema 文件几乎决定了请求的具体形式,请求什么格式,响应什么格式
  • API 请求动作包括:操作类型(query, mutation, subscription)、操作名称、请求名称、请求字段
query HeartBeat {
    ping{
        data
        code
    }
}
复制代码
  • 操作类型: query
  • 操作名称: HeartBeat (操作名称一般省略)
  • 请求名称: ping
  • 响应字段:Pong 对象的字段 data、code

GraphQL 是一种专门用于 API 的查询语言,有语法约束。

具体包括:

!
|

讲了这么些,其实最好的方式还是亲自调用下接口,参照着官方文档,按个调用尝试下,熟悉这套语法规范。

最佳的当然是:Github 的 GraphQL API4 ( developer.github.com/v4/ )

  • 熟络 GraphQL 语法规范
  • 学习 GraphQL 设计规范

登入自己的账号:访问: developer.github.com/v4/explorer…

仅举几个示例:

0. viewer: User!

  • 请求名称:viewer
  • 响应对象:User 非空,即一定会返回一个 User 对象,User 对象由一系列字段、对象组成

1. 基本请求动作

{
  viewer {
    __typename
    ... on User {
      name
    }
  }
}

// 结果

{
  "data": {
    "viewer": {
      "__typename": "User",
      "name": "XieWei"
    }
  }
}

复制代码

2. 别名

{
  AliasForViewer:viewer {
    __typename
    ... on User {
      name
    }
  }
}


# 结果
{
  "data": {
    "AliasForViewer": {
      "__typename": "User",
      "name": "XieWei"
    }
  }
}

复制代码

3.操作名称,变量,指令

query PrintViewer($Repository: String!,$Has: Boolean!){
  AliasForViewer:viewer{
    __typename
    ... on User {
      name
    }
    url
    status{
      createdAt
      emoji
      id
    }
    repository(name: $Repository) {
      name
      createdAt
      description @include(if:$Has)
      
    }
  
  }
}

# 变量
{
  "Repository": "2019-daily",
  "Has": false
}

# 结果

{
  "data": {
    "AliasForViewer": {
      "__typename": "User",
      "name": "XieWei",
      "url": "https://github.com/wuxiaoxiaoshen",
      "status": null,
      "repository": {
        "name": "2019-daily",
        "createdAt": "2019-01-11T15:17:43Z"
      }
    }
  }
}

# 如果变量为:

{
  "Repository": "2019-daily",
  "Has": true
}

# 则结果为

{
  "data": {
    "AliasForViewer": {
      "__typename": "User",
      "name": "XieWei",
      "url": "https://github.com/wuxiaoxiaoshen",
      "status": null,
      "repository": {
        "name": "2019-daily",
        "createdAt": "2019-01-11T15:17:43Z",
        "description": "把2019年的生活过成一本书"
      }
    }
  }
}

复制代码

对照着文档多尝试。

上文多是讲述使用 GraphQL 进行查询操作时的语法。

2

schema 是所有请求、响应、对象声明的集合,对后端而言,是开发依据,对前端而言,是 API 文档。

如何定义 schema ?

你只需要知道这些内容即可:

!
type
enum
input

举一个具体的示例:小程序: 腾讯投票

首页

VbQbYbU.jpg!web

详情

ERfmErm.jpg!web

Step1: 定义类型对象的字段

定义的类型对象和响应的字段设计几乎保持一致。

# 类似于 map, 左边表示字段名称,右边表示类型
# [] 表示列表
# ! 修饰符表示非空
type Vote {
    id: ID!
    createdAt: Time
    updatedAt: Time
    deletedAt: Time
    title: String
    description: String
    options: [Options!]!
    deadline: Time
    class: VoteClass
}

type Options {
    name: String
}

# 输入类型: 一般用户更改资源中的输入是列表对象,完成复杂任务

input optionsInput {
    name:String!
}

# 枚举类型:投票区分:单选、多选两个选项值
enum VoteClass {
    SINGLE
    MULTIPLE
}

# 自定义类型,默认类型(ID、String、Boolean、Float)不包含 Time 类型
scalar Time

# 对象类型,用于检查服务是否完好
type Ping {
    data: String
    code: Int

}
复制代码

Step2: 定义操作类型:Query 用于查询,Mutation 用于创建、更改、删除资源

# Query、Mutation 关键字固定
# 左边表示操作名称,右边表示返回的值的类型
# Query 一般完成查询操作
# Mutation 一般完成资源的创建、更改、删除操作

type Query {
    ping: Ping
    pinWithData(data: String): Ping
    vote(id:ID!): Vote
}

type Mutation {
    createVote(title:String!, options:[optionsInput],deadline:Time, description:String, class:VoteClass!): Vote
    updateVote(title:String!, description:String!): Vote
}

复制代码

schema 完成了对对象类型的定义和一些操作,是后端开发者的开发文档,是前端开发者的API文档。

3

客户端如何使用:Go : (graphql-go)

主题: 小程序腾讯投票

Step0: 项目结构

├── Makefile
├── README.md
├── cmd
│   ├── root_cmd.go
│   └── sync_cmd.go
├── main.go
├── model
│   └── vote.go
├── pkg
│   ├── database
│   │   └── database.go
│   └── router
│       └── router.go
├── schema.graphql
├── script
│   └── db.sh
└── web
    ├── mutation
    │   └── mutation_type.go
    ├── ping
    │   └── ping_query.go
    ├── query
    │   └── query_type.go
    └── vote
        ├── vote_curd.go
        ├── vote_params.go
        └── vote_type.go
复制代码
  • cmd: 命令行文件:主要用于同步数据库表结构
  • main.go 函数主入口
  • model 模型定义,每种资源单独一个文件 比如 vote.go
  • pkg 基础设施:数据库连接、路由设计
  • web 核心业务路径,总体上按资源划分文件夹
    • vote
      • vote_curd.go 资源的增删改查
      • vote_params.go 请求参数
      • vote_type.go schema 中资源,即类型对象的定义
    • query
      • query.go
    • mutation
      • mutation.go

和之前的 RESTful API 的设计项目的结构基本保持一致。

Step1: 依据Schema 的定义:完成数据库模型定义

type base struct {
	Id        int64      `xorm:"pk autoincr notnull" json:"id"`
	CreatedAt time.Time  `xorm:"created" json:"created_at"`
	UpdatedAt time.Time  `xorm:"updated" json:"updated_at"`
	DeletedAt *time.Time `xorm:"deleted" json:"deleted_at"`
}

const (
	SINGLE = iota
	MULTIPLE
)

var ClassMap = map[int]string{}

func init() {
	ClassMap = make(map[int]string)
	ClassMap[SINGLE] = "SINGLE"
	ClassMap[MULTIPLE] = "MULTIPLE"
}

type Vote struct {
	base        `xorm:"extends"`
	Title       string    `json:"title"`
	Description string    `json:"description"`
	OptionIds   []int64   `json:"option_ids"`
	Deadline    time.Time `json:"deadline"`
	Class       int       `json:"class"`
}

type VoteSerializer struct {
	Id          int64              `json:"id"`
	CreatedAt   time.Time          `json:"created_at"`
	UpdatedAt   time.Time          `json:"updated_at"`
	Title       string             `json:"title"`
	Description string             `json:"description"`
	Options     []OptionSerializer `json:"options"`
	Deadline    time.Time          `json:"deadline"`
	Class       int                `json:"class"`
	ClassString string             `json:"class_string"`
}

func (V Vote) TableName() string {
	return "votes"
}

func (V Vote) Serializer() VoteSerializer {
	var optionSerializer []OptionSerializer
	var options []Option
	database.Engine.In("id", V.OptionIds).Find(&options)
	for _, i := range options {
		optionSerializer = append(optionSerializer, i.Serializer())
	}
	classString := func(value int) string {
		if V.Class == SINGLE {
			return "单选"
		}
		if V.Class == MULTIPLE {
			return "多选"
		}
		return ""
	}
	return VoteSerializer{
		Id:          V.Id,
		CreatedAt:   V.CreatedAt.Truncate(time.Second),
		UpdatedAt:   V.UpdatedAt.Truncate(time.Second),
		Title:       V.Title,
		Description: V.Description,
		Options:     optionSerializer,
		Deadline:    V.Deadline,
		Class:       V.Class,
		ClassString: classString(V.Class),
	}
}

type Option struct {
	base `xorm:"extends"`
	Name string `json:"name"`
}

type OptionSerializer struct {
	Id        int64     `json:"id"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
	Name      string    `json:"name"`
}

func (O Option) TableName() string {
	return "options"
}

func (O Option) Serializer() OptionSerializer {
	return OptionSerializer{
		Id:        O.Id,
		CreatedAt: O.CreatedAt.Truncate(time.Second),
		UpdatedAt: O.UpdatedAt.Truncate(time.Second),
		Name:      O.Name,
	}
}
复制代码

依然保持了个人的模型设计风格:

  • 定义一个结构体,对应数据库表
  • 定义个序列化结构体,对应模型的响应
  • 单选、多选项,实质在数据库中用0,1 表示,响应显示中文:单选、多选

Step2: query.go 文件描述

var Query = graphql.NewObject(graphql.ObjectConfig{
	Name: "Query",
	Fields: graphql.Fields{
		"ping": &graphql.Field{
			Type: ping.Ping,
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				return ping.Default, nil
			},
		},
	},
})

func init() {
	Query.AddFieldConfig("pingWithData", &graphql.Field{
		Type: ping.Ping,
		Args: graphql.FieldConfigArgument{
			"data": &graphql.ArgumentConfig{
				Type: graphql.NewNonNull(graphql.String),
			},
		},
		Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
			if p.Args["data"] == nil {
				return ping.Default, nil
			}
			return ping.MakeResponseForPing(p.Args["data"].(string)), nil
		},
	})
}

func init() {
	Query.AddFieldConfig("vote", &graphql.Field{
		Type: vote.Vote,
		Args: graphql.FieldConfigArgument{
			"id": &graphql.ArgumentConfig{
				Type: graphql.NewNonNull(graphql.ID),
			},
		},
		Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
			id := p.Args["id"]
			ID, _ := strconv.Atoi(id.(string))
			return vote.GetOneVote(int64(ID))
		},
	})
}
复制代码

基本和 schema 文件中 Query 定义一致:

type Query {
    ping: Ping
    pinWithData(data: String): Ping
    vote(id:ID!): Vote
}
复制代码
  • Fields 表示对象字段
  • Type 表示返回类型
  • Args 表示参数
  • Resolve 表示具体的处理函数

内置类型:(ID, String, Boolean, Float)

- graphql.ID
- graphql.String
- graphql.Boolean
- graphql.Float
...
复制代码

简单的说:所有的对象、字段都需要有处理函数。

var Query = graphql.NewObject(graphql.ObjectConfig{
	Name: "Query",
	Fields: graphql.Fields{
		"ping": &graphql.Field{
			Type: ping.Ping,
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				return ping.Default, nil
			},
		},
	},
})

func init() {
	Query.AddFieldConfig("pingWithData", &graphql.Field{
		Type: ping.Ping,
		Args: graphql.FieldConfigArgument{
			"data": &graphql.ArgumentConfig{
				Type: graphql.NewNonNull(graphql.String),
			},
		},
		Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
			if p.Args["data"] == nil {
				return ping.Default, nil
			}
			return ping.MakeResponseForPing(p.Args["data"].(string)), nil
		},
	})
}

var Ping = graphql.NewObject(graphql.ObjectConfig{
	Name: "ping",
	Fields: graphql.Fields{
		"data": &graphql.Field{
			Type: graphql.String,
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				if response, ok := p.Source.(ResponseForPing); ok {
					return response.Data, nil
				}
				return nil, fmt.Errorf("field not found")
			},
		},
		"code": &graphql.Field{
			Type: graphql.String,
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				if response, ok := p.Source.(ResponseForPing); ok {
					return response.Code, nil
				}
				return nil, fmt.Errorf("field not found")
			},
		},
	},
})

type ResponseForPing struct {
	Data string `json:"data"`
	Code int    `json:"code"`
}

var Default = ResponseForPing{
	Data: "pong",
	Code: http.StatusOK,
}

func MakeResponseForPing(data string) ResponseForPing {
	return ResponseForPing{
		Data: data,
		Code: http.StatusOK,
	}
}
复制代码

使用 Go Graphql-go 客户端,绝大多数工作都在定义对象、定义字段类型、定义字段的处理函数等。

  • graphql.Object
  • graphql.InputObject
  • graphql.Enum

Step3: mutation.go 文件描述

var Mutation = graphql.NewObject(graphql.ObjectConfig{
	Name: "Mutation",
	Fields: graphql.Fields{
		"createVote": &graphql.Field{
			Type: vote.Vote,
			Args: graphql.FieldConfigArgument{
				"title": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.String),
				},
				"options": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.NewList(vote.OptionInput)),
				},
				"description": &graphql.ArgumentConfig{
					Type: graphql.String,
				},
				"deadline": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.String),
				},
				"class": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(vote.Class),
				},
			},
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				log.Println(p.Args)
				var params vote.CreateVoteParams
				params.Title = p.Args["title"].(string)
				if p.Args["description"] != nil {
					params.Description = p.Args["description"].(string)
				}
				params.Deadline = p.Args["deadline"].(string)
				params.Class = p.Args["class"].(int)
				var options []vote.OptionParams
				for _, i := range p.Args["options"].([]interface{}) {
					var one vote.OptionParams
					k := i.(map[string]interface{})
					one.Name = k["name"].(string)
					options = append(options, one)
				}
				params.Options = options
				log.Println(params)
				result, err := vote.CreateVote(params)
				if err != nil {
					return nil, err
				}
				return result, nil

			},
		},
		"updateVote": &graphql.Field{
			Type: vote.Vote,
			Args: graphql.FieldConfigArgument{
				"title": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.String),
				},
				"description": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.String),
				},
				"id": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.ID),
				},
			},
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				var params vote.UpdateVoteParams
				id := p.Args["id"]
				ID, _ := strconv.Atoi(id.(string))
				params.Id = int64(ID)
				params.Title = p.Args["title"].(string)
				params.Description = p.Args["description"].(string)
				return vote.UpdateOneVote(params)
			},
		},
	},
})

复制代码

Step4: 构建 schema 启动服务

func RegisterSchema() *graphql.Schema {
	schema, err := graphql.NewSchema(
		graphql.SchemaConfig{
			Query:    query.Query,
			Mutation: mutation.Mutation,
		})
	if err != nil {
		panic(fmt.Sprintf("schema init fail %s", err.Error()))
	}
	return &schema

}

func Register() *handler.Handler {
	return handler.New(&handler.Config{
		Schema:   RegisterSchema(),
		Pretty:   true,
		GraphiQL: true,
	})
}
func StartWebServer() {
	log.Println("Start Web Server...")
	http.Handle("/graphql", Register())
	log.Fatal(http.ListenAndServe(":7878", nil))
}
复制代码

Step5: 运行,接口调用

/graphql
POST

接口调用示例:(根据查询文档,可以根据调用者的需求,自主选择响应的字段)

mutation {
    createVote(
        title: "去哪玩?",
        description:"本次团建去哪玩?",
        options:[
            {
                name: "杭州西湖"
            },{
                name:"安徽黄山"
            },{
                name:"香港九龙"
            }
            ],
        deadline: "2019-08-01 00:00:00",
        class: SINGLE
        ) {
            id
            title
            deadline
            description
            createdAt
            updatedAt
            options{
                name
            }
            class
            classString
        }
}

# 结果

{
	"data": {
		"vote": {
			"class": "SINGLE",
			"classString": "单选",
			"createdAt": "2019-07-30T19:33:27+08:00",
			"deadline": "2019-08-01T00:00:00+08:00",
			"description": "本次团建去哪玩?",
			"id": "1",
			"options": [
				{
					"name": "杭州西湖"
				},
				{
					"name": "安徽黄山"
				},
				{
					"name": "香港九龙"
				}
			],
			"title": "去哪玩?",
			"updatedAt": "2019-07-30T19:33:27+08:00"
		}
	}
}

复制代码
query{
    vote(id:1){
            id
            title
            deadline
            description
            createdAt
            updatedAt
            options{
                name
            }
            class
            classString
    }
}

# 结果

{
	"data": {
		"createVote": {
			"class": "SINGLE",
			"classString": "SINGLE",
			"createdAt": "2019-07-30T19:33:27+08:00",
			"deadline": "2019-08-01T00:00:00+08:00",
			"description": "本次团建去哪玩?",
			"id": "1",
			"options": {
				{
					"name": "杭州西湖"
				},
				{
					"name": "安徽黄山"
				},
				{
					"name": "香港九龙"
				}
			},
			"title": "去哪玩?",
			"updatedAt": "2019-07-30T19:33:27+08:00"
		}
	}

}

复制代码

4

建议:

  • 优先设计:Schema, 指导着开发者
  • 如果请求或者更改动作过多,按功能或者资源划分(项目结构按功能划分,一定程度上有助于减轻思维负担)
var Query = graphql.NewObject(graphql.ObjectConfig{}

func init(){
    // 资源一
    Query.AddFieldConfig("filedsName", &graphql.Field{})
}

func init(){
    // 资源二
}
复制代码
  • 如何处理复杂请求参数:
var Mutation = graphql.NewObject(graphql.ObjectConfig{
	Name: "Mutation",
	Fields: graphql.Fields{
		"createVote": &graphql.Field{
			Type: vote.Vote,
			Args: graphql.FieldConfigArgument{
				"title": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.String),
				},
				"options": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.NewList(vote.OptionInput)),
				},
				"description": &graphql.ArgumentConfig{
					Type: graphql.String,
				},
				"deadline": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(graphql.String),
				},
				"class": &graphql.ArgumentConfig{
					Type: graphql.NewNonNull(vote.Class),
				},
			},
			Resolve: func(p graphql.ResolveParams) (i interface{}, e error) {
				log.Println(p.Args)
				var params vote.CreateVoteParams
				params.Title = p.Args["title"].(string)
				if p.Args["description"] != nil {
					params.Description = p.Args["description"].(string)
				}
				params.Deadline = p.Args["deadline"].(string)
				params.Class = p.Args["class"].(int)
				var options []vote.OptionParams
				for _, i := range p.Args["options"].([]interface{}) {
					var one vote.OptionParams
					k := i.(map[string]interface{})
					one.Name = k["name"].(string)
					options = append(options, one)
				}
				params.Options = options
				log.Println(params)
				result, err := vote.CreateVote(params)
				if err != nil {
					return nil, err
				}
				return result, nil

			},
		},
	},
})
复制代码

Args 定义所有该请求的字段和类型。 p.Args 类型(map[string]interface),可以获取到请求参数。返回是个 interface, 根据 Args 内定义的类型,类型转化


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK