- 原文件地址:graphql.org/learn/
- 文件翻譯時間:2019年/3月/20日
- 譯者:賀瑞豐(深度使用過GraqhQL)
- 目的:提供更接地氣的中文說明,還有筆者自己的使用經驗 plus:其他的翻譯太爛啦
簡介
- 客戶端:GraphQL是一種查詢語言(for API),即傳送Graphql請求
- 伺服器端:在執行時(runtime)使用你預先為你的資料定義好的型別系統(Type System)來處理客戶端傳送過來的Graphql請求。
它並沒有指定特定的資料庫或者儲存器,完全靠你現有的程式碼和資料支撐
建立一個GraphQL service 需要定義types和內部的fields,然後還需要一個解析函式來處理這些types.
舉例:一個GraphQL service會如下所示 定義types 和 fields
// who the logged in user is (me)
type Query {
me: User
}
// 針對User的查詢
type User {
id: ID
name: String
}
複製程式碼
伺服器端需要對每一個Field解析
function Query_me(request) {
return request.auth.user;
}
function User_name(user) {
return user.getName();
}
複製程式碼
一旦一個GraphQL Service 啟動了,就可以接受GraphQL queries並檢驗和執行。先保證只處理預定好的types-fileds有關的query,然後執行解析函式並返回結果,示例如下
//傳送如下query
{
me {
name
}
}
//得到如下json
{
"me": {
"name": "Luke Skywalker"
}
}
複製程式碼
Queries and Mutations(查詢和修改)
Fields(欄位)
最簡情況下,Graphql會向物件上請求特定的欄位,讓我們從一個簡單的例子開始
- Query
{ hero { name } } 複製程式碼
- Result
{ "data": { "hero": { "name": "R2-D2" } } } 複製程式碼
你可以立即發現上面的query和result的結構是一致的。這是GraqhQL的特性——以為你總是獲取到你想要的,並且服務端精確地知道客戶端想要哪些欄位。
name 欄位返回了一個 String型別,在上例中也就是星球大戰中的hero("R2-D2")
在上面的例子中,我們僅僅是查詢了hero的名字並得到了一個String,但是fields不僅僅可以是類似name這樣一個變數的形式,也可以是一個Objects,這樣你就可以對“hero”中的欄位進行次級選擇(sub-selection ),GraphQL queries可以遍歷相關的Objects-Fields,從而使客戶端可以在一次請求中獲取大量的相關資料。往往我們在REST架構下可能會做多次的往返請求才能實現上述效果。下面是一個例子
-
Query
{ hero { name # Queries can have comments! # friends 就是上述所說的次級選擇,實現了相關查詢 ?? friends { name } } } 複製程式碼
-
返回結果
{ "data": { "hero": { "name": "R2-D2", "friends": [ { "name": "Luke Skywalker" }, { "name": "Han Solo" }, ] } } } 複製程式碼
注意,friends欄位返回了一個陣列。GraphQL queries會等同的對待單個的items或者列表,然而我們可以通過schema中推匯出我們期望返回是哪種型別。
Arguments(引數)
如果僅僅是做可以遍歷相關物件及其欄位的話,GraqhQL已經在獲取資料上非常有用了。但是當你在查詢時可以接受引數時,事情就會變得更加有趣了!
在REST風格的系統中,你只能傳遞一個引數集合--也就是http請求中的查詢引數與URL段。但是在GraphQL中,每一個欄位或內嵌物件都可以獲取一個引數集,這讓GraphQL徹底的取代了執行多次API獲取這種方式。你甚至可以向 scalar fields傳參,來實現在服務端一次性的資料轉換而不是分別在每個客戶端做。
-
Query
{ human(id: "1000") { name # 這裡對高度做了傳參,要求單位是英尺 height(unit: FOOT) } } 複製程式碼
-
返回結果
{ "data": { "human": { "name": "Luke Skywalker", # 返回了英尺高度 ?? "height": 5.6430448 } } } 複製程式碼
引數可以有很多型別。在上例中,我們使用了一個列舉型別(Enumeration type),即一個有限的選擇的集合(set),也就是 米 或者 英尺 等。 GraphQL預設內建了一個型別集, as long as they can be serialized into your transport format.
Read more about the GraphQL type system here.
Aliases(別名)
如果你敏銳的話,你可能已經發現了,既然返回的結果中的物件欄位與query中的相關欄位是匹配的但是並不包含引數,那麼你就不可以針對同一個欄位通過傳輸不同的引數來獲取不同的結果了。這就是我們需要別名 aliases的原因。
譯者注:當你需要針對某個欄位通過不同的引數在一個Query中完成所有的資料獲取時,你就需要用到別名了
- query
# 星球大戰北京。。。。 帝國 絕地武士,關注這裡的別名就行了
{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
複製程式碼
- result
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
複製程式碼
譯者注 :如果沒有別名,你不可能在一個request對hero這個欄位獲取到兩種資料的,所以通過別名傳遞不同的引數來獲得兩份資料,並且在返回的結果中也不是hero了,而是替換成了 empireHero 和 jediHero,這樣也方便了客戶端的處理。
Fragments(片段):可複用單元
加入我們App有一個複雜的頁面,在其中兩種hero的陣營分別佔據頁面的一邊,你可以發現query一下子就複雜起來了,因為我們會重複的去宣告某些欄位。
這也就是為什麼GraphQL包含了 fragments 這種可複用, fragments 讓你可以構建一個欄位集並且在query中不停的複用。下面是例子
- query
{
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
複製程式碼
- result
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"appearsIn": ["NEWHOPE", "EMPIRE","JEDI"],
"friends": [
{ "name": "Han Solo"},
{ "name": "Leia Organa" },
]
},
"rightComparison": {
"name": "R2-D2",
"appearsIn": ["NEWHOPE","EMPIRE","JEDI"],
"friends": [{ "name": "Luke Skywalker" } ]
}
}
}
複製程式碼
你可以發現如果沒有fragments 的話,那些欄位會重複的出現。片段的概念在將複雜的應用資料切分的時候經常被使用到,特別是當你有多個UI元件(有不同的片段)在初始化資料獲取時
在片段中使用變數
這裡用法太簡單了請翻看官方程式碼:graphql.org/learn/queri…
Operation name(操作名)
到現在為止,我們已經使用過了query簡寫的語法糖,我們省略了 query 關鍵字 和 query的名稱,但是在生產型的專案中指定query的名稱是很有必要的。
下面是一個完整的例子:query 關鍵字作為一個 **operation type **, 然後 HeroNameAndFriends 作為操作名
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
複製程式碼
這種 operation type 既不是 query,mutationm 也不是 subscription, 它僅僅的描述了你想做哪種型別的操作。這非常有必要,因為你使用省略語法糖的話你就不能給你的查詢操作傳遞變數了
譯者注:變數的定義都是放在了操作名之後的括號中的,如果省略了操作名,就不能傳變數給query了
給你的query命好名字,在 debug 和伺服器打 log 的時候非常有用。舉個JavaScript的例子,如果你老是喜歡用匿名函式,那麼當這個函式報錯的時候,debug的時候就很難受。具名的query會讓你非常容易跟蹤
Variables(變數)
迄今為止,我們都是在query中將引數用字串寫死的,但是在大多數情況下,這些引數都是動態的。舉個例子:你可能會有一個下拉框來選擇一個 Star Wars episode ,並獲取到其值,傳遞給 query 作為查詢引數。
!!請不要這樣做!!,直接在你的 query 程式碼中 嵌入動態的變數,這樣雖然在 runtime 的時候變數會被解析成相應的字串,然後被 serialize 轉化成 GraphQL-specific format。 !! 應該這樣 !!,GraphQL在處理外部傳輸過來的動態值的時候可以像對待一等公民的處理方式,並且像一個單獨的 dictionary.
要使用變數,需要下面三個步驟
- 將 query 中的靜態值替換成 $variableName
- 在 query 操作名之後的括號中聲名該變數
- 給變數一個單獨的,大多數情況下JSON格式的 變數表,還要指定變數的型別。
query HeroNameAndFriends($episode: Episode) {
# episode引數接受一個$episode變數,而變數在query操作名中被提前定義好了,並且要指定$episode變數的型別
# 本例中變數型別是 Episode
hero(episode: $episode) {
name
friends {
name
}
}
}
// 變數
這裡就是為什麼上面叫 dictionary的原因,通過episode欄位可以來匹配
{
"episode": "JEDI"
}
複製程式碼
現在,我們可以動態的傳遞episode的值給query,而不是重複的定義多個靜態的query(譯者注:如何傳遞變數給query現在還沒有涉及到),再次強調,上述的用法是官方推薦,不要在query中採用插值的方式來構建query
譯者注:比如我之前在JavaScript中經常這麼幹
const {episode} = outerParameter
# 採用ES6 插值字串的方式來構建
hero(episode: `${episode}`) {
name
friends {
name
}
}
}
複製程式碼
Directives (指令)
我們在上面討論瞭如何使用變數來避免手動的採用字串插值的方法構建動態的查詢。在引數中使用變數確實解決了一大類問題。然而我們會需要動態的去調整我們整個query的結構,同樣的使用變數的方式。 舉個例子:我們的 UI 元件有一個彙總和細節兩個檢視,其中一個比另一個包含的欄位要多很多。
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
# 變數
{
"episode": "JEDI",
"withFriends": false
}
# result
{
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
}
複製程式碼
上述例子就是使用來了特性 directive。
directive:可以給附加個一個欄位或者 fragment,服務端會根據傳遞的變數值來處理你的query。
在 GraphQL 標準的核心中寫入了兩種 directive,按照標準實現的後端服務中都應該支援這兩種 directive
- @include(if: Boolean),只有傳遞true時,才會返回對應的結果
- @skip(if: Boolean) 傳遞true時,不會返回對應結果
譯者注:大多數情況下,你並不會用到它,只有當你發現你的query構建的非常複雜時,記得過來看看上述指令是否能幫助到你。
Mutations(變更)
在 REST 中,一個會對伺服器有副作用的請求,不會使用 GET 方法(HTTP Request Method). GraphQL要簡單點-技術上來說任何query都可以被時限為資料寫入。然而任何對於資料的修改(新建,修改,刪除)按照慣例來說都通過 mutation來完成是很有用的。
和 query 一樣,如果 mutaion 也返回一個物件,你也可以獲取內部的欄位,用法幾乎和query是一樣的。
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
# 變數
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
# 返回值
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
複製程式碼
你可能發現了,我們給review傳遞的引數比較特殊,是一個object type,意味著它可以接受一個物件,而不僅僅是一個 scalar type
Multiple fields in mutations
query 和 mutation有一點特殊的不同。 query的欄位是並行執行的,而mutation的欄位是序列的,一個接一個有序的進行的。 這就意味著如果我們在一個請求中發起了兩個 incrementCredits mutation的操作的話,第一個請求會首先被返回,確保我們自己的應用對race condition的考慮。
譯者注:當你的業務比較複雜時,你的頁面可能有很複雜的非同步請求,這個時候不僅僅要考慮到客戶端請求的發出順序,還要考慮到伺服器對每個請求的返回順序。
Inline Fragments(內聯片段)
類似於其他型別系統,GraphQL schemas 也支援介面和集合型別
如果你想查詢的欄位返回的是一個介面或者集合型別,那麼你就需要用到內斂片段來獲取實際資料,如下例:
- request
query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { name ... on Droid { primaryFunction } ... on Human { height } } } // 變數 { "ep": "JEDI" } 複製程式碼
- result
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
複製程式碼
在上述 query中, hero 的欄位返回了一個 Character 型別,依賴於episode引數來返回 Human or Droid,如果沒有內聯片段你就只能查詢Character型別內建好的欄位,比如name
Meta fields
使用內聯片段的時候,如果沒有 __typename這種元欄位標明資料的來源,客戶端無法分辨資料,看下面的例子你就明白了
- query
{ search(text: "an") { __typename ... on Human { name } ... on Droid { name } ... on Starship { name } } } 複製程式碼
- result
{ "data": { "search": [ { "__typename": "Human", "name": "Han Solo" }, { "__typename": "Human", "name": "Leia Organa" }, { "__typename": "Starship", "name": "TIE Advanced x1" } ] } } 複製程式碼
譯者注:沒有標誌的話,那麼返回的資料是下面這樣的?你完全沒法處理
{
"data": {
"search": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "TIE Advanced x1"
}
]
}
}
複製程式碼
GraphQL 提供了一些元欄位,參看下面的連結
!!! 全文完 !!!
文件的Server端的翻譯也做好了:juejin.im/post/5c9330…