分享
  1. 首页
  2. 文章

go+typescript+graphQL+react构建简书网站(二) 编写GraphQL API

云燕 · · 1042 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

项目地址:https://github.com/unrotten/hello-world-web

开始之前,关于GraphQL的介绍

二话不说,先贴官网:https://graphql.org

DraggedImage.png

如图所见,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函数返回。

定义用户数据

首先我们需要分析,关于用户,我们需要什么数据,以下列了几个最基础的:

  1. 用户注册:用户名,邮箱,密码
  2. 用户登录:用户名/邮箱,密码
  3. 获取当前登录用户信息:当前登录用户详细信息
  4. 获取指定用户信息: 指定用户详细信息
  5. 获取用户列表:用户列表
  6. 修改用户信息:用户详细信息

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:

DraggedImage-1.png

定义文章数据

照例我们需要分析关于文章,我们需要什么数据:

  1. 获取指定文章内容:文章的标题,内容
  2. 获取文章列表:文章的标题列表,也可以包含简介
  3. 新增文章:文章标题,内容,作者,标签
  4. 修改文章:文章标题,内容,作者,标签,ID
  5. 删除文章:文章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,效果如下:

DraggedImage-2.png

到这里对于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
作者微信公众号:
DraggedImage-3.png


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1042 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