2017年12月06日

GraphQL remote schemas 如何運作?

了解 GraphQL schema stitching (第一部分)

How do GraphQL remote schemas work?

在本文中,我們希望了解如何使用*任何*現有的 GraphQL API,並透過我們自己的伺服器公開它。在這種設定中,我們的伺服器只是將其接收到的 GraphQL 查詢和變更*轉發*到底層的 GraphQL API。負責轉發這些操作的組件稱為*遠端(可執行)schema*。

遠端 schema 是 *schema stitching* 這組工具和技術的基礎,這是 GraphQL 社群中一個全新的主題。在接下來的文章中,我們將更詳細地討論 schema stitching 的不同方法。

回顧:GraphQL Schema

先前的文章中,我們已經介紹了 GraphQL schema 的基本機制和內部運作方式。讓我們快速回顧一下!

在開始之前,重要的是要消除術語 *GraphQL schema* 的歧義,因為它可以表示多種含義。在本文的上下文中,我們主要使用該術語來指稱 GraphQLSchema 類別的實例,該類別由 GraphQL.js 參考實作提供,並用作以 Node.js 撰寫的 GraphQL 伺服器的基礎。

Schema 由兩個主要組件組成

  • Schema 定義:這部分通常以 GraphQL Schema Definition Language (SDL) 撰寫,並以*抽象*方式描述 API 的功能,因此尚未有實際的*實作*。本質上,schema 定義指定伺服器將接受哪些類型的操作(查詢、變更、訂閱)。請注意,為了使 schema 定義有效,它需要包含 Query 類型 — 以及可選的 Mutation 和/或 Subscription 類型。(在程式碼中引用 schema 定義時,對應的變數通常稱為 typeDefs。)
  • Resolvers:這裡是 schema 定義*活起來*並接收其實際*行為*的地方。Resolvers *實作* schema 定義指定的 API。(如需更多資訊,請參閱上一篇文章。)

當 schema 具有 schema 定義以及 resolver 函數時,我們也將其稱為可執行 schema。請注意,GraphQLSchema 的實例不一定是可執行的 — 可能的情況是它僅包含 schema 定義,但沒有附加任何 resolver。

這是一個簡單範例的外觀,使用來自 graphql-toolsmakeExecutableSchema 函數

typeDefs 包含 schema 定義,包括必需的 Query 和一個簡單的 User 類型。resolvers 是一個物件,其中包含在 Query 類型上定義的 user 欄位的實作。

makeExecutableSchema 現在將 schema 定義中 SDL 類型的欄位對應到 resolvers 物件中定義的對應函數。它返回 GraphQLSchema 的一個實例,我們現在可以使用它來執行實際的 GraphQL 查詢,例如使用來自 GraphQL.js 的 graphql 函數

因為 graphql 函數能夠針對 GraphQLSchema 的實例*執行*查詢,所以它也被稱為 *GraphQL(執行)引擎*。

GraphQL 執行引擎是一個程式(或函數),給定一個可執行 schema 和一個查詢(或變更),它會產生有效的回應。因此,其主要責任是協調可執行 schema 中 resolver 函數的調用,並根據 GraphQL 規範正確地封裝回應資料。

有了這些知識,讓我們深入了解如何基於現有的 GraphQL API 建立 GraphQLSchema 的可執行實例。

Introspecting GraphQL API

GraphQL API 的一個方便特性是它們允許introspection。這表示您可以透過發送所謂的*introspection 查詢*來提取任何 GraphQL API 的 schema 定義。

考慮上面的範例,您可以使用以下查詢從 schema 中提取所有類型及其欄位

這將返回以下 JSON 資料

如您所見,此 JSON 物件中的資訊等同於我們上面基於 SDL 的 schema 定義(實際上它並非 100% 等效,因為我們沒有要求欄位上的*參數*,但我們可以簡單地擴展上面的 introspection 查詢以包含這些參數)。

建立遠端 schema

透過 introspection 現有 GraphQL API 的 schema 的能力,我們現在可以簡單地建立一個新的 GraphQLSchema 實例,其 schema 定義與現有的 schema 定義相同。這正是來自 graphql-toolsmakeRemoteExecutableSchema 的想法。

