GraphQL 技術淺析

graphql.png

背景

7月份咱們前端團隊推進落地了一個 toB 類型的系統,因爲服務端也由咱們前端工程師來承接,因此服務端技術選型上咱們有了話語權,API 這一起咱們選擇了 GraphQL 。本文將闡述我學習 GraphQL 這門技術的一些思考。javascript

GraphQL 在解決什麼問題

學習一門新技術,首先要把問題域弄清楚。社區有大量 GraphQL 與傳統 API 解決方案(含 REST API)對比文章,總結下來,傳統 API 存在如下問題:html

  • 接口數量衆多維護成本高:接口的數量一般由業務場景的數量決定,爲了儘可能減小接口數量,服務端工程師一般會對業務作抽象,首先構建粒度較小的數據接口,再根據業務場景對數據接口進行組合,對外暴露業務接口,即使這樣,服務端對前端暴露的接口數量仍是很是多,由於業務老是多變的。
  • 接口擴展成本高:出於帶寬的考慮移動端咱們要求接口返回儘可能少的字段,PC 端一般要展示更多字段;考慮首屏性能,咱們又要求對接口作合併;傳統 API 應對這些需求,先後端都面臨改造,成本較高。
  • 接口響應的數據格式沒法預知:因爲接口文檔幾乎老是不能及時更新,前端工程師沒法預知接口響應的數據格式,影響前端開發進度。

針對以上問題,GraphQL 給出了較爲完善的解決方案。前端

GraphQL 如何解決問題

接下來我經過一個實例講解 GraphQL 解決問題的思路,客戶端的述求:根據性別查詢團隊成員列表,返回 idgendernamenickName ,GrahpQL 的處理過程以下圖:
image.pngjava

請求參數在發送到服務端以前會先通過 GraphQL Client 轉換成客戶端 Schema,這段 Schema 實際上是一段 query 開頭的字符串,描述了客戶端的對數據的述求:調用哪一個方法,傳遞什麼樣的參數,返回哪些字段。服務端拿到這段 Schema 以後,經過事先定義好的服務端 Schema 接收請求參數並執行對應的 resolve 函數提供數據服務。整個過程能夠想象成咱們吃自助餐的過程,服務端 Schema 就比如自助餐線,擺上咱們能提供的全部食物;客戶端 Schema 就描述了咱們想要吃的食物,按需獲取就行了。 node

講到這裏,好奇心強的同窗可能已經開始思考這個問題了:客戶端 Schema 本質上就是一段字符串,服務端如何識別並響應這段字符串?react

graphql-js

識別與響應客戶端 Schema 依賴於官方類庫 graphql-js ,服務端拿到客戶端 Schema 字符串後會作以下處理:
image.pngwebpack

  • 解析階段

爲了識別客戶端 Schema, graphql-js 定義了一系列的特徵標識符:ios

export const TokenKind = Object.freeze({
    BANG: '!',
    DOLLAR: '$',
    PAREN_L: '(',
    PAREN_R: ')',
    SPREAD: '...',
    COLON: ':',
    EQUALS: '=',
    BRACKET_L: '[',
    BRACKET_R: ']',
    ...
});

並定義了 AST 語法樹規範,規定語法樹支持如下節點:git

/**
 * The set of allowed kind values for AST nodes.
 */
export const Kind = Object.freeze({
  // Name
  NAME: 'Name',

  // Document
  DOCUMENT: 'Document',
  OPERATION_DEFINITION: 'OperationDefinition',
  VARIABLE_DEFINITION: 'VariableDefinition',
  VARIABLE: 'Variable',

  // Values
  INT: 'IntValue',
  FLOAT: 'FloatValue',
  STRING: 'StringValue',
  BOOLEAN: 'BooleanValue',
  ...
});

有了特徵字符串與 AST 語法樹規範,GraphQL Server 對客戶端 Schema 進行逐字符掃描(charCodeAt),最終解析階段的產出物爲 document ,上文示例中的客戶端 Schema 解析完成以後的部分 documentgithub

