2017年12月6日

GraphQL 遠端模式如何工作?

理解 GraphQL 模式拼接(第一部分)

How do GraphQL remote schemas work?

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

遠端模式是一組被稱為模式拼接的工具和技術的基礎,這是 GraphQL 社群中的一個全新話題。在接下來的文章中,我們將更詳細地討論模式拼接的不同方法。

回顧:GraphQL 模式

上一篇文章中,我們已經介紹了 GraphQL 模式的基本機制和內部工作原理。讓我們快速回顧一下!

在開始之前,重要的是要澄清術語GraphQL 模式,因為它可能有多種含義。在本文中,我們主要使用該術語來指代 GraphQLSchema 類的例項,該例項由 GraphQL.js 參考實現提供,並用作用 Node.js 編寫的 GraphQL 伺服器的基礎。

模式由兩個主要元件組成

  • 模式定義:這部分通常用 GraphQL 模式定義語言 (SDL) 編寫,並以抽象方式描述 API 的能力,因此還沒有實際的實現。本質上,模式定義指定了伺服器將接受哪些型別的操作(查詢、變更、訂閱)。請注意,一個有效的模式定義需要包含 Query 型別——以及可選的 Mutation 和/或 Subscription 型別。(在程式碼中引用模式定義時,相應的變數通常稱為 typeDefs。)
  • 解析器:這是模式定義變為現實並獲得其實際行為的地方。解析器實現了模式定義指定的 API。(更多資訊,請參閱上一篇文章。)

當一個模式既有模式定義又有解析器函式時,我們也稱之為可執行模式。請注意,GraphQLSchema 的例項不一定可執行——它可能只包含模式定義而沒有任何解析器附加。

這是一個簡單的示例,使用了 graphql-tools 中的 makeExecutableSchema 函式

typeDefs 包含了模式定義,包括必需的 Query 和一個簡單的 User 型別。resolvers 是一個物件,其中包含 Query 型別上定義的 user 欄位的實現。

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

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

GraphQL 執行引擎是一個程式(或函式),給定一個可執行模式和查詢(或變更),它會產生一個有效的響應。因此,它的主要職責是協調可執行模式中解析器函式的呼叫,並根據 GraphQL 規範正確封裝響應資料。

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

內省 GraphQL API

GraphQL API 的一個便利特性是它們允許內省。這意味著您可以透過傳送所謂的*內省查詢*來提取任何 GraphQL API 的*模式定義*。

考慮到上面的示例,您可以使用以下查詢從模式中提取所有型別及其欄位

這將返回以下 JSON 資料

如您所見,此 JSON 物件中的資訊與我們上面基於 SDL 的模式定義等效(實際上並非 100% 等效,因為我們沒有要求欄位上的引數,但我們可以簡單地擴充套件上面的內省查詢以包含這些引數)。

建立遠端模式

有了內省現有 GraphQL API 模式的能力,我們現在可以簡單地建立一個新的 GraphQLSchema 例項,其模式定義與現有模式相同。這正是 graphql-toolsmakeRemoteExecutableSchema 的思想。

makeRemoteExecutableSchema 接收兩個引數

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

從這裡開始,makeRemoteExecutableSchema實現相當簡單。模式定義被用作新模式的基礎。但是解析器呢,它們從何而來?

顯然,我們不能像下載模式定義那樣*下載*解析器——解析器沒有內省查詢。但是,我們可以建立*新的*解析器,它們使用前面提到的 Link 元件,簡單地將任何傳入的查詢或變更*轉發*到底層 GraphQL API。

廢話少說,讓我們看一些程式碼!這裡有一個基於 Graphcool CRUD API 的示例,用於名為 User 的型別,以建立一個遠端模式,然後透過一個專用伺服器(使用 graphql-yoga)暴露。

可以在這裡找到此程式碼的執行示例

作為背景,User 型別的 CRUD API 看起來與此類似(完整版本可以在這裡找到)

遠端模式的幕後

讓我們探討上面示例中的 databaseServiceSchemaDefinitiondatabaseServiceExecutableSchema 在底層是什麼樣子。

檢查 GraphQL 模式

首先要注意的是,它們都是 GraphQLSchema 的例項。然而,databaseServiceSchemaDefinition 只包含模式定義,而 databaseServiceExecutableSchema 實際上是一個可執行模式——這意味著它的型別欄位上附加了解析器函式。

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

GraphQLSchema 的一個非可執行例項GraphQLSchema 的一個非可執行例項

藍色矩形顯示了 Query 型別及其屬性。正如預期,它有一個名為 allUsers 的欄位(以及其他欄位)。然而,在這個模式例項中,Query 的欄位沒有附加任何解析器——因此它不可執行。

讓我們也看看 databaseServiceExecutableSchema

可執行模式 = 模式定義 + 解析器可執行模式 = 模式定義 + 解析器

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

我們可以更進一步,實際檢視 resolve 函式的實現(請注意,此程式碼是由 makeRemoteExecutableSchema 動態生成的)