makeRemoteExecutableSchema 接收兩個參數

  • 一個 schema 定義(您可以使用上面看到的 introspection 查詢獲得)。請注意,最佳實務是在開發時就下載 schema 定義,並將其作為 .graphql 檔案上傳到您的伺服器,而不是在執行時發送 introspection 查詢(這會導致很大的效能開銷)。
  • 一個 Link,它連接到要代理的 GraphQL API。本質上,此 Link 是一個組件,可以將查詢和變更轉發到現有的 GraphQL API — 因此它需要知道其 (HTTP) 端點。

從這裡開始,makeRemoteExecutableSchema實作相當簡單。Schema 定義用作新 schema 的基礎。但是 resolver 呢?它們從哪裡來?

顯然,我們無法以與下載 schema 定義相同的方式下載 resolver — 沒有用於 resolver 的 introspection 查詢。但是,我們可以建立新的 resolver,這些 resolver 使用提及的 Link 組件來簡單地將任何傳入的查詢或變更轉發到底層的 GraphQL API。

廢話不多說,讓我們看一些程式碼!這是一個基於 Graphcool CRUD API 的範例,用於名為 User 的類型,以便建立一個遠端 schema,然後透過專用伺服器(使用 graphql-yoga)公開該 schema

這裡找到此程式碼的運作範例

作為背景資訊,User 類型的 CRUD API 看起來有點類似於這樣(完整版本可以在這裡找到)

遠端 schema 的幕後機制

讓我們調查一下上面範例中的 databaseServiceSchemaDefinitiondatabaseServiceExecutableSchema 在幕後看起來像什麼。

檢查 GraphQL schema

首先要注意的是,它們都是 GraphQLSchema 的實例。但是,databaseServiceSchemaDefinition 僅包含 schema 定義,而 databaseServiceExecutableSchema 實際上是一個可執行 schema — 意味著它確實在其類型的欄位上附加了 resolver 函數。

使用 Chrome 除錯器,我們可以揭示 databaseServiceSchemaDefinition 是一個 JavaScript 物件,如下所示

A non-executable instance of GraphQLSchemaGraphQLSchema 的非可執行實例

藍色矩形顯示了具有其屬性的 Query 類型。正如預期的那樣,它有一個名為 allUsers 的欄位(以及其他欄位)。但是,在此 schema 實例中,沒有 resolver 附加到 Query 的欄位—因此它是不可執行的。

讓我們也看看 databaseServiceExecutableSchema

Executable Schema = Schema definition + Resolvers可執行 Schema = Schema 定義 + Resolvers

此螢幕截圖看起來與我們剛才看到的非常相似—只是 allUsers 欄位現在附加了此 resolve 函數。(Query 類型上的其他欄位(Usernodeuser_allUsersMeta)也是如此,但在螢幕截圖中不可見。)

我們可以更進一步,實際查看 resolve 函數的實作(請注意,此程式碼是透過 makeRemoteExecutableSchema 動態產生的)

第 12–16 行是我們感興趣的內容:一個名為 fetcher 的函數被調用,並帶有三個參數:queryvariablescontextfetcher 是根據我們之前提供的 Link 產生的,它基本上是一個能夠將 GraphQL 操作發送到特定端點(用於建立 Link 的端點)的函數,這正是它在這裡所做的事情。請注意,作為第 13 行中查詢值的實際 GraphQL 文件源自傳遞到 resolver 的 info 參數(請參閱第 10 行)。info 包含查詢的 AST 表示。

非根 resolver 不會發出網路呼叫

以與我們上面探索 allUsers 根欄位的 resolver 函數相同的方式,我們也可以調查 User 類型上的欄位的 resolver 看起來像什麼。因此,我們需要導航到 databaseServiceExecutableSchema_typeMaps 屬性,我們可以在其中找到具有其欄位的 User 類型

The User type has two fields: id and name (both have an attached resolver function)User 類型有兩個欄位:id 和 name(兩者都有附加的 resolver 函數)

兩個欄位(idname)都有一個附加到它們的 resolve 函數,這是它們由 makeRemoteExecutableSchema 產生的實作(請注意,兩個欄位都相同)

