GraphQL簡單學習-執行原理
一、簡介
可以參考一下前兩天寫的文章:https://my.oschina.net/hanchao/blog/3014079
二、Execution
參考文章:https://graphql.org/learn/execution/
參考文章:http://graphql.cn/learn/execution/
這篇文件解釋了GraphQL是如何根據定義的schema來完成資料的組裝和聚合的,應該算是整個GraphQL架構中最核心的設計之一了,值得了解。
在必要的資料校驗環境後,GraphQL服務端會根據每一個query的實際要求來剪裁恰如其分的資料結構以進行響應,通常來說是JSON格式。
GraphQL非常依賴其型別模型(type system),我們來看一個實際的例子來演示如何執行一個query。這個例子和文件中其他部分是一致的:
type Query { human(id: ID!): Human } type Human { name: String appearsIn: [Episode] starships: [Starship] } enum Episode { NEWHOPE EMPIRE JEDI } type Starship { name: String }
為了瞭解query執行的細節,我們來看一個例子:
// 請求 { human(id: 1002) { name appearsIn starships { name } } } // 響應 { "data": { "human": { "name": "Han Solo", "appearsIn": [ "NEWHOPE", "EMPIRE", "JEDI" ], "starships": [ { "name": "Millenium Falcon" }, { "name": "Imperial shuttle" } ] } } }
你可以將在GraphQL查詢中每一個欄位(field)看做是前一種型別的函式或方法,它返回下一種型別。事實上,這就是GraphQL的工作原理。
在GraphQL服務端,每一個型別的每一個欄位背後都是靠一個被稱為 resolver
的函式來提供資料(提供支援)的。每當一個欄位被要求返回,與其對應的 resolver
函式就會被執行,併產生下一個值(進入新一輪resolver
執行)。
如果發現欄位返回的是一個標量值,如字串或數字,此時執行就算告一段落了。
然而如果該欄位執行後返回的是一個物件值,那麼執行器會繼續試圖獲取該物件值包含的欄位,一直到最終得到一個標量值為止。
Root fields & resolvers
每個GraphQL服務的最頂層型別是一個包含一切的統一API入口型別,通常我們稱之為 Roottype
或 Querytype
。
在我們的例子中, Querytype
提供了一個欄位叫 human
,它接受一個引數 id
。這個欄位對應的 resolver
函式通過操作資料庫並構建和返回 human
物件。
Query: {
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}
這個例子是用JavaScript寫的,但GQL服務端可以用 多種語言 來實現。 resolver
函式接受4個引數:
- obj: 前一個物件(譯:觸發該
resolver
的欄位所在的物件),這個引數對最頂層欄位沒有意義(譯:當然啊,不然嘞~) - args: 來自GraphQL query
- context: 提供所有
resolver
依賴的資源,例如資料庫連線物件,當前登入的使用者資訊等 - info: 包含與當前查詢相關的特定於欄位的資訊的值,以及模式細節,可以參考 graphqlobjecttype
Asynchronous resolvers
我們來近距離看一下這個 resolver
函式的細節:
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
context
引數中包含了資料庫連線物件,用於資料查詢來得到GraphQL query中要求的 id
資料。查詢資料庫是一個非同步呼叫,所以返回一個 promise
。 promise
是javascript中的非同步呼叫概念,不過其他很多語言也有對應的實現方式,通常被稱之為 futures
, tasks
或者 Deferred
。當資料庫操作返回後,我們就可以構建和返回一個新的 Human
物件啦。
需要注意的是儘管 resolver
函式返回的是 promise
,但GraphQL query並不是非同步的,它會期望 human
攜帶了所有要求返回的資料。在執行過程中,GQL會一直等到 Promise
, Futures
或 Tasks
完結後才會繼續並最大化保持併發度(譯:這一點很重要)。
Trivial resolvers
現在我們已經得到了一個 Human
物件,接下來GraphQL執行器將繼續處理其下的欄位。
Human: {
name(obj, args, context, info) {
return obj.name
}
}
GraphQL服務依靠型別系統來決定如何繼續執行下去。甚至是在 human
返回任何結果之前,GraphQL就可以根據型別系統要求提供的 human
型別宣告得到下一步應該處理的欄位有哪些。
在這個例子裡 name
欄位的處理是非常簡單明瞭的。傳入 name resolver
函式的 obj
引數就是前一步返回的那個 new Human
物件。例子中我們期望得到的 human
物件包含name
欄位,已經如願以償。
事實上,很多GraphQL類庫都不需要你提供這種簡單的 resolver
,它們會預設自動從 obj
中讀取並返回對應名字的屬性(譯:預設解析器規則)。
Scalar coercion
當 name
欄位被處理過後, appearsIn
和 starships
欄位會被同時處理。 appearsIn
欄位也有一個 trivial resolver
,我們來仔細看一下:
Human: {
appearsIn(obj) {
return obj.appearsIn // returns [ 4, 5, 6 ]
}
}
注意,我們的型別系統宣告 appearsIn
將返回一個列舉型別,然而這個函式卻返回的是number陣列!實際上,如果我們檢視結果,我們將看到相應的Enum值被歸還。發生了什麼?
這就是一個 Scalar coercion
的例子。型別系統知道應該返回什麼,並將解析器返回的資料轉換成了API宣告要求的型別。在這個例子中,在服務的其他位置應該存在一個列舉型別的定義來標識 4,5,6
對應的列舉值。
List resolvers
通過 appearsIn
,我們已經看到了當一個欄位需要一個返回多條資料時的細節。它返回了一個列舉值陣列,然後型別系統將每個值轉換成了對應的列舉值。那 starships
欄位解析的細節有是什麼呢?
Human: {
starships(obj, args, context, info) {
return obj.starshipIDs.map(
id => context.db.loadStarshipByID(id).then(
shipData => new Starship(shipData)
)
)
}
}
這個欄位的 resolver
不只是返回一個 promise
,它返回了一個 promise
陣列。 human
物件擁有一個 starships
的 id
陣列,但我們需要載入所有這些id
關聯的 starship
物件。
GraphQL會等待所有的 promise
併發的完成後才會繼續,當所有 starship
物件都得到後,GQL會繼續併發的嘗試獲取這些物件的 name
欄位。
Producing the result
當所有欄位都處理完畢後,結果值構建成一個從葉子節點到根節點全鏈路的鍵值對對映,鍵為欄位名,值為 resolver
返回的結果,最終按照請求的結構返回給客戶端對應的資料結構(JSON結構)。