服务计算 - 5 | GraphQL简单web服务与客户端开发

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

概述

利用 web 客户端调用远端服务是服务开发本实验的重要内容。其中,要点建立 API First 的开发理念,实现前后端分离,使得团队协作变得更有效率。

任务目标

  1. 选择合适的 API 风格,实现从接口或资源(领域)建模,到 API 设计的过程
  2. 使用 API 工具,编制 API 描述文件,编译生成服务器、客户端原型
  3. 使用 Github 建立一个组织,通过 API 文档,实现 客户端项目 与 RESTful 服务项目同步开发
  4. 使用 API 设计工具提供 Mock 服务,两个团队独立测试 API
  5. 使用 travis 测试相关模块

开发环境选取

  1. API使用GraphQL规范进行设计
  2. 客户端使用Vue框架
  3. 服务器使用GraphQL官方提供的生成基于 graphql 的服务器的库GQLGen进行开发。
  4. 数据库使用BoltDB实现

GITHUB传送门


项目实现

项目参照星球大战API SWAPI编写

API设计

客户端实现

数据库实现

服务器实现

服务器实现其实并不难,但是理解GraphQL和GQLGEN这两个预备工作比较困难,需要大量阅读。

GraphQL

GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。
与Restful相比,GraphQL不会由复杂的URL,请求的Json按照规范被放在数据中。由于有完备的规范,使用GraphQL构建服务器时不需要自行对每个请求进行解析,可以使用现成的框架,如GQLGen,按规范编写Schema后即可生成相应的解析函数,最终只需要自己编写resolve中的查询函数即可。无需对每个数据规定复杂的URL,大大简化了开发流程。

GraphQL官网
GraphQL核心概念
GQLGEN样例

GraphQL只是一个规范,具体使用时必须自行实现解析。这里可以用各种开源库来简化开发流程。

GQLGEN

使用GQLGEN首先应该编写schema.graphql文件,其中按照GraphQL规范,定义了所有结构的内容,以及查询的方法,在这个项目中没有用到客户端更新数据,所以没有使用Mutation。

  • type Query中定义了所有的查询查询方法,在这个类型中的查询函数会被GQLGEN自动实现解析,并在resolver.go文件中新建空白查询函数,而我们的任务就是编写该文件中的函数,返回对应的数据。

    """
    The query root, from which multiple types of requests can be made.
    """
    type Query {
        """
        Look up a specific people by its ID.
        """
        people(
            """
            The ID of the entity.
            """
            id: ID!
        ): People
    
        """
        Look up a specific film by its ID.
        """
        film(
            """
            The ID of the entity.
            """
            id: ID!
        ): Film
        
        """
        Look up a specific starship by its ID.
        """
        starship(
            """
            The ID of the entity.
            """
            id: ID!
        ): Starship
        
        """
        Look up a specific vehicle by its ID.
        """
        vehicle(
            """
            The ID of the entity.
            """
            id: ID!
        ): Vehicle
        
        """
        Look up a specific specie by its ID.
        """
        specie(
            """
            The ID of the entity.
            """
            id: ID!
        ): Specie
        
        """
        Look up a specific planet by its ID.
        """
        planet(
            """
            The ID of the entity.
            """
            id: ID!
        ): Planet
    
        """
        Browse people entities.
        """
        peoples (
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): PeopleConnection!
    
        """
        Browse film entities.
        """
        films (
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): FilmConnection!
    
        """
        Browse starship entities.
        """
        starships (
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): StarshipConnection!
    
        """
        Browse vehicle entities.
        """
        vehicles (
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): VehicleConnection!
    
        """
        Browse specie entities.
        """
        species (
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): SpecieConnection!
    
        """
        Browse planet entities.
        """
        planets (
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): PlanetConnection!
    
        """
        Search for people entities matching the given query.
        """
        peopleSearch (
            """
            The search field for name, in Lucene search syntax.
            """
            search: String!
            
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): PeopleConnection
    
        """
        Search for film entities matching the given query.
        """
        filmsSearch (
            """
            The search field for title, in Lucene search syntax.
            """
            search: String!
            
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): FilmConnection
    
        """
        Search for starship entities matching the given query.
        """
        starshipsSearch (
            """
            The search field for name or model, in Lucene search syntax.
            """
            search: String!
            
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): StarshipConnection
    
        """
        Search for vehicle entities matching the given query.
        """
        vehiclesSearch (
            """
            The search field for name or model, in Lucene search syntax.
            """
            search: String!
            
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): VehicleConnection
    
        """
        Search for specie entities matching the given query.
        """
        speciesSearch (
            """
            The search field for name, in Lucene search syntax.
            """
            search: String!
            
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): SpecieConnection
    
        """
        Search for planet entities matching the given query.
        """
        planetsSearch (
            """
            The search field for name, in Lucene search syntax.
            """
            search: String!
            
            """
            The number of entities in the connection.
            """
            first: Int
    
            """
            The connection follows by.
            """
            after: ID
        ): PlanetConnection
    }
    
  • 其它部分按照GraphQL规范编写即可,具体可以查看项目中的schema.graphql文件。这里的schema.graphql有些臃肿,可以通过实现共同属性的interface来减少定义的工作量。
  • 具体设计参阅API文档

