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

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

项目地址: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

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