服務計算 - 5 | GraphQL簡單web服務與客戶端開發

概述

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章