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

剛開始使用 GraphQL 時,首先要問的問題之一是如何建置 GraphQL 伺服器?由於 GraphQL 僅以規格形式發布,因此您的 GraphQL 伺服器實際上可以使用您偏好的任何程式語言實作。
在開始建置伺服器之前,GraphQL 要求您設計一個schema,進而定義伺服器的 API。在這篇文章中,我們希望了解 schema 的主要組件、闡明實際實作 schema 的機制,並了解諸如 GraphQL.js、graphql-tools
和 graphene-js
等函式庫如何在此過程中協助您。
本文僅探討純 GraphQL 功能 — 沒有定義伺服器如何與用戶端通訊的網路層概念。重點在於「GraphQL 執行引擎」的內部運作方式和查詢解析過程。若要了解網路層,請查看下一篇文章。
GraphQL schema 定義伺服器的 API
定義 schema:Schema Definition Language
GraphQL 具有自己的型別語言,用於編寫 GraphQL schema:Schema Definition Language (SDL)。以最簡單的形式來說,GraphQL SDL 可用於定義如下所示的型別
User
型別本身不會向用戶端應用程式公開任何功能,它僅定義應用程式中使用者模型的結構。為了將功能新增至 API,您需要將欄位新增至 GraphQL schema 的根型別:Query
、Mutation
和 Subscription
。這些型別定義 GraphQL API 的進入點。
例如,考慮以下查詢
只有當對應的 GraphQL schema 使用以下 user
欄位定義 Query
根型別時,此查詢才有效
因此,schema 的根型別決定伺服器將接受的查詢和 mutation 的形狀。
GraphQL schema 為用戶端-伺服器通訊提供明確的合約。
GraphQLSchema
物件是 GraphQL 伺服器的核心
GraphQL.js 是 Facebook 的 GraphQL 參考實作,並為其他函式庫(如 graphql-tools
和 graphene-js
)提供基礎。使用這些函式庫中的任何一個時,您的開發過程都將圍繞 GraphQLSchema
物件,該物件由兩個主要組件組成
- schema 定義
- 實際實作(以 resolver 函式的形式)
對於上面的範例,GraphQLSchema
物件如下所示
如您所見,schema 的 SDL 版本可以直接翻譯成 GraphQLSchema
型別的 JavaScript 表示形式。請注意,此 schema 沒有任何 resolver — 因此它不允許您實際執行任何查詢或 mutation。更多相關資訊請見下一節。
Resolvers 實作 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
實例(具有 id
和 name
欄位的 JS 物件),則 resolve
函式現在可以執行 schema。
在我們深入探討之前,讓我們先花一點時間了解傳遞到 resolver 中的四個引數
root
(有時也稱為parent
):還記得我們說過 GraphQL 伺服器解析查詢所需做的就是調用查詢欄位的 resolver 嗎?嗯,它是以廣度優先(逐層)的方式執行此操作,並且每個 resolver 調用中的root
引數只是先前調用的結果(如果未另行指定,則初始值為null
)。args
:此引數攜帶查詢的參數,在本例中為要取得的User
的id
。context
:一個物件,它會透過 resolver 鏈傳遞,每個 resolver 都可以寫入和讀取該物件(基本上是 resolver 用於溝通和共享資訊的方式)。info
:查詢或 mutation 的 AST 表示形式。您可以在本系列的第三部分中閱讀更多有關詳細資訊:解開 GraphQL Resolvers 中 info 引數的神秘面紗。
稍早我們提到 GraphQL schema 中的每個欄位都由 resolver 函式支援。目前我們只有一個 resolver,而我們的 schema 總共有三個欄位:Query
型別上的根欄位 user
,以及 User
型別上的 id
和 name
欄位。其餘兩個欄位仍需要其 resolver。如您所見,這些 resolver 的實作很簡單
查詢執行
考慮我們上面的查詢,讓我們了解它是如何執行的以及如何收集資料。查詢總共包含三個欄位:user
(根欄位)、id
和 name
。這表示當查詢到達伺服器時,伺服器需要調用三個 resolver 函式 — 每個欄位一個。讓我們逐步了解執行流程

