大家好,我叫谢伟,是一名程序员。
今天的主题:Go GraphQL 教程。
一般的 Web 开发都是使用 RESTful 风格进行API的开发,这种 RESTful 风格的 API 开发的一般流程是:
一种资源一般都可以抽象出 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 一般需要处理这些具体的问题:
前端或者客户端,根据具体的需求,调用接口,对接口返回的字段进行处理。尽管有时候需求并不需要所有字段,又或者有时候需求需要 调用多个接口,组装成一个大的格式,以完成需求。
后端抽象出多少实体,对应就会设计各种资源实体的接口。后续需求变更,为了兼容,需要维护越来越多的接口。
看到没,这类的接口设计:
GraphQL 是一种专门用于API 的查询语言,由大厂 Facebook 推出,但是至今 GraphQL 并没有引起广泛的使用, 绝大多少还是采用 RESTful API 风格的形式开发。
GraphQL 尝试解决这些问题:
既然是一种专门用于 API 的查询语言,其必定有一些规范或者语法约束。具体 GraphQL 包含哪些知识呢?
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
}
}
能发现一些规律么?
query HeartBeat {
ping{
data
code
}
}
GraphQL 是一种专门用于 API 的查询语言,有语法约束。
具体包括:
!
表示非空|
通过对象类型组合而成讲了这么些,其实最好的方式还是亲自调用下接口,参照着官方文档,按个调用尝试下,熟悉这套语法规范。
最佳的当然是:Github 的 GraphQL API4 (https://developer.github.com/v4/)
登入自己的账号:访问:https://developer.github.com/v4/explorer/
仅举几个示例:
0. viewer: 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 进行查询操作时的语法。
schema 是所有请求、响应、对象声明的集合,对后端而言,是开发依据,对前端而言,是 API 文档。
如何定义 schema ?
你只需要知道这些内容即可:
!
表示非空type
关键字enum
关键字input
关键字举一个具体的示例:小程序: 腾讯投票
首页
image
详情
image
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文档。
客户端如何使用: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
和之前的 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,
}
}
依然保持了个人的模型设计风格:
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
}
内置类型:(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 客户端,绝大多数工作都在定义对象、定义字段类型、定义字段的处理函数等。
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
(query 动作当然也可以使用 GET,遇到请求参数较多时,不够方便)接口调用示例:(根据查询文档,可以根据调用者的需求,自主选择响应的字段)
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"
}
}
}
建议:
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 内定义的类型,类型转化
总结:本文简单讲解 GraphQL的语法和 Go 编程实现 GraphQL 操作。
建议如何学习?
<完>