内容简介:利用 web 客户端调用远端服务是服务开发本实验的重要内容。其中,要点建立 API First 的开发理念,实现前后端分离,使得团队协作变得更有效率。项目参照星球大战API服务器实现其实并不难,但是理解GraphQL和GQLGEN这两个预备工作比较困难,需要大量阅读。
概述
利用 web 客户端调用远端服务是服务开发本实验的重要内容。其中,要点建立 API First 的开发理念,实现前后端分离,使得团队协作变得更有效率。
任务目标
- 选择合适的 API 风格,实现从接口或资源(领域)建模,到 API 设计的过程
- 使用 API 工具,编制 API 描述文件,编译生成服务器、客户端原型
- 使用 Github 建立一个组织,通过 API 文档,实现 客户端项目 与 RESTful 服务项目同步开发
- 使用 API 设计 工具 提供 Mock 服务,两个团队独立测试 API
- 使用 travis 测试相关模块
开发环境选取
GITHUB传送门
项目实现
项目参照星球大战API SWAPI 编写
API设计
客户端实现
数据库实现
服务器实现
服务器实现其实并不难,但是理解GraphQL和GQLGEN这两个预备工作比较困难,需要大量阅读。
GraphQL
GraphQL 是一个用于 API 的查询语言,是一个使用基于类型系统来执行查询的服务端运行时(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。
与Restful相比,GraphQL不会由复杂的URL,请求的Json按照规范被放在数据中。由于有完备的规范,使用GraphQL构建服务器时不需要自行对每个请求进行解析,可以使用现成的框架,如 GQLGen ,按规范编写Schema后即可生成相应的解析函数,最终只需要自己编写resolve中的查询函数即可。无需对每个数据规定复杂的URL,大大简化了开发流程。
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文档
解析函数的编写
-
对于普通的通过ID查询的函数,直接通过数据库提供的方法查询对应ID的对象。
func (r *queryResolver) People(ctx context.Context, id string) (*People, error) { err, people := GetPeopleByID(id, nil) checkErr(err) return people, err }
-
分页查询则需要解析需要的元素数量,起始位置即
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 }
-
其次是基于相关字段的分页查询,与普通分页查询类似,只是多了一个查询字段的字符串来限定,获取对应的页。
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方法的实现基本相同。
以上所述就是小编给大家介绍的《服务计算 - 5 | GraphQL简单web服务与客户端开发》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 客户端负载均衡-微服务架构
- Nacos解读:服务发现客户端
- 【Netty】服务端和客户端
- 服务器如何获取真实客户端 IP
- golang thrift 客户端/服务端代码记录
- Python 绝技 —— TCP 服务器与客户端
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Game Engine Architecture, Second Edition
Jason Gregory / A K Peters/CRC Press / 2014-8-15 / USD 69.95
A 2010 CHOICE outstanding academic title, this updated book covers the theory and practice of game engine software development. It explains practical concepts and techniques used by real game studios,......一起来看看 《Game Engine Architecture, Second Edition》 这本书的介绍吧!