- 查詢到達伺服器。
- 伺服器調用根欄位
user
的 resolver — 假設fetchUserById
傳回此物件:{ "id": "abc", "name": "Sarah" }
- 伺服器調用
User
型別上欄位id
的 resolver。此 resolver 的root
輸入引數是先前調用的傳回值,因此它可以簡單地傳回root.id
。 - 與 3 類似,但最終傳回
root.name
。(請注意,3 和 4 可以平行發生。) - 解析過程終止 — 最後,結果會使用
data
欄位包裝,以符合 GraphQL 規範
現在,您真的需要自己為 user.id
和 user.name
編寫 resolver 嗎?使用 GraphQL.js 時,如果實作像範例中一樣簡單,您就不必實作 resolver。因此,您可以省略它們的實作,因為 GraphQL.js 已經根據欄位的名稱和 root
引數推斷出它需要傳回的內容。
最佳化請求:DataLoader 模式
使用上述執行方法,當用戶端傳送深度巢狀查詢時,很容易遇到效能問題。假設我們的 API 也有文章和評論可以請求,並允許此查詢
請注意,我們如何從給定的使用者請求特定的 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
為中心
parse
和buildASTSchema
:給定以 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
實例和查詢 — 然後調用 validate
和 execute
若要了解所有這些函式,請查看這個簡單的節點指令碼,它在一個簡單的範例中使用它們。
graphql
函式正在針對 schema 執行 GraphQL 查詢,而 schema 本身已經包含結構和行為。graphql
的主要作用是協調 resolver 函式的調用,並根據提供的查詢形狀封裝回應資料。在這方面,graphql
函式實作的功能也稱為 GraphQL 引擎。
graphql-tools
:橋接介面和實作
使用 GraphQL 的好處之一是您可以採用 schema 優先的開發流程,這表示您建置的每個功能都會首先在 GraphQL schema 中體現出來 — 然後透過對應的 resolver 實作。這種方法有很多好處,例如,它允許前端開發人員在後端開發人員實際實作 API 之前,開始針對模擬 API 進行工作 — 這要歸功於 SDL。
GraphQL.js 最大的缺點是它不允許您在 SDL 中編寫 schema,然後輕鬆產生
GraphQLSchema
的可執行版本。
如上所述,您可以使用 parse
和 buildASTSchema
從 SDL 建立 GraphQLSchema
實例,但這缺少使執行成為可能所需的 resolve
函式!讓您的 GraphQLSchema
可執行(使用 GraphQL.js)的唯一方法是手動將 resolve
函式新增至 schema 的欄位。
graphql-tools
使用一項重要的功能填補了這個缺口:addResolveFunctionsToSchema
。這非常有用,因為它可用於為建立 schema 提供更友善、基於 SDL 的 API。而這正是 graphql-tools
使用 makeExecutableSchema
所做的事情
因此,使用 graphql-tools
的最大好處是它提供了一個友善的 API,用於將您的宣告式 schema 與 resolver 連接起來!
何時不使用 graphql-tools
?
我們剛剛了解到 graphql-tools
的核心是在 GraphQL.js 之上提供一個便利層,那麼在某些情況下,它不是實作伺服器的正確選擇嗎?
與大多數抽象概念一樣,graphql-tools
透過在其他地方犧牲靈活性,使某些工作流程更容易。它提供了絕佳的「入門」體驗,並避免了快速建置 GraphQLSchema
時的摩擦。但是,如果您的後端有更多自訂需求,例如動態建構和修改您的 schema,則其束縛可能會有點太緊 — 在這種情況下,您可以直接回頭使用 GraphQL.js。
關於 graphene-js
的簡短說明
graphene-js
是一個新的 GraphQL 函式庫,它遵循其 Python 對應項目中的想法。它也在底層使用 GraphQL.js,但不允許在 SDL 中宣告 schema。
graphene-js
深入採用現代 JavaScript 語法,提供了一個直覺的 API,其中查詢和 mutation 可以實作為 JavaScript 類別。看到更多 GraphQL 實作出現以使用新穎的想法豐富生態系統,真是令人興奮!
結論
在本文中,我們揭示了 GraphQL 執行引擎的機制和內部運作方式。從 GraphQL schema 開始,它定義了伺服器的 API,並決定將接受哪些查詢和 mutation,以及回應格式的外觀。然後,我們深入探討了 resolver 函式,並概述了 GraphQL 引擎在解析傳入查詢時啟用的執行模型。最後,我們概述了可用的 JavaScript 函式庫,這些函式庫可協助您實作 GraphQL 伺服器。
如果您想實際了解本文中討論的內容,請查看這個儲存庫。請注意,它具有
graphql-js
和graphql-tools
分支,以比較不同的方法。
一般而言,務必注意 GraphQL.js 提供了建置 GraphQL 伺服器所需的所有功能 — graphql-tools
只是在頂層實作了一個便利層,以滿足大多數用例並提供絕佳的「入門」體驗。只有在對建置 GraphQL schema 有更進階的需求時,才可能有意義卸下包裝,並使用純 GraphQL.js。
在下一篇文章中,我們將討論網路層以及用於實作 GraphQL 伺服器的不同函式庫,如 express-graphql、apollo-server 和 graphql-yoga。然後,第 3 部分涵蓋了 GraphQL resolver 中 info 物件的結構和角色。
別錯過下一篇文章!
註冊 Prisma 電子報