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的參數- 查詢包含
id
和name
字段,這些字段也是我們希望查詢可以返回的
我們看看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
字段降序排列。查詢中包含的字段是:name
、shell_size
、manufacturer
和price
)。
你可能已經猜到返回的結果是什么樣子的了:
{
"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_category
的name
,還有所有的這個類別下的產品,每個產品的字段也都分別指定。返回的結果:
{
"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
的字段,還指定了相關的product
,product_category
的名稱。
GraphQL有非常重要的一個特點:強類型。
每一個GraphQL server都要定義類型系統。查詢實在這個類型系統的上下文中執行的。
也就是說,你可以查詢值類型:Int
, Float
, String
, Boolean
和ID
。
而上例中的purchase
里的字段,product
、client
和product_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 sets
和cymbals
。在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
}
}
你可能已經注意到,在查詢的兩個對象中都包含了字段:id
、name
、manufacturer
、price
。
為了避免重復字段,我們可以使用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_client
、update_client
和destroy_client
這些operation都是在GraphQL server實現好的。如果有什么方法可以知道GraphQL server都實現了什么方法不是很好,是的有GraphQL的doc可以查看。
定義說明
GraphQL的一個非常好的特性就是,它會根據已經定義好的類型系統來自動生成出一個說明文檔。這樣你就不用一次一次的翻看代碼,而直接查看文檔來了解operation的全部實現細節。如果你用的是express-graphql
, 並設置graphiql
為true
的話,那么就會生成一個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,參數為ids
、name
和dob
。第二個操作是products
,在這里的參數是ids
、product_category_id
和order
、limit
。
最后
GraphQL可以讓我們定義更加便捷的查詢Server。如果你有興趣學習的話,我強烈的建議你可以讀一讀GraphQL的定義說明,然后試着自己實現一個GraphQL server。