{
  "kind":"Document",
  "definitions":[
  {
    "kind":"OperationDefinition",
    "operation":"query",
    "name":{
      "kind":"Name",
      "value":"DisplayMember",
      "loc":{
        "start":13,
        "end":26
      }
    },
    "selectionSet":{
      "kind":"SelectionSet",
      "selections":[
        {
          "kind":"Field",
          "alias":null,
          "name":{
            "kind":"Name",
            "value":"fetchByGender",
            "loc":{
              "start":37,
              "end":50
            }
          },
          "arguments":[
            {
              "kind":"Argument",
              "name":{
                "kind":"Name",
                "value":"gender",
                "loc":{
                  "start":51,
                  "end":57
                }
              },
              "value":{
                "kind":"StringValue",
                "value":"M",
                "loc":{
                  "start":59,
                  "end":62
                }
              },
              "loc":{
                "start":51,
                "end":62
              }
            }
          ],
...

若是客戶端 Schema 不符合服務端定義的 AST 規範,解析過程會直接拋出語法異常 Syntax Error ,拿上文的示例舉例,我將客戶端 Schema 中的 fetchByGender(gender: "M") 改成 fetchByGender(gender) ,只傳遞參數名,不傳遞參數值,則服務端會響應:

{
    "errors":[
        {
            "message":"Syntax Error GraphQL request (3:29) Expected :, found )

2: query DisplayMember {
3: fetchByGender(gender) {
^
4: list {
",
            "locations":[
                {
                    "line":3,
                    "column":29
                }
            ]
        }
    ]
}

結構化的報錯信息也是 GraphQL 的一大特色,定位問題很是方便。只要語法沒問題解析階段就能順利完成,而後進入校驗階段。

  • 校驗階段

校驗階段用於驗證客戶端 Schema 是否按照服務端 Schema 定義好的方式獲取數據,好比:獲取數據的方法名是否有誤,必填項是否有值等等,校驗範圍一共有幾十種,我沒有辦法一一舉例。拿上文的示例舉例,我將客戶端 Schema 中的 fetchByGender 改成 fetchByGenfetchByGen 在服務端根本沒有定義,則服務端會響應:

{
    "errors":[
        {
            "message":"Cannot query field "fetchByGen" on type "Query". Did you mean "fetchByGender"?",
            "locations":[
                {
                    "line":3,
                    "column":9
                }
            ]
        }
    ]
}

不只返回結構化的報錯信息,還很是人性化的告訴你正確的調用方式是什麼。校驗階段經過以後會進入執行階段

  • 執行階段

執行階段依賴的輸入爲:解析階段的產出物 document ,服務端 Schema;其中 document 準確描述了客戶端對數據的述求:請求哪一個方法,參數是什麼,須要哪些字段;服務端 Schema 描述了提供數據的方式;拿上文的示例舉例,服務端 Schema 須要這樣定義:

const graphqlApi = require('graphql');
const {
  GraphQLObjectType,
  GraphQLList,
  GraphQLNonNull,
  GraphQLSchema,
  GraphQLString,
} = graphqlApi;

const dataSource = require('./dataSource');

const memType = new GraphQLObjectType({
  name: 'Male',
  description: 'A member gender is Male.',
  fields: () => ({
    id: {
      type: new GraphQLNonNull(GraphQLString),
      description: 'The id of member',
    },
    name: {
      type: GraphQLString,
      description: 'The name of the character.',
    },
    nickName: {
      type: GraphQLString,
      description: 'The nickName of the character.',
    },
    gender: {
      type: GraphQLString,
      description: 'The gender of the character.',
    },
    list: {
      type: new GraphQLList(memType),
      description: 'The mems list by gender.',
    },
  })
});

const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    fetchByGender: {
      type: memType,
      args: {
        gender: {
          description: 'gender of the human',
          type: new GraphQLNonNull(GraphQLString),
        },
      },
      resolve: (root, { gender }) => {
        // 訪問數據庫或三方 API 查詢成員列表
        return {
          list: dataSource.getMembers(gender),
        };
      },
    },
  }),
});

module.exports = new GraphQLSchema({
  query: queryType,
  types: [memType],
});

執行服務端 Schema 中的 resolve 函數,獲得執行階段的輸出:

{
    "data":{
        "fetchByGender":{
            "list":[
                {
                    "id":"1",
                    "gender":"M",
                    "name":"童開宏",
                    "nickName":"慕冥"
                }
            ]
        }
    }
}

固然要完成服務端 Schema 的定義,你須要學習 GraphQL 的 類型系統 ,你們翻閱 API 文檔便可。

技術邊界

原理弄清楚以後咱們須要對 GraphQL 這門技術的邊界有一個清醒的認識:

  • 客戶端邊界:核心能力是將請求參數按照服務端定義好的 AST 語法樹規範拼裝成客戶端 Schema 字符串,實現方案你們可參考apollo提供的 Webpack 插件 ,固然也有一些 GraphQL 客戶端連發送 Ajax 請求的活兒也幹了,無非是在底層調用其餘類庫好比 axios 發請求。
  • 服務端邊界:核心能力是識別客戶端 Schema 字符串,並經過服務端 Schema 調用底層的數據服務按需返回用戶想要的數據,至於底層數據源來自哪裏(數據庫或者三方接口),以何種方式獲取數據(直連數據庫或者 ORM 方法調用),這些不屬於 GraphQL 關心的範疇。

問題解決的怎麼樣

因爲 GraphQL 經過客戶端 Schema 而不是經過 URL 描述數據述求,因此理論上服務端只須要對客戶端暴露一個地址便可,解決了接口數量衆多維護成本高的問題;同時,服務端提供的是全量字段,客戶端可按需獲取,面對接口擴展的需求,服務端沒有開發成本;最後,經過 GraphiQL 可視化調試界面展示服務端能提供的全部數據,開發過程再也不依賴接口文檔:
image.png

GraphQL 社區在忙什麼

GraphQL 官方提供核心能力:

  • graphql-js :GraphQL 理念的 JavaScript 實現,該類庫可同時運行在瀏覽器環境與 Node 環境,該類庫的原理我在上文中已經講過了。
  • graphiql :提高調試體驗,我在上文中提過。
  • dataloader :提高性能,經過合併請求儘可能減小數據庫查詢次數。
  • Relay :前端框架,使 GraphQL 與 React 很好的融合在一塊兒,嵌入性較強,須要 GraphQL Server 配合。

咱們還缺什麼?

  • 服務端

官方只提供了 JavaScript 語言支持,社區愛好者很快在不一樣編程語言中實現了 GraphQL 的理念:JAVA.NET 等等,更多語言支持,請查看 官網

  • 客戶端

官方提供的 Relay 解決了 GraphQL 與 React 相結合的問題,Apollo Client 提供了與其餘前端框架融合的解決方案,好比 Vue、Angular 等等。

  • 開發體驗

    • graphql-tools :在上文示例代碼的服務端 Schema 中,咱們將類型的定義(typeDefs)與處理函數的定義(resolvers)放在同一個文件中,職責上不夠單一,藉助 graphql-tools 咱們能夠將兩者分不一樣的文件定義;
    • egg-graphql :與 Node 框架 egg 相結合,制定 目錄規範 並提供語法糖提升開發效率;

總結

GraphQL 的優勢上文已經講過了,真的是從業務痛點出發,解決了傳統 API 存在的問題,可是 GraphQL 在解決問題的同時也帶了一些新的問題,這些問題在某種程度上阻礙了這門技術的普及:

  • 數據庫性能:GraphQL 將數據描述成一張巨大的網,理論上客戶端 Schema 能夠寫出任意嵌套層級的查詢語句,好比:
query IAmEvil {
  author(id: "abc") {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                # that could go on as deep as the client wants!
              }
            }
          }
        }
      }
    }
  }
}

這樣的查詢語句會給數據庫帶來很大的性能開銷,服務端不得不作 限流 來規避這樣的問題,這也帶來了額外的開發成本。

  • 侵入性:GraphQL 受益最大的是前端,卻須要服務端鼎力支持,特別是老系統遷移,服務端與前端都面臨較大的改造。
  • 學習成本:GraphQL 是一套全新的理念,須要先後端同窗都學習新的知識才能掌握這門技術,這也帶來較大的學習成本。

任何技術都有利弊,你們要結合本身的場景權衡收益作出適合本身的技術選型。

參考文檔

文章可隨意轉載,但請保留此 原文連接
很是歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj(at)alibaba-inc.com 。
相關文章
相關標籤/搜索