解析函数的编写

  1. 对于普通的通过ID查询的函数,直接通过数据库提供的方法查询对应ID的对象。

    func (r *queryResolver) People(ctx context.Context, id string) (*People, error) {
        err, people := GetPeopleByID(id, nil)
        checkErr(err)
        return people, err
    }
  2. 分页查询则需要解析需要的元素数量,起始位置即after游标在数据库中的位置,是否有前后页及当前页开始和结束位置元素的游标,用于客户端在需要的时候获取前后页。

     func (r *queryResolver) Peoples(ctx context.Context, first *int, after *string) (PeopleConnection, error) {
         from := -1
         if after != nil {
             b, err := base64.StdEncoding.DecodeString(*after)
             if err != nil {
                 return PeopleConnection{}, err
             }
             i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor"))
             if err != nil {
                 return PeopleConnection{}, err
             }
             from = i
         }
         count := 0
         startID := ""
         hasPreviousPage := true
         hasNextPage := true
         // 获取edges
         edges := []PeopleEdge{}
         db, err := bolt.Open("./data/data.db", 0600, nil)
         CheckErr(err)
         defer db.Close()
         db.View(func(tx *bolt.Tx) error {
             c := tx.Bucket([]byte(peopleBucket)).Cursor()
    
             // 判断是否还有前向页
             k, v := c.First()
             if from == -1 || strconv.Itoa(from) == string(k) {
                 startID = string(k)
                 hasPreviousPage = false
             }
    
             if from == -1 {
                 for k, _ := c.First(); k != nil; k, _ = c.Next() {
                     _, people := GetPeopleByID(string(k), db)
                     edges = append(edges, PeopleEdge{
                         Node:   people,
                         Cursor: encodeCursor(string(k)),
                     })
                     count++
                     if count == *first {
                         break
                     }
                 }
             } else {
                 for k, _ := c.First(); k != nil; k, _ = c.Next() {
                     if strconv.Itoa(from) == string(k) {
                         k, _ = c.Next()
                         startID = string(k)
                     }
                     if startID != "" {
                         _, people := GetPeopleByID(string(k), db)
                         edges = append(edges, PeopleEdge{
                             Node:   people,
                             Cursor: encodeCursor(string(k)),
                         })
                         count++
                         if count == *first {
                             break
                         }
                     }
                 }
             }
    
             k, v = c.Next()
             if k == nil && v == nil {
                 hasNextPage = false
             }
             return nil
         })
         if count == 0 {
             return PeopleConnection{}, nil
         }
         // 获取pageInfo
         pageInfo := PageInfo{
             HasPreviousPage: hasPreviousPage,
             HasNextPage:     hasNextPage,
             StartCursor:     encodeCursor(startID),
             EndCursor:       encodeCursor(edges[count-1].Node.ID),
         }
    
         return PeopleConnection{
             PageInfo:   pageInfo,
             Edges:      edges,
             TotalCount: count,
         }, nil
     }
  3. 其次是基于相关字段的分页查询,与普通分页查询类似,只是多了一个查询字段的字符串来限定,获取对应的页。

    func (r *queryResolver) PeopleSearch(ctx context.Context, search string, first *int, after *string) (*PeopleConnection, error) {
    
        if strings.HasPrefix(search, "Name:") {
            search = strings.TrimPrefix(search, "Name:")
        } else {
            return &PeopleConnection{}, errors.New("Search content must be ' Name:<People's Name you want to get> ' ")
        }
        from := -1
        if after != nil {
            b, err := base64.StdEncoding.DecodeString(*after)
            if err != nil {
                return &PeopleConnection{}, err
            }
            i, err := strconv.Atoi(strings.TrimPrefix(string(b), "cursor"))
            if err != nil {
                return &PeopleConnection{}, err
            }
            from = i
        }
        count := 0
        hasPreviousPage := false
        hasNextPage := false
        // 获取edges
        edges := []PeopleEdge{}
        db, err := bolt.Open("./data/data.db", 0600, nil)
        CheckErr(err)
        defer db.Close()
        db.View(func(tx *bolt.Tx) error {
            c := tx.Bucket([]byte(peopleBucket)).Cursor()
            k, _ := c.First()
            // 判断是否还有前向页
            if from != -1 {
                for k != nil {
                    _, people := GetPeopleByID(string(k), db)
                    if people.Name == search {
                        hasPreviousPage = true
                    }
                    if strconv.Itoa(from) == string(k) {
                        k, _ = c.Next()
                        break
                    }
                    k, _ = c.Next()
                }
            }
    
            // 添加edge
            for k != nil {
                _, people := GetPeopleByID(string(k), db)
                if people.Name == search {
                    edges = append(edges, PeopleEdge{
                        Node:   people,
                        Cursor: encodeCursor(string(k)),
                    })
                    count++
                }
                k, _ = c.Next()
                if first != nil && count == *first {
                    break
                }
            }
    
            // 判断是否还有后向页
            for k != nil {
                _, people := GetPeopleByID(string(k), db)
                if people.Name == search {
                    hasNextPage = true
                    break
                }
                k, _ = c.Next()
            }
            return nil
        })
        if count == 0 {
            return &PeopleConnection{}, nil
        }
        // 获取pageInfo
        pageInfo := PageInfo{
            StartCursor:     encodeCursor(edges[0].Node.ID),
            EndCursor:       encodeCursor(edges[count-1].Node.ID),
            HasPreviousPage: hasPreviousPage,
            HasNextPage:     hasNextPage,
        }
    
        return &PeopleConnection{
            PageInfo:   pageInfo,
            Edges:      edges,
            TotalCount: count,
        }, nil
    
    }

其他的查询函数实现和上述People方法的实现基本相同。


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

本文来自:Segmentfault

感谢作者:刘一

查看原文:服务计算 - 5 | GraphQL简单web服务与客户端开发

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

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