GraphQL 伺服器的結構與實作(第三部分)

如果您之前撰寫過 GraphQL 伺服器,很有可能已經接觸過傳遞到解析器中的 info 物件。幸運的是,在大多數情況下,您真的不需要理解它實際上做什麼,以及它在查詢解析期間的角色。
然而,在許多邊緣情況下,info 物件是造成許多混淆和誤解的原因。本文的目標是深入了解 info 物件的內部,並闡明其在 GraphQL 執行過程中的作用。
本文假設您已經熟悉 GraphQL 查詢和變異 (mutation) 如何解析的基本知識。如果您在這方面感到有點不確定,您絕對應該查看本系列的前幾篇文章:第一部分:GraphQL Schema(必讀)第二部分:網路層(可選)
info
物件的結構
回顧:GraphQL 解析器的簽名
快速回顧一下,當使用 GraphQL.js 建立 GraphQL 伺服器時,您有兩個主要任務
- 定義您的 GraphQL schema(以 SDL 或純 JS 物件形式)
- 對於 schema 中的每個欄位,實作一個 *resolver* 函數,該函數知道如何傳回該欄位的值
一個 resolver 函數接受四個參數(按順序排列)
parent
:先前 resolver 呼叫的結果(更多資訊)。args
:resolver 欄位的參數。context
:每個 resolver 都可以讀取/寫入的自訂物件。info
:*這就是我們將在本文中討論的內容。*
以下是一個簡單 GraphQL 查詢的執行過程以及所屬解析器的調用概觀。由於 *第二層解析器* 的解析是微不足道的,因此實際上不需要實作這些解析器 — 它們的回傳值由 GraphQL.js 自動推斷。

