GraphQL入門

小紅星閃啊閃發表於2018-10-04

Graphql入門

GraphQL是一個查詢語言,由Facebook開發,用於替換RESTful API。服務端可以用任何的語言實現。具體的你可以檢視Facebook關於GraphQL的文件各種語言的實現

GraphQL的小歷史

早在2012年,Facebook認為人們只有在離開PC的時候才會用智慧手機,很快他們就發現這個認識是多麼的錯誤!於是Facebook把注意力從Web移到了智慧終端上。在那個時候,他們嚴重的依賴於RESTful API。大量的併發請求和對補充資料的二次請求給他們造成了很大的麻煩,尤其是響應時間。一個解決方案是設計足夠多的資源來滿足單次的請求。但是,這造成了服務端的擴充套件和維護困難。

在尋找更好的解決方案的過程中,Facebook的工程師發現開發人員不應該先入為主的把資料看成RESTful一樣的集合。如何更好地儲存和獲取資料不應該是他們要主要考慮的內容。他們應該更多的考慮資料的關係,網狀的關係。

在這個情況下GraphQL應運而生。

GraphQL工作機制

一個GraphQL查詢可以包含一個或者多個操作(operation),類似於一個RESTful API。操作(operation)可以使兩種型別:查詢(Query)或者修改(mutation)。我們看一個例子:

query {
  client(id: 1) {
    id 
    name
  }
}
複製程式碼

你的第一印象:“這個不是JSON?”。還真不是!就如我們之前說的,GraphQL設計的中心是為客戶端服務。GraphQL的設計者希望可以寫一個和期待的返回資料schema差不多的查詢。

注意上面的例子有三個不同的部分組成:

  • client是查詢的operation
  • (id: 1)包含了傳入給Query的引數
  • 查詢包含idname欄位,這些欄位也是我們希望查詢可以返回的

我們看看server會給這個查詢返回什麼:

{
  "data": {
    "client": {
      "id": "1",
      "name": "Uncle Charlie"
    }
  }
}
複製程式碼

就如我們期望的,server會返回一個JSON串。這個JSON的schema和查詢的基本一致。

我們再看看另一個例子:

query {
  products(product_category_id: 1, order: "price DESC") {
    name 
    shell_size
    manufacturer
    price
  }
}
複製程式碼

這次我們查詢products,並傳入兩個引數:product_category_id用於過濾,一個指明按照price欄位降序排列。查詢中包含的欄位是:nameshell_sizemanufacturerprice)。

你可能已經猜到返回的結果是什麼樣子的了:

{
  "data": {
    "products": [
      {
        "name": "Mapex Black Panther Velvetone 5-pc Drum Shell Kit",
        "shell_size": "22\"x18\" Bass Drum, 10\"x8\" & 12\"x9\" Toms, 14\"x14\" & 16\"x16\" Floor Toms",
        "manufacturer": "Mapex",
        "price": 2949.09
      },
      {
        "name": "Pearl MCX Masters Natural Birdseye Maple 4pc Shell Pack with 22\" Kick",
        "shell_size": "22x18\" Virgin Bass Drum 10x8\" Rack Tom 12x9\" Rack Tom 16x16\" Floor Tom",
        "manufacturer": "Pearl",
        "price": 1768.33
      }
    ]
  }
}
複製程式碼

從這幾個初級的例子裡你可以看出來GraphQL允許客戶端明確指定它要的是什麼,避免了資料後去的冗餘或者不足。和RESTful API對比一下,每一個客戶端都會對應很多個RESTful API或者一個API要服務很多個客戶端。所以說GraphQL是很好的查詢語言。所有的operation、引數和所有可以查詢的欄位都需要在GraphQL server上定義、實現。

GraphQL還解決了另外一個問題。假設我們要查詢product_categories和相關的products。在一個RESTful server上你可以實現一個API,返回全部的資料。但是,大多數的情況下,客戶端會先請求product_categories之後在其他的請求中獲取相關的某些products

我們看看使用GraphQL可以怎麼做:

query {
  product_categories {
    name 
    products {
      name 
      price
    }
  }
}
複製程式碼

我們這一次沒有使用引數。在查詢中我們指定了我麼需要每一個product_categoryname,還有所有的這個類別下的產品,每個產品的欄位也都分別指定。返回的結果:

{
  "data": {
    "product_categories": [
      {
        "name": "Acoustic Drums",
        "products": [
          {
            "name": "Mapex Black Panther Velvetone 5-pc Drum Shell Kit",
            "price": 2949.09
          },
          {
            "name": "Pearl MCX Masters Natural Birdseye Maple 4pc Shell Pack with 22\" Kick",
            "price": 1768.33
          }
        ]
      },
      {
        "name": "Cymbals",
        "products": [
          {
            "name": "Sabian 18\" HHX Evolution Crash Cymbal - Brilliant",
            "price": 319
          },
          {
            "name": "Zildjian 20\" K Custom Dry Light Ride Cymbal",
            "price": 396.99
          },
          {
            "name": "Zildjian 13\" K Custom Dark Hi Hat Cymbals",
            "price": 414.95
          }
        ]
      }
    ]
  }
}
複製程式碼

查詢的巢狀沒有限制,全看我們的查詢和server的實現。比如西面的例子完全合法:

{
  purchases(client_id: 1) {
    date
    quantity
    total
    product {
      name
      price
      product_category {
        name
      }
    }
    client {
      name
      dob
    }
  }
}
複製程式碼

這裡我們請求server返回某個客戶的purchases。查詢裡不僅指定了purchase的欄位,還指定了相關的productproduct_category的名稱。

GraphQL有非常重要的一個特點:強型別

每一個GraphQL server都要定義型別系統。查詢實在這個型別系統的上下文中執行的。
複製程式碼

也就是說,你可以查詢值型別:Int, Float, String, BooleanID

而上例中的purchase裡的欄位,productclientproduct_category都是物件型別(Object Type)的。這些型別都需要我們自己定義。

由於GraphQL查詢都是結構化的,資訊也是類樹結構展示的。值型別(Scalar Type)的可以理解為葉子,物件型別(Object Type)可以理解為樹幹。

操作(Operation)和欄位別名

在GraphQL查詢中可以為Operation裡的欄位指定別名。比如查詢裡指定了欄位cymbal_size,但是客戶端只能接受diameter。另外查詢的返回結果都包含在以operation名稱為key的物件裡,所以這個名稱也可以設定一個別名:

{
  my_product: product(id: 3) {
    id 
    name
    diameter: cymbal_size
  }
}
複製程式碼

返回的資料:

{
  "data": {
    "my_product": {
      "id": "3",
      "name": "Zildjian 13\" K Custom Dark Hi Hat Cymbals",
      "diameter": "13\""
    }
  }
}
複製程式碼

Fragments

現在,客戶端APP要獲取另個分開的list: drum setscymbals。在GraphQL裡你不會被限制在一個operation裡。同時我們也可以像設定欄位別名那樣設定返回結果的別名:

query {
  drumsets: product(product_category_id: 1) {
    id
    name
    manufacturer
    price
    pieces
    shell_size
    shell_type
  }

  cymbals: products(product_category_id: 2) {
    id
    name
    manufacturer
    price
    cymbal_size
  }
}
複製程式碼

你可能已經注意到,在查詢的兩個物件中都包含了欄位:idnamemanufacturerprice

為了避免重複欄位,我們可以使用GraphQL提供的Fragments。我們來把重複的欄位都提出來,放到一個fragment裡:

query {
  drumsets: products(product_category_id: 1) {
    ...ProductCommonFields
    prices
    shell_size
    shell_type
  }

  cymbals: products(product_category_id: 2) {
    ...ProductCommonFields
    cymbal_size
  }
}

fragment ProductCommonFields on Product {
  id
  name
  manufacturer
  price
}
複製程式碼

要使用一個Fragment就使用操作符:...

變數(Variable)

我們要減少查詢語句中的重複,我們來看看另外的一個例子該如何處理:

client(id: 1) {
  name
  dob
}

purchasses(client_id: 1) {
  date
  quantity
  total
  product {
    name 
    price
    product_category {
      name
    }
  }
  client {
    name 
    dob 
  }
}
複製程式碼

我們使用兩個operation查詢server,並且每個都包含了client_id引數。如果可以把這個集中到一起就非常好了。我們可以使用GraphQL的變數來實現這個效果。我們來新增一個clientID變數。

query($clientId: Int) {
  client(id: $clientId) {
    name
    dob
  }

  purchases(client_id: $clientId) {
    date
    quantity
    total
    product {
      name
      price
      product_category {
        name
      }
    }
    client {
      name
      dob
    }
  }
}
複製程式碼

