概述
利用 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方法的實現基本相同。