GraphQL 解析器鏈中 parent
和 args
參數的概觀
info
包含查詢 AST 和更多執行資訊
那些對 info 物件的結構和角色感到好奇的人仍然感到困惑。官方 spec 和 文件 都沒有提及它。過去有一個 GitHub issue 要求為其提供更好的文件,但該 issue 在沒有顯著行動的情況下被關閉。因此,除了深入研究程式碼之外,別無他法。
從非常高的層次來看,可以說 info 物件包含傳入 GraphQL 查詢的 AST。由於這個原因,解析器知道它們需要傳回哪些欄位。
若要了解更多關於查詢 AST 的外觀,請務必查看 Christian Joudrey 的精彩文章 Life of a GraphQL Query — Lexing/Parsing 以及 Eric Baer 的精彩演講 GraphQL Under the Hood。
為了理解 info
的結構,讓我們看一下 它的 Flow 型別定義
以下是每個鍵的概觀和快速說明
fieldName
:如前所述,您的 GraphQL schema 中的每個欄位都需要由 resolver 支援。fieldName
包含屬於目前 resolver 的欄位名稱。fieldNodes
:一個陣列,其中每個物件代表剩餘 *選擇集* 中的一個欄位。returnType
:對應欄位的 GraphQL 型別。parentType
:此欄位所屬的 GraphQL 型別。path
:追蹤已遍歷的欄位,直到到達目前欄位(即 resolver)。schema
:代表您 *可執行* schema 的GraphQLSchema
實例。fragments
:查詢文件中 *fragment* 的對應表。rootValue
:傳遞給執行的rootValue
參數。operation
:*整個* 查詢的 AST。variableValues
:與查詢一起提供的任何變數的對應表,對應於 variableValues 參數。
如果這仍然看起來很抽象,請別擔心,我們很快就會看到所有這些的範例。
欄位特定與全域
關於上面的鍵,有一個有趣的觀察。 info
物件上的鍵要么是 *欄位特定* 的,要么是 *全域* 的。
*欄位特定* 僅表示該鍵的值取決於傳遞 info
物件的欄位(及其支援的 resolver)。明顯的範例是 fieldName
、rootType
和 parentType
。考慮以下 GraphQL 型別的 author
欄位
該欄位的 fieldName
只是 author
,returnType
是 User!
,而 parentType
是 Query
。
現在,對於 feed
,這些值當然會有所不同:fieldName
是 feed
,returnType
是 [Post!]!
,而 parentType
也是 Query
。
因此,這三個鍵的值是欄位特定的。進一步的欄位特定鍵是:fieldNodes
和 path
。實際上,上面 Flow 定義的前五個鍵是欄位特定的。
*全域*,另一方面,表示這些鍵的值不會改變 — 無論我們談論的是哪個 resolver。schema
、fragments
、rootValue
、operation
和 variableValues
將始終為所有 resolver 攜帶相同的值。
一個簡單的範例
現在讓我們繼續看一個 info
物件內容的範例。為了設定情境,以下是我們將用於此範例的 schema 定義
假設該 schema 的 resolver 實作如下
請注意,
Post.title
resolver 實際上不是必需的,我們仍然在此處包含它,以查看調用 resolver 時info
物件的外觀。
現在考慮以下查詢
為了簡潔起見,我們將僅討論 Query.author
欄位的 resolver,而不是 Post.title
的 resolver(當執行上述查詢時,它仍然會被調用)。
如果您想試玩這個範例,我們準備了一個 repository,其中包含上述 schema 的執行版本,因此您可以進行實驗!
接下來,讓我們看看 info
物件中的每個鍵,看看當調用 Query.author
resolver 時它們的外觀(您可以在 這裡 找到 info
物件的完整記錄輸出)。
fieldName
fieldName
只是 author
。
fieldNodes
請記住,fieldNodes
是欄位特定的。它有效地包含查詢 AST 的 *摘錄*。此摘錄從目前欄位(即 author
)開始,而不是從查詢的 *根* 開始。(從根開始的整個查詢 AST 儲存在 operation
中,請參閱下文)。
returnType
& parentType
如前所述,returnType
和 parentType
非常簡單
path
path
追蹤已遍歷的欄位,直到目前的欄位。對於 Query.author
,它看起來很簡單:"path": { "key": "author" }
。
為了比較,在 Post.title
resolver 中,path
看起來如下
其餘五個欄位屬於「全域」類別,因此對於
Post.title
resolver 而言將是相同的。
schema
schema
是對可執行 schema 的參考。
fragments
fragments
包含 fragment 定義,由於查詢文件沒有任何 fragment,因此它只是一個空對應表:{}
。
rootValue
如前所述,rootValue
鍵的值對應於首先傳遞給 graphql 執行函數的 rootValue
參數。在範例中,它只是 null
。
operation
operation
包含傳入查詢的完整 查詢 AST。回想一下,除其他資訊外,這包含我們在上面看到的 fieldNodes
的相同值
variableValues
此鍵表示已為查詢傳遞的任何變數。由於我們的範例中沒有變數,因此該值再次只是一個空對應表:{}
。
如果查詢是用變數撰寫的
variableValues
鍵將只具有以下值
使用 GraphQL bindings 時 info
的角色
如本文開頭所述,在大多數情況下,您完全不需要擔心 info
物件。它只是恰好成為您的 resolver 簽名的一部分,但您實際上沒有將其用於任何用途。那麼,它何時變得相關?
將 info
傳遞給 binding 函數
如果您之前使用過 GraphQL bindings,您已經看到 info
物件是產生的 binding 函數的一部分。考慮以下 schema
使用 graphql-binding
,您現在可以透過調用專用的 *binding 函數* 而不是傳送原始查詢和變異來傳送可用的查詢和變異。
例如,考慮以下原始查詢,檢索特定的 User
使用 binding 函數實現相同的功能將如下所示
透過在 binding 實例上調用 user
函數並傳遞相應的參數,我們傳達了與上述原始 GraphQL 查詢完全相同的資訊。
來自 graphql-binding
的 binding 函數接受三個參數
args
:包含欄位的參數(例如,上面createUser
變異的username
)。context
:在 resolver 鏈中向下傳遞的context
物件。info
:info
物件。請注意,您可以傳遞一個簡單定義選擇集的字串,而不是GraphQLResolveInfo
的實例(info 的型別)。
使用 Prisma 將應用程式 schema 對應到資料庫 schema
info 物件可能引起混淆的另一個常見用例是基於 Prisma 和 prisma-binding 實作 GraphQL 伺服器。
在這種情況下,想法是擁有兩個 GraphQL 層
- 資料庫層 由 Prisma 自動產生,並提供通用且強大的 CRUD API
- *應用程式層* 定義暴露給客戶端應用程式且根據您的應用程式需求量身定制的 GraphQL API
作為後端開發人員,您負責定義應用程式層的 *應用程式 schema* 並實作其 resolver。感謝 prisma-binding
,resolver 的實作僅僅是一個 委派 傳入查詢到基礎資料庫 API 的過程,而沒有 major overhead。
讓我們考慮一個簡單的範例 — 假設您從以下 Prisma 資料庫服務的資料模型開始
Prisma 根據此資料模型產生的資料庫 schema 看起來與此類似
現在,假設您想要建立一個看起來與此類似的應用程式 schema
feed
查詢不僅傳回 Post
元素的列表,而且還能夠傳回列表的 count
。請注意,它可選地採用 authorId
,它會篩選 feed 以僅傳回由特定 User
撰寫的 Post
元素。
實作此應用程式 schema 的第一個直覺可能如下所示。
實作 1:此實作看起來正確,但有一個細微的缺陷
此實作看起來相當合理。在 feed
resolver 內部,我們正在根據潛在的傳入 authorId
建構 authorFilter
。authorFilter
然後用於執行 posts
查詢並檢索 Post
元素,以及 postsConnection
查詢,該查詢提供對列表 count
的存取。
也可以僅使用 *postsConnection* 查詢來檢索實際的 *Post* 元素。為了簡化起見,我們仍然使用 *posts* 查詢來實現這一點,並將另一種方法留給細心的讀者練習。
事實上,當使用此實作啟動 GraphQL 伺服器時,一開始看起來會很好。您會注意到簡單的查詢得到正確的服務,例如,以下查詢將成功
直到您嘗試檢索 Post
元素的 author
時,您才會遇到問題
好的!因此,由於某種原因,實作沒有傳回 author
,這會觸發錯誤 *「Cannot return null for non-nullable Post.author.」*,因為 Post.author
欄位在 *應用程式 schema* 中被標記為必填。
讓我們再次看一下實作的相關部分
這是我們檢索 Post 元素的地方。但是,我們沒有將 *選擇集* 傳遞給 posts binding 函數。如果沒有將第二個參數傳遞給 Prisma binding 函數,則預設行為是查詢該型別的所有 *純量* 欄位。
這確實解釋了這種行為。對 ctx.db.query.posts
的呼叫傳回了正確的 Post
元素集,但僅傳回了它們的 id
和 title
值 — 沒有關於 author
的關聯資料。
那麼,我們如何解決這個問題?顯然需要一種方法來告訴 posts
binding 函數它需要傳回哪些欄位。但是,該資訊位於 feed
resolver 的上下文中嗎?您能猜到嗎?
正確:在 info
物件內部!由於 Prisma binding 函數的第二個參數可以是字串 *或* info
物件,因此讓我們只將傳遞到 feed
resolver 的 info
物件傳遞到 posts
binding 函數。
此查詢失敗,並顯示實作 2:「型別為 'Post' 的欄位 'posts' 必須具有子選擇。」
但是,透過此實作,*沒有* 請求會得到正確的服務。例如,考慮以下查詢
錯誤訊息 *「型別為 'Post' 的欄位 'posts' 必須具有子選擇。」* 是由上述實作的 *第 8 行* 產生的。
那麼,這裡發生了什麼事?失敗的原因是因為 info
物件中的 *欄位特定* 鍵與 posts
查詢不符。
在 feed
resolver 內部列印 info
物件可以更清楚地了解情況。讓我們僅考慮 fieldNodes
中的欄位特定資訊
此 JSON 物件也可以表示為字串選擇集
現在一切都說得通了!我們正在將上述選擇集傳送到 Prisma 資料庫 schema 的 posts
查詢,該查詢當然不知道 feed
和 count
欄位。誠然,產生的錯誤訊息不是超級有幫助,但至少我們現在了解了正在發生的事情。
那麼,這個問題的解決方案是什麼?解決此問題的一種方法是 *手動* 解析出 fieldNodes
選擇集的正確部分,並將其傳遞給 posts
binding 函數(例如,作為字串)。
但是,有一個更優雅的解決方案來解決這個問題,那就是為應用程式 schema 中的 Feed
型別實作專用的 resolver。以下是正確的實作方式。
實作 3:此實作修正了上述問題
此實作修正了上面討論的所有問題。有幾件事需要注意
- 在 *第 8 行* 中,我們現在傳遞一個字串選擇集(
{ id }
)作為第二個參數。這只是為了提高效率,因為否則將會擷取所有純量值(在我們的範例中這不會造成巨大的差異),而我們只需要 ID。 - 我們從
Query.feed
resolver 傳回posts
,而不是傳回postIds
,它只是一個 ID 陣列(表示為字串)。 - 在
Feed.posts
resolver 中,我們現在可以存取由 *parent* resolver 傳回的postIds
。這次,我們可以利用傳入的info
物件,並簡單地將其傳遞給posts
binding 函數。
如果您想試玩這個範例,您可以查看 這個 repository,其中包含上述範例的執行版本。隨時嘗試本文中提到的不同實作,並自行觀察其行為!
總結
在本文中,您深入了解了在使用 GraphQL.js 實作 GraphQL API 時使用的 info
物件。
info
物件沒有正式文件記錄 — 若要了解更多關於它的資訊,您需要深入研究程式碼。在本教學中,我們首先概述了其內部結構,並理解了其在 GraphQL resolver 函數中的作用。然後,我們介紹了一些邊緣情況和潛在的陷阱,在這些情況下,需要更深入地了解 info
。
本文中顯示的所有程式碼都可以在相應的 GitHub repository 中找到,因此您可以自行實驗並觀察 info 物件的行為。
不要錯過下一篇文章!
註冊 Prisma Newsletter