第 12-16 行是我們感興趣的部分:一個名為 fetcher 的函式被呼叫,帶有三個引數:queryvariablescontextfetcher 是根據我們之前提供的 Link 生成的,它基本上是一個能夠將 GraphQL 操作傳送到特定端點(用於建立 Link 的端點)的函式,這正是它在這裡所做的。請注意,在第 13 行中作為查詢值傳遞的實際 GraphQL 文件源自傳遞給解析器的 info 引數(參見第 10 行)。info 包含查詢的 AST 表示。

非根解析器不進行網路呼叫

與我們上面探索 allUsers 根欄位的解析器函式的方式相同,我們也可以調查 User 型別欄位的解析器是什麼樣子。因此,我們需要導航到 databaseServiceExecutableSchema_typeMaps 屬性中,在那裡我們可以找到帶有其欄位的 User 型別

User 型別有兩個欄位:id 和 name(兩者都有附加的解析器函式)User 型別有兩個欄位:id 和 name(兩者都有附加的解析器函式)

兩個欄位(idname)都附加了一個 resolve 函式,這是由 makeRemoteExecutableSchema 生成的實現(請注意,這兩個欄位的實現是相同的)

有趣的是,這次生成的解析器沒有使用 fetcher 函式——實際上它根本沒有進行網路呼叫。返回的結果只是簡單地從傳遞給函式的 parent 引數(第 10 行)中檢索。

遠端模式中的解析器追蹤資料

遠端可執行模式的解析器的追蹤資料也證實了這一發現。在下面的截圖中,我們用 ArticleComment 型別(每個都連線到 existingUser)擴充套件了之前的模式定義,以便我們可以傳送更深層次的巢狀查詢。

GraphQL Playgrounds 開箱即用地支援顯示解析器的追蹤資料(右下)GraphQL Playgrounds 開箱即用地支援顯示解析器的追蹤資料(右下)

從追蹤資料中可以明顯看出,只有根解析器(針對 allUsers 欄位)耗時顯著(167 毫秒)。所有負責返回非根欄位資料的其餘解析器僅需幾微秒即可執行。這可以用我們之前觀察到的現象來解釋:根解析器使用 fetcher 轉發接收到的查詢,而所有非根解析器則根據傳入的 parent 引數簡單地返回它們的資料。

解析器策略

在實現模式定義的解析器函式時,有多種方法可以處理。

標準模式:型別級別解析

考慮以下模式定義

基於 Query 型別,可以向 API 傳送以下查詢

相應的解析器通常會如何實現?一個標準的方法如下(假設此程式碼中以 fetch 開頭的函式正在從資料庫載入資源)

透過這種方法,我們正在進行型別級別的解析。這意味著針對特定查詢的實際物件(例如特定的 Article)是在 Article 型別的任何解析器被呼叫之前獲取的。

考慮上面查詢的解析器呼叫

  1. Query.user 解析器被呼叫,並從資料庫載入一個特定的 User 物件。請注意,它將載入 User 物件的所有標量欄位,包括 idname,儘管這些欄位並未在查詢中請求。但它尚未載入任何 articles 的內容——這將在下一步中發生。
  2. 接下來,User.articles 解析器被呼叫。請注意,輸入引數 parent 是上一個解析器的返回值,因此它是一個完整的 User 物件,這允許解析器訪問 Userid 來載入其對應的 Article 物件。

如果您在理解此示例時遇到困難,請務必閱讀關於 GraphQL 模式的上一篇文章

遠端可執行模式使用多級解析器方法

現在讓我們再次思考遠端模式示例及其解析器。我們瞭解到,當使用遠端可執行模式執行查詢時,資料來源只會在根解析器中被命中*一次*(我們在那裡找到了 fetcher——參見上面的截圖)。所有其他解析器僅根據傳入的 parent 引數返回規範結果(它是初始根解析器呼叫結果的一部分)。

但這如何工作呢?看起來根解析器在一個解析器中獲取所有需要的資料——但這不是很低效嗎?嗯,如果我們總是載入所有物件欄位*包括*所有關係資料,那確實會非常低效。那麼我們如何才能只加載傳入查詢中指定的資料呢?

這就是為什麼遠端可執行模式的根解析器會利用可用的 info 引數,該引數包含查詢資訊。透過檢視實際查詢的選擇集,解析器無需載入物件的所有欄位,而只加載它需要的欄位。正是這個“技巧”使得在單個解析器中載入所有資料仍然高效。

總結

在本文中,我們學習瞭如何使用 graphql-tools 中的 makeRemoteExecutableSchema 為任何現有 GraphQL API 建立一個*代理*。這個代理被稱為*遠端可執行模式*,並執行在您自己的伺服器上。它只是將接收到的任何查詢轉發到底層 GraphQL API。

我們還看到,這個遠端可執行模式是使用*多級*解析器實現的,其中巢狀資料由第一個解析器一次性獲取,而不是在型別級別多次獲取。

關於遠端模式,還有很多有待探索:這與模式拼接有什麼關係?這與 GraphQL 訂閱如何協同工作?我的 context 物件會怎樣?在評論中告訴我們您接下來想學習什麼!👋

不要錯過下一篇文章!

訂閱 Prisma 新聞通訊

© . This site is unofficial and not affiliated with Prisma Data, Inc.