2017年11月14日

GraphQL 伺服器基礎:GraphQL Schema、TypeDefs 和 Resolver 詳解

GraphQL 伺服器的結構與實現(第一部分)

GraphQL Server Basics

初次接觸 GraphQL 時,最先要問的問題之一就是 如何構建一個 GraphQL 伺服器?由於 GraphQL 只是作為一項 規範 釋出,您的 GraphQL 伺服器實際上可以使用您偏好的任何程式語言來 實現

在開始構建伺服器之前,GraphQL 要求您設計一個 schema,它反過來定義了伺服器的 API。在本文中,我們將瞭解 schema 的主要元件,闡明其實際實現的機制,並學習 GraphQL.js、graphql-toolsgraphene-js 等庫如何在此過程中為您提供幫助。

本文僅涉及 純 GraphQL 功能——不涉及定義伺服器與客戶端通訊 方式 的網路層概念。重點在於“GraphQL 執行引擎”的內部工作原理和查詢解析過程。要了解網路層,請檢視下一篇文章

GraphQL schema 定義了伺服器的 API

定義 schema:Schema 定義語言

GraphQL 有自己的型別語言,用於編寫 GraphQL schema:Schema 定義語言 (SDL)。最簡單的形式下,GraphQL SDL 可以用於定義如下所示的型別

單獨的 User 型別不會向客戶端應用程式公開任何功能,它只是定義了應用程式中使用者 模型 的結構。為了向 API 新增功能,您需要將欄位新增到 GraphQL schema 的根型別QueryMutationSubscription。這些型別定義了 GraphQL API 的 入口點

例如,考慮以下查詢

只有當相應的 GraphQL schema 定義了具有以下 user 欄位的 Query 根型別時,此查詢才有效

因此,schema 的根型別決定了伺服器將接受的查詢和變更的形態。

GraphQL schema 為客戶端-伺服器通訊提供了清晰的契約。

GraphQLSchema 物件是 GraphQL 伺服器的核心

GraphQL.js 是 Facebook 的 GraphQL 參考實現,為 graphql-toolsgraphene-js 等其他庫提供了基礎。使用這些庫中的任何一個時,您的開發過程都圍繞著一個 GraphQLSchema 物件展開,該物件由兩個主要元件組成

  • schema 定義
  • 以 resolver 函式形式的實際 實現

對於上面的示例,GraphQLSchema 物件如下所示

如您所見,schema 的 SDL 版本可以直接轉換為 GraphQLSchema 型別的 JavaScript 表示。請注意,此 schema 沒有任何 resolver——因此它不會允許您實際 執行 任何查詢或變更。下一節將詳細介紹。

Resolver 實現 API

GraphQL 伺服器中的結構與行為

GraphQL 明確分離了 結構行為。GraphQL 伺服器的 結構 ——正如我們剛剛討論的——是其 schema,它是對伺服器功能的一種抽象描述。這種結構透過具體的 實現 變得生動起來,該實現決定了伺服器的 行為。實現的關鍵元件是所謂的 resolver 函式。

GraphQL schema 中的每個欄位都由一個 resolver 支援。

最基本的形式是,GraphQL 伺服器的 schema 中每個欄位都有 一個 resolver 函式。每個 resolver 都知道如何為其欄位獲取資料。由於 GraphQL 查詢的本質只是一組欄位,因此 GraphQL 伺服器要收集所需資料,只需呼叫查詢中指定欄位的所有 resolver 函式即可。(這也是為什麼 GraphQL 經常與 RPC 風格的系統進行比較,因為它本質上是一種用於呼叫遠端函式的語言。)

Resolver 函式解析

使用 GraphQL.js 時,GraphQLSchema 物件中型別的每個欄位都可以附加一個 resolve 函式。讓我們考慮上面的示例,特別是 Query 型別上的 user 欄位——我們可以在這裡新增一個簡單的 resolve 函式,如下所示

假設 fetchUserById 函式實際可用並返回一個 User 例項(一個具有 idname 欄位的 JS 物件),那麼 resolve 函式現在就可以啟用 執行 的 schema。

