项目地址:https://github.com/unrotten/hello-world-web
开始之前,关于GraphQL的介绍
二话不说,先贴官网:https://graphql.org
如图所见,GraphQL可以分为三个部分:一是对数据的描述,称为类型系统,通过定义不同的type,来描述数据之间的关系。这其实很容易理解,可以直接类比为我们再Go中定义的struct,不同的是,在GraphQL中,每一个字段都是一个type。二是请求;在之前,我们定义了三个type,query,matutation和subscription,其实就是请求的方式。这里可以发现,其实我们所请求的东西,就是type,即我们对数据的描述。那么请求的内容也显而易见,应该是我们定义好的type。三是返回的结果;GraphQL是对数据的描述,从定义,到请求,再到结果,都不外乎是GraphQL中的type。
通过以上的三部分,不难发现,GraphQL的整一个流程,无非就是开发者定义好一系列的type,使用者拿到这些type,然后根据自己所需,去请求所要的type,最终获得返回。那么这里就体现了GraphQL的宗旨,即由调用方去决定请求什么数据,而提供方只需按照GraphQL的方式,将他所拥有的数据,描述出来即可。
GraphQL库使用入门
我们首先来看作者的示例程序:
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/graphql-go/graphql"
)
func main() {
// Schema
fields := graphql.Fields{
"hello": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return "world", nil
},
},
}
rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)}
schema, err := graphql.NewSchema(schemaConfig)
if err != nil {
log.Fatalf("failed to create new schema, error: %v", err)
}
// Query
query := `
{
hello
}
`
params := graphql.Params{Schema: schema, RequestString: query}
r := graphql.Do(params)
if len(r.Errors) > 0 {
log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := json.Marshal(r)
fmt.Printf("%s \n", rJSON) // {"data":{"hello":"world"}}
}
我们从底部看起,可以看到我们编写的请求hello通过graphql.Params构建了一个请求参数,并交由graphql.Do方法执行。而Params中,定义了一个Schema。再往上看,可以知道Schema由graphql.NewSchema(schemaConfig)声明,而schemaConfig的Query参数接收了一个Object指针。这个Object就是我们对数据的定义,它在rootQuery中被描述为,名称为RootQuery,字段为hello的类型。其中字段hello就是我们具体的数据,其类型为Field。对于每个字段,我们至少需要定义他的类型Type,在这里hello的类型是一个String。Resolve是对字段的解析函数,字段的值由Resolve函数返回。
定义用户数据
首先我们需要分析,关于用户,我们需要什么数据,以下列了几个最基础的:
- 用户注册:用户名,邮箱,密码
- 用户登录:用户名/邮箱,密码
- 获取当前登录用户信息:当前登录用户详细信息
- 获取指定用户信息: 指定用户详细信息
- 获取用户列表:用户列表
- 修改用户信息:用户详细信息
在controller
目录下新建user.go
文件:
package controller
import (
"github.com/graphql-go/graphql"
"github.com/graphql-go/relay"
)
var gender = graphql.NewEnum(graphql.EnumConfig{
Name: "Gender",
Values: graphql.EnumValueConfigMap{
"man": {Value: "man", Description: "男"},
"woman": {Value: "woman", Description: "女"},
"unknown": {Value: "unknown", Description: "保密"},
},
Description: "性别",
})
var userState = graphql.NewEnum(graphql.EnumConfig{
Name: "UserState",
Values: graphql.EnumValueConfigMap{
"unsign": {Value: "unsign", Description: "未认证"},
"normal": {Value: "normal", Description: "正常"},
"forbidden": {Value: "forbidden", Description: "禁止发言"},
"freeze": {Value: "freeze", Description: "冻结"},
},
Description: "用户状态",
})
var userType = graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Fields: graphql.Fields{
"id": relay.GlobalIDField("User", nil),
"username": {Type: graphql.String, Description: "用户名"},
"email": {Type: graphql.String, Description: "邮箱"},
"avatar": {Type: graphql.String, Description: "头像"},
"gender": {Type: gender, Description: "性别"},
"introduce": {Type: graphql.String, Description: "个人简介"},
"state": {Type: userState, Description: "状态"},
"root": {Type: graphql.Boolean, Description: "管理员"},
},
Description: "用户数据",
})
var userConnectionDefinition = relay.ConnectionDefinitions(relay.ConnectionConfig{
Name: "User",
NodeType: userType,
})
func registerUserType() {
queryType.AddFieldConfig("GetUserList", &graphql.Field{
Type: userConnectionDefinition.ConnectionType,
Args: relay.ConnectionArgs,
Resolve: nil,
Description: "用户列表",
})
queryType.AddFieldConfig("GetUser", &graphql.Field{
Type: userType,
Args: graphql.FieldConfigArgument{
"id": {Type: graphql.ID, Description: "ID"},
"username": {Type: graphql.String, Description: "用户名"},
},
Resolve: nil,
Description: "获取用户信息",
})
queryType.AddFieldConfig("CurrentUser", &graphql.Field{
Type: userType,
Resolve: nil,
Description: "获取当前登录用户信息",
})
mutationType.AddFieldConfig("CreatUser", &graphql.Field{
Type: userType,
Args: graphql.FieldConfigArgument{
"username": {Type: graphql.NewNonNull(graphql.String), Description: "用户名"},
"email": {Type: graphql.NewNonNull(graphql.String), Description: "邮箱"},
"password": {Type: graphql.NewNonNull(graphql.String), Description: "密码"},
},
Resolve: nil,
Description: "注册新用户",
})
mutationType.AddFieldConfig("SignIn", &graphql.Field{
Type: userType,
Args: graphql.FieldConfigArgument{
"username": {Type: graphql.NewNonNull(graphql.String), Description: "用户名"},
"password": {Type: graphql.NewNonNull(graphql.String), Description: "密码"},
},
Resolve: nil,
Description: "用户登录",
})
mutationType.AddFieldConfig("UpdateUser", &graphql.Field{
Type: userType,
Args: graphql.FieldConfigArgument{
"username": {Type: graphql.String, Description: "用户名"},
"email": {Type: graphql.String, Description: "邮箱"},
"avatar": {Type: graphql.String, Description: "头像"},
"gender": {Type: gender, Description: "性别"},
"introduce": {Type: graphql.String, Description: "个人简介"},
},
Resolve: nil,
Description: "修改用户信息",
})
}
首先我们需要知道,Field在GraphQL中的定义:
type Field struct {
Name string `json:"name"` // used by graphlql-relay
Type Output `json:"type"`
Args FieldConfigArgument `json:"args"`
Resolve FieldResolveFn `json:"-"`
DeprecationReason string `json:"deprecationReason"`
Description string `json:"description"`
}
Name是字段名,Type是所属类型,Args即获取本字段所需的参数,Resolve是解析函数。
我们首先定义了两个枚举类型,分别是gender和userState。
在userType中,我们使用了relay库,并将字段id定义为GlobalIDField,这是为了便于使用relay提供的分页功能。在registerUserType函数中,我们向queryType注册了两个字段GetUserList和GetUser,分别用于获取用户列表和获取单个用户的信息。其中参数relay.ConnectionArgs定义为:
var ConnectionArgs = graphql.FieldConfigArgument{
"before": &graphql.ArgumentConfig{
Type: graphql.String,
},
"after": &graphql.ArgumentConfig{
Type: graphql.String,
},
"first": &graphql.ArgumentConfig{
Type: graphql.Int,
},
"last": &graphql.ArgumentConfig{
Type: graphql.Int,
},
}
我们定义了一个mutationType的字段CreateUser,用于新用户的注册。
可以看到,所有定义的类型中,解析函数Resolve目前均为空,后面我们会将具体的处理逻辑加上去。
修改graphql.go
文件,调用registerUserType注册用户类型字段:
queryType = graphql.NewObject(graphql.ObjectConfig{Name: "Query", Fields: graphql.Fields{}})
mutationType = graphql.NewObject(graphql.ObjectConfig{Name: "Mutation", Fields: graphql.Fields{}})
subscriptType = graphql.NewObject(graphql.ObjectConfig{Name: "Subscription", Fields: graphql.Fields{
"test": {
Name: "test",
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return "test", nil
},
Description: "test",
},
}})
registerUserType()
schemaConfig := graphql.SchemaConfig{
Query: queryType,
Mutation: mutationType,
Subscription: subscriptType,
}
启动项目,进入GraphiQL:
定义文章数据
照例我们需要分析关于文章,我们需要什么数据:
- 获取指定文章内容:文章的标题,内容
- 获取文章列表:文章的标题列表,也可以包含简介
- 新增文章:文章标题,内容,作者,标签
- 修改文章:文章标题,内容,作者,标签,ID
- 删除文章:文章ID
我们在controller
目录下新建文件article.go
:
package controller
import (
"github.com/graphql-go/graphql"
"github.com/graphql-go/relay"
)
var articleState = graphql.NewEnum(graphql.EnumConfig{
Name: "ArticleState",
Values: graphql.EnumValueConfigMap{
"unaudited": {Value: "unaudited", Description: "未审核"},
"online": {Value: "online", Description: "已上线"},
"offline": {Value: "offline", Description: "已下线"},
"deleted": {Value: "deleted", Description: "已删除"},
},
Description: "文章状态",
})
var articleType = graphql.NewObject(graphql.ObjectConfig{
Name: "Article",
Fields: graphql.Fields{
"id": relay.GlobalIDField("Article", nil),
"sn": {Type: graphql.NewNonNull(graphql.String), Description: "序号"},
"title": {Type: graphql.NewNonNull(graphql.String), Description: "标题"},
"uid": {Type: graphql.NewNonNull(graphql.ID), Description: "作者ID"},
"cover": {Type: graphql.String, Description: "封面"},
"content": {Type: graphql.String, Description: "文章内容"},
"tags": {Type: graphql.NewList(graphql.String), Description: "标签"},
"state": {Type: graphql.NewNonNull(articleState), Description: "状态"},
},
Description: "文章",
})
var articleConnectionDefinition = relay.ConnectionDefinitions(relay.ConnectionConfig{
Name: "Article",
NodeType: articleType,
})
func registerArticleType() {
queryType.AddFieldConfig("GetArticle", &graphql.Field{
Type: articleType,
Args: graphql.FieldConfigArgument{"id": {Type: graphql.NewNonNull(graphql.ID), Description: "ID"}},
Resolve: nil,
Description: "获取指定文章",
})
queryType.AddFieldConfig("Articles", &graphql.Field{
Type: articleConnectionDefinition.ConnectionType,
Args: relay.NewConnectionArgs(graphql.FieldConfigArgument{
"title": {Type: graphql.String, Description: "标题"},
"uid": {Type: graphql.ID, Description: "作者ID"},
"content": {Type: graphql.String, Description: "内容"},
"tags": {Type: graphql.NewList(graphql.String), Description: "标签"},
}),
Resolve: nil,
Description: "获取文章列表",
})
mutationType.AddFieldConfig("CreateArticle", &graphql.Field{
Type: articleType,
Args: graphql.FieldConfigArgument{
"title": {Type: graphql.NewNonNull(graphql.String), Description: "标题"},
"cover": {Type: graphql.String, Description: "封面"},
"content": {Type: graphql.String, Description: "文章内容"},
"tags": {Type: graphql.NewList(graphql.String), Description: "标签"},
},
Resolve: nil,
Description: "新增文章",
})
mutationType.AddFieldConfig("UpdateArticle", &graphql.Field{
Type: articleType,
Args: graphql.FieldConfigArgument{
"id": {Type: graphql.NewNonNull(graphql.ID), Description: "ID"},
"title": {Type: graphql.NewNonNull(graphql.String), Description: "标题"},
"cover": {Type: graphql.String, Description: "封面"},
"content": {Type: graphql.String, Description: "文章内容"},
"tags": {Type: graphql.NewList(graphql.String), Description: "标签"},
},
Resolve: nil,
Description: "修改文章",
})
mutationType.AddFieldConfig("DeleteArticle", &graphql.Field{
Type: articleType,
Args: graphql.FieldConfigArgument{"id": {Type: graphql.NewNonNull(graphql.ID), Description: "ID"}},
Resolve: nil,
Description: "删除文章",
})
}
修改graphql.go
文件:
registerUserType()
registerArticleType()
schemaConfig := graphql.SchemaConfig{
Query: queryType,
Mutation: mutationType,
Subscription: subscriptType,
}
var err error
schema, err = graphql.NewSchema(schemaConfig)
if err != nil {
panic(err)
}
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: false,
Playground: true,
})
在这里我们不仅新注册了文章的type,同时也将GraphQL的调试界面,从GrapiQL换成了Playground,效果如下:
到这里对于GraphQL API的定义方式都有一定了解了。剩余未定义的包括:评论,评论回复,赞,标签,用户计数,文章扩展等数据尚未定义,可以自己动手尝试。我们定义完成之后,可以从Playground中,下载graphql库根据定义,生成的.graphql文件:
type Article {
content: String
count: ArticleEx
cover: String
id: ID!
sn: String!
state: ArticleState!
tags: [String]
title: String!
uid: ID!
}
type ArticleConnection {
edges: [ArticleEdge]
pageInfo: PageInfo!
}
type ArticleEdge {
cursor: String!
node: Article
}
type ArticleEx {
aid: ID!
cmtNum: Int!
viewNum: Int!
zanNum: Int!
}
enum ArticleState {
unaudited
online
offline
deleted
}
type Comment {
aid: String!
content: String!
floor: Int!
id: ID!
replies: [CommentReply]
state: ArticleState!
uid: String!
zanNum: Int!
}
type CommentConnection {
edges: [CommentEdge]
pageInfo: PageInfo!
}
type CommentEdge {
cursor: String!
node: Comment
}
type CommentReply {
cid: ID!
content: String!
id: ID!
state: ArticleState!
uid: ID!
}
enum Gender {
man
woman
unknown
}
type Mutation {
AddTag(name: String!): Tag
CancelFollow(uid: ID!): UserFollow
CancelZan(id: ID!): Boolean
Comment(
aid: ID!
content: String!
): Comment
CreatUser(
username: String!
email: String!
password: String!
): User
CreateArticle(
title: String!
cover: String
content: String
tags: [String]
): Article
DeleteArticle(id: ID!): Article
DeleteComment(id: ID!): Comment
DeleteReply(id: ID!): CommentReply
Follow(uid: ID!): UserFollow
Reply(
cid: ID!
content: String!
): CommentReply
SignIn(
username: String!
password: String!
): User
UpdateArticle(
id: ID!
title: String!
cover: String
content: String
tags: [String]
): Article
UpdateUser(
gender: Gender
introduce: String
username: String
email: String
avatar: String
): User
Zan(
objtype: Objtype!
objid: ID!
): Zan
}
enum Objtype {
reply
article
comment
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}
type Query {
Articles(
title: String
uid: ID
content: String
tags: [String]
last: Int
before: String
after: String
first: Int
): ArticleConnection
Comments(
aid: ID!
before: String
after: String
first: Int
last: Int
): CommentConnection
CurrentUser: User
GetArticle(id: ID!): Article
GetUser(
id: ID
username: String
): User
GetUserList(
before: String
after: String
first: Int
last: Int
): UserConnection
Tag(name: String): [Tag]
}
type Subscription {
test: String
}
type Tag {
id: ID!
name: String!
}
type User {
avatar: String!
email: String!
fans: [User]
follows: [User]
gender: Gender!
id: ID!
introduce: String
root: Boolean!
state: UserState!
userCount: UserCount
username: String!
}
type UserConnection {
edges: [UserEdge]
pageInfo: PageInfo!
}
type UserCount {
articleNum: Int!
fansNum: Int!
followNum: Int!
uid: ID!
words: Int!
zanNum: Int!
}
type UserEdge {
cursor: String!
node: User
}
type UserFollow {
fuid: ID!
id: ID!
uid: ID!
}
enum UserState {
unsign
normal
forbidden
freeze
}
type Zan {
id: ID!
objid: ID!
objtype: Objtype!
uid: ID!
}
其实这个时候,如果我们是前后端分开做的,已经可以开始让前端一起进行了。但是实际上——至少我不是,所以后续我们会将后端的Resolve初步完成,才会开始前端的工作。
作者个人博客地址:https://unrotten.org
作者微信公众号:
有疑问加站长微信联系(非本文作者)