概述
利用 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官网
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文档
解析函数的编写
-
对于普通的通过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方法的实现基本相同。
有疑问加站长微信联系(非本文作者)