在我們深入探討之前,讓我們花點時間瞭解一下傳遞給 resolver 的四個引數

  1. root(有時也稱為 parent):還記得我們說過 GraphQL 伺服器要解析查詢所需要做的就是呼叫查詢欄位的 resolver 嗎?它以 廣度優先(逐層)的方式執行此操作,並且每個 resolver 呼叫中的 root 引數只是上一個呼叫的結果(如果未另行指定,則初始值為 null)。
  2. args:此引數承載查詢的引數,在本例中是要獲取的 Userid
  3. context:一個在 resolver 鏈中傳遞的物件,每個 resolver 都可以對其進行讀寫(基本上是 resolver 之間進行通訊和共享資訊的一種方式)。
  4. info:查詢或變更的 AST 表示。您可以在本系列的第三部分中瞭解更多詳細資訊:GraphQL Resolver 中 info 引數的解密

前面我們說過,GraphQL schema 中的每個欄位都由一個 resolver 函式支援。目前我們只有一個 resolver,而我們的 schema 總共有三個欄位:Query 型別上的根欄位 user,以及 User 型別上的 idname 欄位。其餘兩個欄位仍然需要它們的 resolver。正如您將看到的,這些 resolver 的實現非常簡單

查詢執行

考慮到我們上面的查詢,讓我們瞭解它是如何執行和收集資料的。該查詢總共包含三個欄位:user根欄位)、idname。這意味著當查詢到達伺服器時,伺服器需要呼叫三個 resolver 函式——每個欄位一個。讓我們逐步瞭解執行流程

Blog image
  1. 查詢到達伺服器。
  2. 伺服器呼叫根欄位 user 的 resolver——我們假設 fetchUserById 返回以下物件:{ "id": "abc", "name": "Sarah" }
  3. 伺服器呼叫 User 型別上欄位 id 的 resolver。此 resolver 的 root 輸入引數是上一個呼叫的返回值,因此它可以簡單地返回 root.id
  4. 類似於 3,但最終返回 root.name。(請注意,3 和 4 可以並行發生。)
  5. 解析過程終止——最終結果將用 data 欄位包裝,以遵守 GraphQL 規範

現在,您真的需要自己為 user.iduser.name 編寫 resolver 嗎?使用 GraphQL.js 時,如果實現像示例中那樣簡單,則無需實現 resolver。因此,您可以省略它們的實現,因為 GraphQL.js 已經根據欄位名稱和 root 引數推斷出需要返回的內容。

最佳化請求:DataLoader 模式

使用上述執行方法,當客戶端傳送深度巢狀的查詢時,很容易遇到效能問題。假設我們的 API 也有帶 評論文章,並允許進行此查詢

請注意,我們如何從給定 user 請求特定的 article,以及其 comments 和撰寫這些評論的使用者的 name

假設這篇文章有五條評論,都由同一個使用者撰寫。這意味著我們將命中 writtenBy resolver 五次,但每次都會返回相同的資料。DataLoader 允許您在這種情況下進行最佳化,以避免 N+1 查詢問題——其普遍思想是 resolver 呼叫被批次處理,因此資料庫(或其他資料來源)只需被訪問一次。

要了解更多關於 DataLoader 的資訊,您可以觀看 Lee Byron 的這部精彩影片:DataLoader — 原始碼解析(約 35 分鐘)

GraphQL.js 與 graphql-tools

現在我們來談談可用於在 JavaScript 中實現 GraphQL 伺服器的庫——主要是 GraphQL.js 和 graphql-tools 之間的區別。

GraphQL.js 為 graphql-tools 奠定了基礎

首先要了解的關鍵一點是,GraphQL.js 為 graphql-tools 提供了基礎。它透過定義所需的型別、實現 schema 構建以及查詢驗證和解析來完成所有繁重的工作。然後,graphql-tools 在 GraphQL.js 之上提供了一個輕薄的便利層。

讓我們快速瀏覽一下 GraphQL.js 提供的函式。請注意,其功能通常圍繞 GraphQLSchema 展開

  • parsebuildASTSchema:給定一個在 GraphQL SDL 中定義為 字串 的 GraphQL schema,這兩個函式將建立一個 GraphQLSchema 例項:const schema = buildASTSchema(parse(sdlString))
  • validate:給定一個 GraphQLSchema 例項和一個查詢,validate 確保該查詢符合 schema 定義的 API。
  • execute:給定一個 GraphQLSchema 例項和一個查詢,execute 呼叫查詢欄位的 resolver,並根據 GraphQL 規範建立響應。當然,這隻有當 resolver 是 GraphQLSchema 例項的一部分時才有效(否則它就像一家只有選單沒有廚房的餐廳)。
  • printSchema:接受一個 GraphQLSchema 例項,並以 SDL 形式(作為 字串)返回其定義。