我們在operation的前面定義了變數,然後我們就可以在整個查詢中使用這個變數了。 為了使用變數的定義,我們需要在查詢的時候附帶變數值的JSON

{
  "clientId": 1
}
複製程式碼

當然,我們也可以指定一個預設值:

query ($date: String = "2017/01/28") {
  purchases(date: $date) {
    date
    quantity
    total
  }
}
複製程式碼

Mutation(修改)

GraphQL不僅可以用來查詢資料,也可以建立、更新和銷燬資料。當然和查詢一樣,這些也需要server端有對應的實現。增、刪、改一類的operation在GraphQL裡統稱為Muration(修改)。我們就通過幾個例子來演示一下mutation。

mutation {
  create_client (
    name: "查理大叔"
    dob: "2017/01/28"
  ) {
    id 
    name
    dob
  }
}
複製程式碼

我們現在指定operation的型別為mutation,而不是query。在create_client操作裡我們傳入了建立一個client需要的資料,並最終返回一個查詢集合:

{
  "data": {
    "create_client": {
      "id": "5",
      "name": "查理大叔",
      "dob": "2017/01/28"
    }
  }
}
複製程式碼

上面的資料有一點錯誤,生日不對。下面就來用更新來fix這個錯誤:

mutation {
  update_client (
    id: 5
    dob: "1990/01/01"
  ) {
    id
    name
    dob
  }
}
複製程式碼

最後,如果我們要刪除這個資料可以這樣:

mutation {
  destroy_client(id: 5) {
    name 
    dob
  }
}
複製程式碼

注意:create_clientupdate_clientdestroy_client這些operation都是在GraphQL server實現好的。如果有什麼方法可以知道GraphQL server都實現了什麼方法不是很好,是的有GraphQL的doc可以檢視。

定義說明

GraphQL的一個非常好的特性就是,它會根據已經定義好的型別系統來自動生成出一個說明文件。這樣你就不用一次一次的翻看程式碼,而直接檢視文件來了解operation的全部實現細節。如果你用的是express-graphql, 並設定graphiqltrue的話,那麼就會生成一個web的除錯介面。在最右側可以直接使用doc:

app.use('/mobile/egoods', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,
  pretty: IS_DEVELOPMENT,
}))
複製程式碼

或者,也可以使用對於應定義好的schema的查詢,如:

{
  __schema {
    queryType {
      name 
      fields {
        name
      }
    }
  }
}
複製程式碼

結果為:

{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query",
        "fields": [
          {
            "name": "client"
          },
          {
            "name": "clients"
          },
          {
            "name": "product"
          },
          {
            "name": "product_categories"
          },
          {
            "name": "product_category"
          },
          {
            "name": "products"
          }
        ]
      }
    }
  }
}
複製程式碼

對於mutation型別的操作也是一樣的:

{
  __schema {
    mutationType {
      name
      fields {
        name
      }
    } 
  }
}
複製程式碼

查詢的結果為:

{
  "data": {
    "__schema": {
      "mutationType": {
        "name": "Mutation",
        "fields": [
          {
            "name": "create_client"
          },
          {
            "name": "destroy_client"
          },
          {
            "name": "update_client"
          }
        ]
      }
    }
  }
}
複製程式碼

就像上文展示的一樣,你還可以查詢很多其他的內容。比如:

{
  __schema {
    queryType {
      name
      fields {
        name
        args {
          name
        }
      }
    }
  }
}
複製程式碼

我們來簡單的看看結果是什麼樣的:

{
  "data": {
    "__schema": {
      "queryType": {
        "name": "Query",
        "fields": [
          {
            "name": "clients",
            "args": [
              {
                "name": "ids"
              },
              {
                "name": "name"
              },
              {
                "name": "dob"
              }
            ]
          },
          {
            ...
          },
          {
            "name": "products",
            "args": [
              {
                "name": "ids"
              },
              {
                "name": "product_category_id"
              },
              {
                "name": "order"
              },
              {
                "name": "limit"
              }
            ]
          }
        ]
      }
    }
  }
}
複製程式碼

你會看到server實現了一個clients的查詢operation,引數為idsnamedob。第二個操作是products,在這裡的引數是idsproduct_category_idorderlimit

最後

GraphQL可以讓我們定義更加便捷的查詢Server。如果你有興趣學習的話,我強烈的建議你可以讀一讀GraphQL的定義說明,然後試著自己實現一個GraphQL server。

相關文章