如果您以前編寫過 GraphQL 伺服器,那麼很可能已經遇到過傳遞給解析器的 `info` 物件。幸運的是,在大多數情況下,您並不需要真正理解它到底做了什麼以及它在查詢解析過程中的作用。
然而,在一些邊緣情況下,`info` 物件會引起很多困惑和誤解。本文的目標是深入探究 `info` 物件的內部,闡明其在 GraphQL 執行過程中的作用。
本文假設您已經熟悉 GraphQL 查詢和變更如何解析的基礎知識。如果您對此感到有些不確定,您應該查閱本系列的前幾篇文章:第一部分:GraphQL 模式(必讀)第二部分:網路層(可選)
`info` 物件的結構
回顧:GraphQL 解析器的簽名
快速回顧一下,在使用 GraphQL.js 構建 GraphQL 伺服器時,您有兩個主要任務
- 定義您的 GraphQL 模式(可以是 SDL 格式或普通 JS 物件)
- 為模式中的每個欄位,實現一個知道如何返回該欄位值的*解析器*函式
一個解析器函式接收四個引數(按此順序)
parent:上一個解析器呼叫的結果(更多資訊)。args:解析器欄位的引數。context:每個解析器都可以讀取/寫入的自定義物件。info:*這正是我們將在本文中討論的。*
這裡是一個簡單 GraphQL 查詢的執行過程以及所屬解析器呼叫的概覽。因為*第二層解析器*的解析是微不足道的,所以實際上沒有必要實現這些解析器——它們的返回值由 GraphQL.js 自動推斷。
GraphQL 解析器鏈中 `parent` 和 `args` 引數的概覽
`info` 包含查詢 AST 和更多執行資訊
那些對 `info` 物件的結構和作用感到好奇的人,卻發現資訊匱乏。無論是官方的規範,還是文件,都沒有提及它。以前有一個 GitHub 議題要求更好地記錄它,但那個議題在沒有顯著進展的情況下被關閉了。所以,除了深入程式碼之外,別無他法。
從非常高層的角度來看,可以說 `info` 物件包含了傳入 GraphQL 查詢的 AST。因此,解析器知道它們需要返回哪些欄位。
要了解更多關於查詢 AST 的樣子,請務必檢視 Christian Joudrey 的精彩文章 GraphQL 查詢的生命週期——詞法分析/解析,以及 Eric Baer 的精彩演講 GraphQL 幕後。
要理解 `info` 的結構,我們來看看它的 Flow 型別定義
以下是這些鍵的概覽和快速解釋
fieldName:如前所述,GraphQL 模式中的每個欄位都需要由一個解析器支援。fieldName包含屬於當前解析器的欄位名稱。fieldNodes:一個數組,其中每個物件代表剩餘*選擇集*中的一個欄位。returnType:相應欄位的 GraphQL 型別。parentType:此欄位所屬的 GraphQL 型別。path:跟蹤直到當前欄位(即解析器)被訪問為止所遍歷的欄位。schema:表示您*可執行*模式的GraphQLSchema例項。fragments:查詢文件中包含的片段的對映。rootValue:傳遞給執行的rootValue引數。operation:*整個*查詢的 AST。variableValues:與查詢一起提供的任何變數的對映,對應於variableValues 引數。
如果這仍然聽起來很抽象,請不要擔心,我們很快就會看到所有這些的示例。
欄位特定 vs 全域性
關於上述鍵,有一個有趣的觀察結果。`info` 物件上的鍵要麼是*欄位特定*的,要麼是*全域性*的。
*欄位特定*簡單地意味著該鍵的值取決於將 `info` 物件傳遞到的欄位(及其支援的解析器)。顯而易見的例子是 `fieldName`、`returnType` 和 `parentType`。考慮以下 GraphQL 型別中的 `author` 欄位
該欄位的 `fieldName` 就是 `author`,`returnType` 是 `User!`,`parentType` 是 `Query`。
現在,對於 `feed`,這些值當然會有所不同:`fieldName` 是 `feed`,`returnType` 是 `[Post!]!`,`parentType` 也是 `Query`。
因此,這三個鍵的值是欄位特定的。其他欄位特定的鍵包括:`fieldNodes` 和 `path`。實際上,Flow 定義中的前五個鍵都是欄位特定的。
*全域性*則意味著這些鍵的值不會改變——無論我們討論的是哪個解析器。`schema`、`fragments`、`rootValue`、`operation` 和 `variableValues` 對於所有解析器來說都將始終攜帶相同的值。
一個簡單示例
現在我們來看一個 `info` 物件內容的示例。為了鋪墊,以下是我們將在此示例中使用的模式定義
假設該模式的解析器實現如下
請注意,`Post.title` 解析器實際上不是必需的,但我們仍將其包含在此處,以便檢視解析器被呼叫時 `info` 物件的樣子。
現在考慮以下查詢
為簡潔起見,我們只討論 `Query.author` 欄位的解析器,而不討論 `Post.title` 的解析器(在執行上述查詢時仍會呼叫它)。
如果您想嘗試此示例,我們準備了一個倉庫,其中包含上述模式的執行版本,供您進行實驗!
接下來,我們來看看 `info` 物件內部的每個鍵,並瞭解當 `Query.author` 解析器被呼叫時它們的樣子(您可以在此處找到 `info` 物件的完整日誌輸出)。
`fieldName`
`fieldName` 簡單地是 `author`。
`fieldNodes`
請記住,`fieldNodes` 是欄位特定的。它實際上包含了查詢 AST 的一個*摘錄*。這個摘錄從當前欄位(即 `author`)開始,而不是從查詢的*根*開始。(從根開始的整個查詢 AST 儲存在 `operation` 中,見下文)。
`returnType` & `parentType`
如前所述,`returnType` 和 `parentType` 相當簡單
`path`
`path` 跟蹤已遍歷的欄位,直到當前欄位。對於 `Query.author`,它看起來很簡單:"path": { "key": "author" }。
相比之下,在 `Post.title` 解析器中,`path` 如下所示
剩餘的五個欄位屬於“全域性”類別,因此對於 `Post.title` 解析器而言將是相同的。
`schema`
`schema` 是對可執行模式的引用。
`fragments`
fragments 包含片段定義,由於查詢文件中沒有任何片段,因此它只是一個空對映:{}。
`rootValue`
如前所述,`rootValue` 鍵的值對應於最初傳遞給 graphql 執行函式的 rootValue 引數。在本示例中,它就是 `null`。
`operation`
`operation` 包含傳入查詢的完整查詢 AST。回想一下,除了其他資訊之外,這還包含我們上面為 `fieldNodes` 看到的值。
`variableValues`
此鍵表示為查詢傳遞的任何變數。由於我們的示例中沒有變數,因此此值仍然是一個空對映:`{}`。
如果查詢是使用變數編寫的
`variableValues` 鍵將簡單地具有以下值
使用 GraphQL 繫結時 `info` 的作用
如本文開頭所述,在大多數情況下,您根本不需要擔心 `info` 物件。它只是作為解析器簽名的一部分存在,但您實際上並未將其用於任何目的。那麼,它何時變得相關呢?
將 `info` 傳遞給繫結函式
如果您之前使用過 GraphQL 繫結,您會看到 `info` 物件作為生成的繫結函式的一部分。考慮以下模式
使用 `graphql-binding`,您現在可以透過呼叫專用的*繫結函式*來發送可用的查詢和變更,而不是傳送原始查詢和變更。
例如,考慮以下原始查詢,用於檢索特定的 `User`
使用繫結函式實現相同的功能將如下所示
透過在繫結例項上呼叫 `user` 函式並傳遞相應的引數,我們傳達的資訊與上面的原始 GraphQL 查詢完全相同。
一個來自 `graphql-binding` 的繫結函式接受三個引數
args:包含欄位的引數(例如,上面 `createUser` 變更的 `username`)。context:沿著解析器鏈傳遞的context物件。info:info物件。請注意,除了GraphQLResolveInfo例項(即 info 的型別)之外,您還可以傳遞一個簡單定義選擇集的字串。
使用 Prisma 將應用程式模式對映到資料庫模式
info 物件可能導致混淆的另一個常見用例是基於 Prisma 和 prisma-binding 實現 GraphQL 伺服器。
在這種情況下,我們的想法是擁有兩個 GraphQL 層
- 此資料庫層由 Prisma 自動生成,提供通用且強大的 CRUD API
- 該*應用程式層*定義了暴露給客戶端應用程式的 GraphQL API,並根據您應用程式的需求量身定製
作為後端開發者,您負責定義應用程式層的*應用程式模式*並實現其解析器。得益於 `prisma-binding`,解析器的實現僅僅是將傳入查詢*委託*給底層資料庫 API 的過程,沒有大的開銷。
讓我們考慮一個簡單的例子——假設您剛開始為您的 Prisma 資料庫服務使用以下資料模型
Prisma 根據此資料模型生成的資料庫模式看起來與此類似
現在,假設您想構建一個類似的應用程式模式
`feed` 查詢不僅返回 `Post` 元素的列表,還能夠返回列表的 `count`。請注意,它可以選擇性地接受 `authorId`,用於過濾動態以僅返回特定 `User` 編寫的 `Post` 元素。
實現此應用程式模式的第一個直覺可能如下所示。
實現 1:此實現看起來正確,但有一個細微的缺陷
此實現看起來足夠合理。在 `feed` 解析器內部,我們根據可能傳入的 `authorId` 構建 `authorFilter`。然後 `authorFilter` 用於執行 `posts` 查詢並檢索 `Post` 元素,以及 `postsConnection` 查詢,後者提供對列表 `count` 的訪問。
也可以只使用 *postsConnection* 查詢來檢索實際的 *Post* 元素。為了保持簡單,我們仍然使用 *posts* 查詢,並將另一種方法留給細心的讀者作為練習。
事實上,當您使用此實現啟動 GraphQL 伺服器時,乍一看一切都會順利。您會注意到簡單的查詢會被正確處理,例如以下查詢將會成功:
直到您嘗試檢索 `Post` 元素的 `author` 時,您才會遇到問題
好吧!所以,由於某種原因,此實現沒有返回 `author`,這觸發了一個錯誤“無法為非空 `Post.author` 返回 null。”因為 `Post.author` 欄位在*應用程式模式*中被標記為必需。
我們再來看看實現的相關部分
這裡是我們檢索 Post 元素的地方。然而,我們沒有向 posts 繫結函式傳遞*選擇集*。如果沒有向 Prisma 繫結函式傳遞第二個引數,預設行為是查詢該型別的所有*標量*欄位。
這確實解釋了這種行為。呼叫 `ctx.db.query.posts` 返回了正確的 `Post` 元素集,但只返回了它們的 `id` 和 `title` 值——沒有關於 `author` 的關係資料。
那麼,我們如何解決這個問題呢?顯然,我們需要一種方法來告訴 `posts` 繫結函式需要返回哪些欄位。但是,在 `feed` 解析器的上下文中,這些資訊儲存在哪裡呢?您能猜到嗎?
正確:在 `info` 物件內部!因為 Prisma 繫結函式的第二個引數可以是字串*或* `info` 物件,所以我們只需將傳入 `feed` 解析器的 `info` 物件傳遞給 `posts` 繫結函式。
此查詢因實現 2 而失敗:“型別 'Post' 的欄位 'posts' 必須具有子選擇。”
然而,使用此實現,*沒有*請求會被正確處理。舉例來說,考慮以下查詢:
錯誤訊息“型別 'Post' 的欄位 'posts' 必須具有子選擇。”是由上述實現的*第 8 行*產生的。
那麼,這裡發生了什麼?這失敗的原因是 `info` 物件中的*欄位特定*鍵與 `posts` 查詢不匹配。
在 `feed` 解析器內部列印 `info` 物件可以更清楚地說明情況。我們只考慮 `fieldNodes` 中欄位特定的資訊
此 JSON 物件也可以表示為字串選擇集
現在一切都說得通了!我們將上述選擇集傳送到 Prisma 資料庫模式的 posts 查詢,該查詢當然不知道 `feed` 和 `count` 欄位。誠然,產生的錯誤訊息並不是非常有用,但至少我們現在明白了發生了什麼。
那麼,解決這個問題的方法是什麼?一種方法是*手動*解析出 `fieldNodes` 選擇集的正確部分,並將其傳遞給 `posts` 繫結函式(例如,作為字串)。
然而,有一個更優雅的解決方案,那就是為應用程式模式中的 `Feed` 型別實現專用的解析器。以下是正確的實現方式。
實現 3:此實現修復了上述問題
此實現修復了上面討論的所有問題。有幾點需要注意
- 在*第 8 行*,我們現在傳遞一個字串選擇集(
{ id })作為第二個引數。這只是為了提高效率,因為否則會獲取所有標量值(這在我們的示例中不會有太大區別),而我們只需要 ID。 - 我們不再從 `Query.feed` 解析器返回 `posts`,而是返回 `postIds`,它只是一個 ID 陣列(表示為字串)。
- 在 `Feed.posts` 解析器中,我們現在可以訪問由*父級*解析器返回的 `postIds`。這次,我們可以利用傳入的 `info` 物件,並將其簡單地傳遞給 `posts` 繫結函式。
如果您想嘗試此示例,可以檢視此倉庫,其中包含上述示例的執行版本。請隨意嘗試本文中提到的不同實現,並親自觀察其行為!
總結
在本文中,您深入瞭解了在使用 GraphQL.js 實現 GraphQL API 時使用的 `info` 物件。
`info` 物件沒有官方文件——要了解更多資訊,您需要深入研究程式碼。在本教程中,我們首先概述了其內部結構並理解了它在 GraphQL 解析器函式中的作用。然後,我們介紹了一些邊緣情況和潛在陷阱,在這些情況下,需要更深入地理解 `info`。
本文中展示的所有程式碼都可以在相應的 GitHub 倉庫中找到,因此您可以親自實驗並觀察 `info` 物件的行為。
不要錯過下一篇文章!
訂閱 Prisma 新聞通訊