有趣的是,這次產生的 resolver 沒有使用 fetcher 函數 — 事實上它根本沒有呼叫網路。返回的結果只是從傳遞到函數的 parent 參數(第 10 行)中檢索出來的。

追蹤遠端 schema 中的 resolver 資料

遠端可執行 schema 的 resolver 的追蹤資料也證實了這一發現。在下面的螢幕截圖中,我們使用 ArticleComment 類型擴展了先前的 schema 定義(每個類型也連接到 existingUser),以便我們可以發送更深層巢狀的查詢。

GraphQL Playgrounds support displaying tracing data for resolvers out-of-the-box (bottom right)GraphQL Playground 支援追蹤 resolver 的資料,開箱即用(右下角)

從追蹤資料中可以很明顯地看出,只有根 resolver(對於 allUsers 欄位)花費了顯著的時間(167 毫秒)。所有負責返回非根欄位資料的剩餘 resolver 僅需幾微秒即可執行。這可以用我們之前觀察到的現象來解釋,即根 resolver 使用 fetcher 來轉發接收到的查詢,而所有非根 resolver 只是根據傳入的 parent 參數返回它們的資料。

Resolver 策略

在實作 schema 定義的 resolver 函數時,有多種方法可以實現這一點。

標準模式:類型層級解析

考慮以下 schema 定義

基於 Query 類型,可以將以下查詢發送到 API

對應的 resolver 通常如何實作?標準方法如下所示(假設此程式碼中以 fetch 開頭的函數是從資料庫載入資源)

透過這種方法,我們在類型層級上進行解析。這表示在呼叫 Article 類型的任何 resolver 之前,會先提取特定查詢的實際物件(例如,特定的 Article)。

考慮上述查詢的 resolver 調用

  1. Query.user resolver 被調用,並從資料庫載入特定的 User 物件。請注意,它將載入 User 物件的所有純量欄位,包括 id 和 name,即使這些欄位在查詢中沒有被請求。它尚未載入任何 articles — 這是下一步發生的事情。
  2. 接下來,User.articles resolver 被調用。請注意,輸入參數 parent 是先前 resolver 的傳回值,因此它是一個完整的 User 物件,允許 resolver 存取 User 的 id 以載入其 Article 物件。

如果您在理解此範例時遇到問題,請務必閱讀關於 GraphQL schema 的上一篇文章

遠端可執行 schema 使用多層級 resolver 方法

現在讓我們再次思考遠端 schema 範例及其 resolver。我們了解到,當使用遠端可執行 schema 執行查詢時,資料來源只會被擊中一次,即在根 resolver 中(我們在其中找到了 fetcher – 請參閱上面的螢幕截圖)。所有其他 resolver 僅根據傳入的 parent 參數返回規範結果(這是初始根 resolver 調用結果的子部分)。

但這是如何運作的?似乎根 resolver 在單個 resolver 中提取所有需要的資料 — 但這不是非常低效嗎?嗯,如果我們總是載入所有物件欄位,包括所有關聯資料,那確實會非常低效。那麼我們如何才能僅載入傳入查詢中指定的資料呢?

這就是為什麼遠端可執行 schema 的根 resolver 利用可用的 info 參數,其中包含查詢資訊。透過查看實際查詢的選擇集,resolver 不必載入物件的所有欄位,而是僅載入它需要的欄位。這個「技巧」使得在單個 resolver 中載入所有資料仍然有效率。

總結

在本文中,我們學習了如何使用來自 graphql-toolsmakeRemoteExecutableSchema 為任何現有的 GraphQL API 建立代理。此代理稱為遠端可執行 schema,並在您自己的伺服器上運行。它只是將其接收到的任何查詢轉發到底層的 GraphQL API。

我們還看到,此遠端可執行 schema 是使用多層級 resolver 實作的,其中巢狀資料由第一個 resolver 提取一次,而不是在類型層級上多次提取。

關於遠端 schema 還有很多值得探索的地方:這與 schema stitching 有何關聯?這如何與 GraphQL 訂閱一起運作?我的 context 物件會發生什麼事?請在評論中告訴我們您接下來想學習什麼!👋

不要錯過下一篇文章!

訂閱 Prisma 電子報