Graphql入門
GraphQL是一個(gè)查詢語言,由Facebook開發(fā),用于替換RESTful API。服務(wù)端可以用任何的語言實(shí)現(xiàn)。具體的你可以查看Facebook關(guān)于GraphQL的文檔和各種語言的實(shí)現(xiàn)
GraphQL的小歷史
早在2012年,F(xiàn)acebook認(rèn)為人們只有在離開PC的時(shí)候才會用智能手機(jī),很快他們就發(fā)現(xiàn)這個(gè)認(rèn)識是多么的錯(cuò)誤!于是Facebook把注意力從Web移到了智能終端上。在那個(gè)時(shí)候,他們嚴(yán)重的依賴于RESTful API。大量的并發(fā)請求和對補(bǔ)充數(shù)據(jù)的二次請求給他們造成了很大的麻煩,尤其是響應(yīng)時(shí)間。一個(gè)解決方案是設(shè)計(jì)足夠多的資源來滿足單次的請求。但是,這造成了服務(wù)端的擴(kuò)展和維護(hù)困難。
在尋找更好的解決方案的過程中,F(xiàn)acebook的工程師發(fā)現(xiàn)開發(fā)人員不應(yīng)該先入為主的把數(shù)據(jù)看成RESTful一樣的集合。如何更好地存儲和獲取數(shù)據(jù)不應(yīng)該是他們要主要考慮的內(nèi)容。他們應(yīng)該更多的考慮數(shù)據(jù)的關(guān)系,網(wǎng)狀的關(guān)系。
在這個(gè)情況下GraphQL應(yīng)運(yùn)而生。
GraphQL工作機(jī)制
一個(gè)GraphQL查詢可以包含一個(gè)或者多個(gè)操作(operation),類似于一個(gè)RESTful API。操作(operation)可以使兩種類型:查詢(Query)或者修改(mutation)。我們看一個(gè)例子:
query {
client(id: 1) {
id
name
}
}
你的第一印象:“這個(gè)不是JSON?”。還真不是!就如我們之前說的,GraphQL設(shè)計(jì)的中心是為客戶端服務(wù)。GraphQL的設(shè)計(jì)者希望可以寫一個(gè)和期待的返回?cái)?shù)據(jù)schema差不多的查詢。
注意上面的例子有三個(gè)不同的部分組成:
-
client是查詢的operation -
(id: 1)包含了傳入給Query的參數(shù) - 查詢包含
id和name字段,這些字段也是我們希望查詢可以返回的
我們看看server會給這個(gè)查詢返回什么:
{
"data": {
"client": {
"id": "1",
"name": "Uncle Charlie"
}
}
}
就如我們期望的,server會返回一個(gè)JSON串。這個(gè)JSON的schema和查詢的基本一致。
我們再看看另一個(gè)例子:
query {
products(product_category_id: 1, order: "price DESC") {
name
shell_size
manufacturer
price
}
}
這次我們查詢products,并傳入兩個(gè)參數(shù):product_category_id用于過濾,一個(gè)指明按照price字段降序排列。查詢中包含的字段是:name、shell_size、manufacturer和price)。
你可能已經(jīng)猜到返回的結(jié)果是什么樣子的了:
{
"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
}
]
}
}
從這幾個(gè)初級的例子里你可以看出來GraphQL允許客戶端明確指定它要的是什么,避免了數(shù)據(jù)后去的冗余或者不足。和RESTful API對比一下,每一個(gè)客戶端都會對應(yīng)很多個(gè)RESTful API或者一個(gè)API要服務(wù)很多個(gè)客戶端。所以說GraphQL是很好的查詢語言。所有的operation、參數(shù)和所有可以查詢的字段都需要在GraphQL server上定義、實(shí)現(xiàn)。
GraphQL還解決了另外一個(gè)問題。假設(shè)我們要查詢product_categories和相關(guān)的products。在一個(gè)RESTful server上你可以實(shí)現(xiàn)一個(gè)API,返回全部的數(shù)據(jù)。但是,大多數(shù)的情況下,客戶端會先請求product_categories之后在其他的請求中獲取相關(guān)的某些products。
我們看看使用GraphQL可以怎么做:
query {
product_categories {
name
products {
name
price
}
}
}
我們這一次沒有使用參數(shù)。在查詢中我們指定了我么需要每一個(gè)product_category的name,還有所有的這個(gè)類別下的產(chǎn)品,每個(gè)產(chǎn)品的字段也都分別指定。返回的結(jié)果:
{
"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的實(shí)現(xiàn)。比如西面的例子完全合法:
{
purchases(client_id: 1) {
date
quantity
total
product {
name
price
product_category {
name
}
}
client {
name
dob
}
}
}
這里我們請求server返回某個(gè)客戶的purchases。查詢里不僅指定了purchase的字段,還指定了相關(guān)的product,product_category的名稱。
GraphQL有非常重要的一個(gè)特點(diǎn):強(qiáng)類型。
每一個(gè)GraphQL server都要定義類型系統(tǒng)。查詢實(shí)在這個(gè)類型系統(tǒng)的上下文中執(zhí)行的。
也就是說,你可以查詢值類型:Int, Float, String, Boolean和ID。
而上例中的purchase里的字段,product、client和product_category都是對象類型(Object Type)的。這些類型都需要我們自己定義。
由于GraphQL查詢都是結(jié)構(gòu)化的,信息也是類樹結(jié)構(gòu)展示的。值類型(Scalar Type)的可以理解為葉子,對象類型(Object Type)可以理解為樹干。
操作(Operation)和字段別名
在GraphQL查詢中可以為Operation里的字段指定別名。比如查詢里指定了字段cymbal_size,但是客戶端只能接受diameter。另外查詢的返回結(jié)果都包含在以operation名稱為key的對象里,所以這個(gè)名稱也可以設(shè)置一個(gè)別名:
{
my_product: product(id: 3) {
id
name
diameter: cymbal_size
}
}
返回的數(shù)據(jù):
{
"data": {
"my_product": {
"id": "3",
"name": "Zildjian 13\" K Custom Dark Hi Hat Cymbals",
"diameter": "13\""
}
}
}
Fragments
現(xiàn)在,客戶端APP要獲取另個(gè)分開的list: drum sets和cymbals。在GraphQL里你不會被限制在一個(gè)operation里。同時(shí)我們也可以像設(shè)置字段別名那樣設(shè)置返回結(jié)果的別名:
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
}
}
你可能已經(jīng)注意到,在查詢的兩個(gè)對象中都包含了字段:id、name、manufacturer、price。
為了避免重復(fù)字段,我們可以使用GraphQL提供的Fragments。我們來把重復(fù)的字段都提出來,放到一個(gè)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
}
要使用一個(gè)Fragment就使用操作符:...。
變量(Variable)
我們要減少查詢語句中的重復(fù),我們來看看另外的一個(gè)例子該如何處理:
client(id: 1) {
name
dob
}
purchasses(client_id: 1) {
date
quantity
total
product {
name
price
product_category {
name
}
}
client {
name
dob
}
}
我們使用兩個(gè)operation查詢server,并且每個(gè)都包含了client_id參數(shù)。如果可以把這個(gè)集中到一起就非常好了。我們可以使用GraphQL的變量來實(shí)現(xiàn)這個(gè)效果。我們來添加一個(gè)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的前面定義了變量,然后我們就可以在整個(gè)查詢中使用這個(gè)變量了。 為了使用變量的定義,我們需要在查詢的時(shí)候附帶變量值的JSON。
{
"clientId": 1
}
當(dāng)然,我們也可以指定一個(gè)默認(rèn)值:
query ($date: String = "2017/01/28") {
purchases(date: $date) {
date
quantity
total
}
}
Mutation(修改)
GraphQL不僅可以用來查詢數(shù)據(jù),也可以創(chuàng)建、更新和銷毀數(shù)據(jù)。當(dāng)然和查詢一樣,這些也需要server端有對應(yīng)的實(shí)現(xiàn)。增、刪、改一類的operation在GraphQL里統(tǒng)稱為Muration(修改)。我們就通過幾個(gè)例子來演示一下mutation。
mutation {
create_client (
name: "查理大叔"
dob: "2017/01/28"
) {
id
name
dob
}
}
我們現(xiàn)在指定operation的類型為mutation,而不是query。在create_client操作里我們傳入了創(chuàng)建一個(gè)client需要的數(shù)據(jù),并最終返回一個(gè)查詢集合:
{
"data": {
"create_client": {
"id": "5",
"name": "查理大叔",
"dob": "2017/01/28"
}
}
}
上面的數(shù)據(jù)有一點(diǎn)錯(cuò)誤,生日不對。下面就來用更新來fix這個(gè)錯(cuò)誤:
mutation {
update_client (
id: 5
dob: "1990/01/01"
) {
id
name
dob
}
}
最后,如果我們要?jiǎng)h除這個(gè)數(shù)據(jù)可以這樣:
mutation {
destroy_client(id: 5) {
name
dob
}
}
注意:create_client、update_client和destroy_client這些operation都是在GraphQL server實(shí)現(xiàn)好的。如果有什么方法可以知道GraphQL server都實(shí)現(xiàn)了什么方法不是很好,是的有GraphQL的doc可以查看。
定義說明
GraphQL的一個(gè)非常好的特性就是,它會根據(jù)已經(jīng)定義好的類型系統(tǒng)來自動生成出一個(gè)說明文檔。這樣你就不用一次一次的翻看代碼,而直接查看文檔來了解operation的全部實(shí)現(xiàn)細(xì)節(jié)。如果你用的是express-graphql, 并設(shè)置graphiql為true的話,那么就會生成一個(gè)web的調(diào)試界面。在最右側(cè)可以直接使用doc:
app.use('/mobile/egoods', graphqlHTTP({
schema: schema,
rootValue: root,
graphiql: true,
pretty: IS_DEVELOPMENT,
}))
或者,也可以使用對于應(yīng)定義好的schema的查詢,如:
{
__schema {
queryType {
name
fields {
name
}
}
}
}
結(jié)果為:
{
"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
}
}
}
}
查詢的結(jié)果為:
{
"data": {
"__schema": {
"mutationType": {
"name": "Mutation",
"fields": [
{
"name": "create_client"
},
{
"name": "destroy_client"
},
{
"name": "update_client"
}
]
}
}
}
}
就像上文展示的一樣,你還可以查詢很多其他的內(nèi)容。比如:
{
__schema {
queryType {
name
fields {
name
args {
name
}
}
}
}
}
我們來簡單的看看結(jié)果是什么樣的:
{
"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實(shí)現(xiàn)了一個(gè)clients的查詢operation,參數(shù)為ids、name和dob。第二個(gè)操作是products,在這里的參數(shù)是ids、product_category_id和order、limit。
最后
GraphQL可以讓我們定義更加便捷的查詢Server。如果你有興趣學(xué)習(xí)的話,我強(qiáng)烈的建議你可以讀一讀GraphQL的定義說明,然后試著自己實(shí)現(xiàn)一個(gè)GraphQL server。