請注意,GraphQL.js 中最重要的函式是 graphql,它接受一個 GraphQLSchema 例項和一個查詢——然後呼叫 validateexecute

要了解所有這些函式,請檢視這個使用它們進行簡單示例的 Node 指令碼

graphql 函式正在對一個 schema 執行 GraphQL 查詢,該 schema 本身已經包含 結構行為。因此,graphql 的主要作用是編排 resolver 函式的呼叫,並根據所提供查詢的形態打包響應資料。在這方面,graphql 函式實現的功能也稱為 GraphQL 引擎

graphql-tools:連線介面與實現

使用 GraphQL 的好處之一是您可以採用 schema-first 開發流程,這意味著您構建的每個功能首先在 GraphQL schema 中體現——然後透過相應的 resolver 實現。這種方法有許多優點,例如它允許前端開發人員在後端開發人員實際實現 API 之前,就可以針對模擬 API 進行開發——這得益於 SDL。

GraphQL.js 最大的缺點是它不允許您以 SDL 編寫 schema,然後輕鬆生成 GraphQLSchema可執行 版本。

如上所述,您可以使用 parsebuildASTSchema 從 SDL 建立 GraphQLSchema 例項,但它缺少使執行成為可能所需的 resolve 函式!您使 GraphQLSchema 可執行(使用 GraphQL.js)的唯一方法是手動將 resolve 函式新增到 schema 的欄位中。

graphql-tools 透過一個重要的功能彌補了這一空白:addResolveFunctionsToSchema。這非常有用,因為它 T 可以用於提供一個更好、基於 SDL 的 API 來建立您的 schema。而這正是 graphql-tools 透過 makeExecutableSchema 所做的事情

因此,使用 graphql-tools 的最大好處是它提供了連線宣告式 schema 與 resolver 的出色 API!

何時不使用 graphql-tools

我們剛剛瞭解到,graphql-tools 的核心是在 GraphQL.js 之上提供了一個便利層,那麼有沒有不適合用它來實現伺服器的情況呢?

與大多數抽象一樣,graphql-tools 透過犧牲其他地方的靈活性來簡化某些工作流程。它提供了出色的“入門”體驗,並避免了快速構建 GraphQLSchema 時的摩擦。但是,如果您的後端有更定製化的需求,例如動態構建和修改 schema,它的限制可能會有點太緊——在這種情況下,您可以退回使用純 GraphQL.js。

關於 graphene-js 的簡要說明

graphene-js 是一個遵循其Python 對應庫思想的新 GraphQL 庫。它也底層使用 GraphQL.js,但不允許在 SDL 中進行 schema 宣告。

graphene-js 深度融合了現代 JavaScript 語法,提供了一個直觀的 API,其中查詢和變更可以作為 JavaScript 類來實現。看到更多 GraphQL 實現湧現出來,以新穎的理念豐富生態系統,這令人非常興奮!

總結

在本文中,我們揭示了 GraphQL 執行引擎的機制和內部工作原理。首先介紹了定義伺服器 API 並決定接受哪些查詢和變更以及響應格式的 GraphQL schema。然後深入探討了 resolver 函式,並概述了 GraphQL 引擎在解析傳入查詢時啟用的執行模型。最後,總結了可用於實現 GraphQL 伺服器的 JavaScript 庫。

如果您想獲得本文所討論內容的實際概述,請檢視此倉庫。請注意,它有一個graphql-js 和一個graphql-tools 分支,用於比較不同的方法。

總的來說,需要注意的是GraphQL.js 提供了構建 GraphQL 伺服器所需的所有功能——而 graphql-tools 只是在其之上實現了一個便利層,滿足了大多數用例並提供了出色的“入門”體驗。只有在構建 GraphQL schema 方面有更高階要求時,才可能需要放開手腳,使用純 GraphQL.js。

下一篇文章中,我們將討論網路層以及實現 GraphQL 伺服器的不同庫,如express-graphqlapollo-servergraphql-yoga。第三部分將涵蓋 GraphQL resolver 中 info 物件的結構和作用。

不要錯過下一篇文章!

訂閱 Prisma 新聞通訊

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