GraphQL服務器的結構和實現(xiàn)(第三部分)

如果您以前編寫過GraphQL服務器,很可能您已經(jīng)遇到了傳遞給解析器的info對象。幸運的是,在大多數(shù)情況下,您實際上并不需要了解它在查詢解析過程中的實際作用以及它的作用。
但是,有許多邊緣情況,其中info對象是導致許多混淆和誤解的原因。本文的目標是查看info對象的內容,并闡明它在GraphQL 執(zhí)行過程中的作用。
本文假設您已經(jīng)熟悉了如何解決GraphQL查詢和突變的基礎知識。如果您在這方面感到有點不穩(wěn)定,那么您一定要查看本系列的前幾篇文章:第一部分:GraphQL架構(中文)(必需)第二部分:網(wǎng)絡層(英文)(可選)
info對象的結構
回顧:GraphQL解析器的簽名
快速回顧一下,建立一個GraphQL服務器時GraphQL.js,你有兩個主要任務:
- 定義GraphQL架構(在SDL中或作為普通的JS對象)
- 對于模式中的每個字段,實現(xiàn)一個知道如何返回該字段值的解析器函數(shù)
解析器函數(shù)需要四個參數(shù)(按此順序):
-
parent:上一個解析器調用的結果(更多信息)。 -
args:解析器字段的參數(shù)。 -
context:每個解析程序可以讀取/寫入的自定義對象。 -
info:這就是我們將在本文中討論的內容。
info 包含查詢AST和更多執(zhí)行信息
關于info對象的結構和作用。官方規(guī)范和文檔都沒有提到它。曾經(jīng)有一個GitHub 問題需要更好的文檔,但是沒有明顯的行動就關閉了。因此,除了深入研究代碼之外別無他法。
在非常高的層次上,可以說info對象包含傳入的是GraphQL查詢的AST。由于這一點,解析器知道他們需要返回哪些字段。
要了解有關ASTs查詢的更多信息,請查看Christian Joudrey的精彩文章LifeQL of GraphQL Query - Lexing / Parsing以及Eric Baer的精彩演講GraphQL Under the Hood。
要了解其結構info,我們來看看它的Flow類型定義:
/* @flow */
export type GraphQLResolveInfo = {
fieldName: string;
fieldNodes: Array<FieldNode>;
returnType: GraphQLOutputType;
parentType: GraphQLCompositeType;
path: ResponsePath;
schema: GraphQLSchema;
fragments: { [fragmentName: string]: FragmentDefinitionNode };
rootValue: mixed;
operation: OperationDefinitionNode;
variableValues: { [variableName: string]: mixed };
};
以下是每個鍵的概述和快速說明:
-
fieldName:如前所述,GraphQL架構中的每個字段都需要由解析程序支持。該fieldName包含屬于當前的解決該域的名稱。 -
fieldNodes:一個數(shù)組,其中每個對象表示剩余選擇集中的字段。 -
returnType:響應字段的GraphQL類型。 -
parentType:此字段所屬的GraphQL類型。 -
path:跟蹤遍歷當前字段(即解析程序)的遍歷字段。 -
schema:GraphQLSchema表示可執(zhí)行schema的實例。 -
fragments:作為查詢文檔一部分的片段映射。 -
rootValue:rootValue傳遞給執(zhí)行的參數(shù)。 -
operation:整個查詢的AST 。 -
variableValues:與查詢一起提供的任何變量的映射對應于variableValues參數(shù)。
不要擔心,如果這仍然是抽象的,我們很快就會看到所有這些的例子。
具體的字段 vs Global
關于上面的鍵,有一個有趣的觀察結果。info對象上的鍵是 具體的字段 或 Global。
具體的字段 意味著該鍵的值取決于info對象傳遞到的字段(及其后備解析程序)。例子如下(fieldName,rootType和parentType):
type Query {
author: User!
feed: [Post!]!
}
在author中 fieldName就是author,而returnType就是 User!和parentType就是 Query。
而相對于feed這些價值當然會有所不同:fieldName是feed,returnType是[Post!]!,parentType也是Query。
因此,這三個鍵的值是取決于字段的。還有其他取決于字段的鍵是:fieldNodes和path。實際上,上面Flow定義的前五個鍵均是取決于字段的。
另一方面,Global意味著這些鍵的值不會改變 - 無論我們談論哪個解析器。schema,fragments,rootValue,operation并且variableValues將始終貫徹相同的值于所有解析器中。
一個簡單的例子
現(xiàn)在讓我們繼續(xù)看一下info對象內容的示例。要設置階段,這里是我們將用于此示例的schema definition
(模式定義):
type Query {
author(id: ID!): User!
feed: [Post!]!
}
type User {
id: ID!
username: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}
假設該schema的解析器實現(xiàn)如下:
const resolvers = {
Query: {
author: (root, { id }, context, info) => {
console.log(`Query.author - info: `, JSON.stringify(info))
return users.find(u => u.id === id)
},
feed: (root, args, context, info) => {
console.log(`Query.feed - info: `, JSON.stringify(info))
return posts
}
},
Post: {
title: (root, args, context, info) => {
console.log(`Post.title - info: `, JSON.stringify(info))
return root.title
},
},
}
請注意,
Post.title實際上并不需要解析器,我們仍然在此處包含它以查看info調用解析器時對象的結構。
現(xiàn)在考慮以下查詢:
query AuthorWithPosts {
author(id: "user-1") {
username
posts {
id
title
}
}
}
出于簡潔的目的,我們將僅討論該Query.author字段的解析器,而不是用于Post.title(在執(zhí)行上述查詢時仍然調用)的解析器。
如果您想要使用此示例,我們準備了一個存儲庫,其中包含上述架構的運行版本,因此您可以嘗試一些內容!
接下來,讓我們看看info對象內部的每個鍵,看看Query.author調用解析器時它們的樣子(你可以在這里找到info對象的整個日志輸出)。
fieldName 屬性
該fieldName其實就是是author。
fieldNodes 屬性
請記住,這fieldNodes是取決于字段的。它實際上包含查詢AST 的摘錄。此摘錄從當前字段(即author)開始,而不是從查詢的root開始。(從root開始的整個查詢AST存儲在operation,見下文)。
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "author",
"loc": { "start": 27, "end": 33 }
},
"arguments": [
{
"kind": "Argument",
"name": {
"kind": "Name",
"value": "id",
"loc": { "start": 34, "end": 36 }
},
"value": {
"kind": "StringValue",
"value": "user-1",
"block": false,
"loc": { "start": 38, "end": 46 }
},
"loc": { "start": 34, "end": 46 }
}
],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "username",
"loc": { "start": 54, "end": 62 }
},
"arguments": [],
"directives": [],
"loc": { "start": 54, "end": 62 }
},
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "posts",
"loc": { "start": 67, "end": 72 }
},
"arguments": [],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "id",
"loc": { "start": 81, "end": 83 }
},
"arguments": [],
"directives": [],
"loc": { "start": 81, "end": 83 }
},
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "title",
"loc": { "start": 90, "end": 95 }
},
"arguments": [],
"directives": [],
"loc": { "start": 90, "end": 95 }
}
],
"loc": { "start": 73, "end": 101 }
},
"loc": { "start": 67, "end": 101 }
}
],
"loc": { "start": 48, "end": 105 }
},
"loc": { "start": 27, "end": 105 }
}
]
}
returnType 屬性 & parentType 屬性
如前所述,returnType 與 parentType則相當簡單:
{
"returnType": "User!",
"parentType": "Query",
}
path 屬性
path即包含已經(jīng)走過,直到當前的一個域的路徑圖。如Query.author,它看起來像"path": { "key": "author" }。
{
"path": { "key": "author" }
}
為了比較,在Post.title解析器中,path結構如下:
{
"path": {
"prev": {
"prev": { "prev": { "key": "author" }, "key": "posts" },
"key": 0
},
"key": "title"
},
}
其余五個字段屬于
“global”類別,因此對于Post.title解析器而言將是相同的。
schema 屬性(可以理解為一種架構、一種規(guī)范、一種結構、一個表)
schema是對可執(zhí)行模式的引用。
fragments 屬性(查詢語句的片段)
fragments包含片段定義,因為查詢文檔沒有任何這些,它只是一個空映射:{}。
rootValue 屬性 (可以自定義一個屬性值作為首次解析的參數(shù))
如前所述,rootValue鍵的值對應于首先rootValue傳遞給graphql執(zhí)行函數(shù)的參數(shù)。在示例的情況下,它只是null。
operation 屬性
operation包含傳入查詢的完整查詢AST?;叵胍幌拢谄渌畔⒅?,它包含我們在fieldNodes上面看到的相同值:
{
"operation": {
"kind": "OperationDefinition",
"operation": "query",
"name": {
"kind": "Name",
"value": "AuthorWithPosts"
},
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "author"
},
"arguments": [
{
"kind": "Argument",
"name": {
"kind": "Name",
"value": "id"
},
"value": {
"kind": "StringValue",
"value": "user-1"
}
}
],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "username"
}
},
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "posts"
},
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "id"
}
},
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "title"
}
}
]
}
}
]
}
}
]
}
}
}
variableValues 屬性(查詢語句附帶的變量組)
此鍵表示已為查詢傳遞的所有變量。由于我們的示例中沒有變量,因此該值的值只是一個空映射:{}。
如果查詢是用變量編寫的:
query AuthorWithPosts($userId: ID!) {
author(id:$userId) {
username
posts {
id
title
}
}
}
該variableValues鍵會有以下值:
{
"variableValues": { "userId": "user-1" }
}
info使用GraphQL綁定時的作用
正如本文開頭所提到的,在大多數(shù)情況下,您根本不需要關心info對象。它只是你的解析器簽名的一部分,但你實際上并沒有將它用于干任何事情。那么,什么時候會變得相關?
傳遞info給綁定函數(shù)
如果您之前使用過GraphQL bindings,那么您已將該info對象視為生成的綁定函數(shù)的一部分。請考慮以下架構:
type Query {
users(): [User]!
user(id: ID!): User
}
type Mutation {
createUser(username: String!): User!
deleteUser(id: ID!!): User
}
type User {
id: ID!
username: String!
}
使用graphql-binding,您現(xiàn)在可以通過調用專用綁定函數(shù)而不是發(fā)送原始 queries 和 mutations 來發(fā)送可用的查詢和突變。
例如,考慮以下原始查詢,檢索特定的User:
query {
user(id: "user-100") {
id
username
}
}
使用綁定功能實現(xiàn)相同的功能如下(這可能是Prisma.js里的內容):
binding.query.user({ id: 'user-100' }, null, '{ id username }')
通過在user綁定實例上調用函數(shù)并傳遞相應的參數(shù),我們傳達的信息與上面的原始GraphQL查詢完全相同。
綁定函數(shù)graphql-binding有三個參數(shù):
-
args:包含字段的參數(shù)(例如,上面username的createUser變異)。 -
context:context傳遞給解析器鏈的對象。 -
info:info對象。請注意,GraphQL ResolveInfo您還可以傳遞一個簡單定義選擇集的字符串,而不是(Info類型)的實例。
使用Prisma(Prisma.js一個graphql框架)將應用程序Schema映射到數(shù)據(jù)庫Schema
注:以下內容可能比較抽象或者你可以認為沒多大用這里我簡單表達下我對下面內容的理解(我們想要知道客戶端需要得到的字段的最佳方案就是通過解析
info對象里的fileNodes屬性得到)
info對象可能引起混淆的另一個常見用例是基于Prisma和prisma綁定的GraphQL服務器的實現(xiàn)。
在這種情況下,我們的想法是有兩個GraphQL層:
- Database層 是由Prisma自動生成,并提供了一個通用和強大CRUD API
- Application層 定義了暴露給客戶端應用程序并根據(jù)您的應用程序需求量身定制的GraphQL API
作為后端開發(fā)人員,您負責定義應用 程序層的應用程序架構并實現(xiàn)其解析器。由于prisma-binding,解析器的實現(xiàn)僅僅是將傳入的查詢委托 給底層數(shù)據(jù)庫API而沒有大的開銷的過程。
讓我們考慮一個簡單的例子 - 假設你開始使用以下Prisma數(shù)據(jù)庫服務的數(shù)據(jù)模型:
type Post {
id: ID! @unique
title: String!
author: User!
}
type User {
id: ID! @uniqe
name: String!
posts: [Post!]!
}
Prisma基于此數(shù)據(jù)模型生成的數(shù)據(jù)庫模式類似于:
type Query {
posts(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Post]!
postsConnection(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): PostConnection!
post(where: PostWhereUniqueInput!): Post
users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
user(where: UserWhereUniqueInput!): User
}
type Mutation {
createPost(data: PostCreateInput!): Post!
updatePost(data: PostUpdateInput!, where: PostWhereUniqueInput!): Post
deletePost(where: PostWhereUniqueInput!): Post
createUser(data: UserCreateInput!): User!
updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
deleteUser(where: UserWhereUniqueInput!): User
}
現(xiàn)在,假設您要構建一個類似于此的應用程序模式:
type Query {
feed(authorId: ID): Feed!
}
type Feed {
posts: [Post!]!
count: Int!
}
該feed查詢不僅返回一個列表Post元素,而且能夠返回count列表。請注意,它可以選擇authorId過濾Feed,以僅返回Post由特定內容寫入的元素User。
實現(xiàn)此應用程序模式的第一個直覺可能如下所示。
實施1:這種實現(xiàn)看起來正確但有一個微妙的缺陷:
const resolvers = {
Query: {
async feed(parent, { authorId }, ctx, info) {
// build filter
const authorFilter = authorId ? { author: { id: authorId } } : {}
// retrieve (potentially filtered) posts
const posts = await ctx.db.query.posts({ where: authorFilter })
// retrieve (potentially filtered) element count
const postsConnection = await ctx.db.query.postsConnection(
{ where: authorFilter },
`{ aggregate { count } }`,
)
return {
count: postsConnection.aggregate.count,
posts: posts,
}
},
},
}
這種實現(xiàn)似乎足夠合理。在feed解析器內部,我們正在構建authorFilter基于潛在的傳入authorId。該authorFilter則用來執(zhí)行posts查詢和檢索Post元素,還有postsConnection它可以訪問查詢count列表。
也可以僅使用postsConnection查詢來檢索實際的Post元素。為了簡單起見,我們仍然使用
Post查詢,并將另一種方法作為練習給細心的讀者。
實際上,在使用此實現(xiàn)啟動GraphQL服務器時,事情看起來似乎很好。您會注意到正確提供了簡單查詢,例如以下查詢將成功:
query {
feed(authorId: "cjdbbsepg0wp70144svbwqmtt") {
count
posts {
id
title
}
}
}
它不是直到你想獲取author的的Post,當你運行到一個問題內容:
query {
feed(authorId: "cjdbbsepg0wp70144svbwqmtt") {
count
posts {
id
title
author {
id
name
}
}
}
}
行!因此,由于某種原因,實現(xiàn)不會返回,author并且會觸發(fā)錯誤“無法為不可為空的Post.author返回null”。因為該Post.author字段在應用程序架構中標記為必需。
讓我們再看看實現(xiàn)的相關部分:
// retrieve (potentially filtered) posts
const posts = await ctx.db.query.posts({ where: authorFilter })
這是我們檢索Post元素的地方。但是,我們沒有將選擇集傳遞給Post綁定功能。如果沒有第二個參數(shù)傳遞給Prisma綁定函數(shù),則默認行為是查詢該類型的所有標量字段。
這確實解釋了這種行為。調用ctx.db.query.posts返回正確的Post元素集,但只返回它們id和title值 - 沒有關于author的關系數(shù)據(jù)。
那么,我們該如何解決這個問題呢?顯然需要一種方法來告訴posts綁定函數(shù)它需要返回哪些字段。但是這些信息在feed解析器的上下文中存在于何處?你能猜到嗎?
沒錯:在info對象內!因為對于一個Prisma的綁定功能的第二個參數(shù)可以是一個字符串或一個info對象,我們只是通過info它獲取傳遞到目標feed分解到posts綁定功能。
此查詢失敗,執(zhí)行2:sub selection類型的Post 字段必須有子集。
const resolvers = {
Query: {
async feed(parent, { authorId }, ctx, info) {
// build filter
const authorFilter = authorId ? { author: { id: authorId } } : {}
// retrieve (potentially filtered) posts
const posts = await ctx.db.query.posts({ where: authorFilter }, info) // pass `info`
// retrieve (potentially filtered) element count
const postsConnection = await ctx.db.query.postsConnection(
{ where: authorFilter },
`{ aggregate { count } }`,
)
return {
count: postsConnection.aggregate.count,
posts: posts,
}
},
},
}
然而,這并不能完全正確實現(xiàn)。例如,請考慮以下查詢:
query {
feed {
count
posts {
title
}
}
}
sub selection類型的錯誤消息Post 字段必須有子選擇?!?由上述實現(xiàn)的第8行 產(chǎn)生。
那么,這里發(fā)生了什么?之所以失敗是因為對象中的特定字段鍵info與posts查詢不匹配。
我們在feed解析器內打印info對象可以更好地了解情況。我們只考慮以下領域的具體信息fieldNodes:
{
"fieldNodes": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "feed"
},
"arguments": [],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "count"
},
"arguments": [],
"directives": []
},
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "posts"
},
"arguments": [],
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "title"
},
"arguments": [],
"directives": []
}
]
}
}
]
}
}
]
}
此JSON對象也可以表示為字符串選擇集:
{
feed {
count
posts {
title
}
}
}
現(xiàn)在一切都有道理!我們將上述選擇集發(fā)送到postsPrisma數(shù)據(jù)庫模式的查詢,當然這些模式不知道feed和count字段。不可否認,所產(chǎn)生的錯誤信息并非超級有用,但至少我們了解現(xiàn)在正在發(fā)生的事情。
那么,這個問題的解決方案是什么?解決此問題的一種方法是手動解析選擇集的正確部分fieldNodes并將其傳遞給posts綁定函數(shù)(例如,作為字符串)。
但是,對于這個問題我們有一個更優(yōu)雅的解決方案,那就是為應用程序模式中的feed類型實現(xiàn)專用的解析器。下面是正確實現(xiàn)的例子
實施3:該實現(xiàn)解決了上述問題
const resolvers = {
Query: {
async feed(parent, { authorId }, ctx, info) {
// build filter
const authorFilter = authorId ? { author: { id: authorId } } : {}
// retrieve (potentially filtered) posts
const posts = await ctx.db.query.posts({ where: authorFilter }, `{ id }`) // second argument can also be omitted
// retrieve (potentially filtered) element count
const postsConnection = await ctx.db.query.postsConnection(
{ where: authorFilter },
`{ aggregate { count } }`,
)
return {
count: postsConnection.aggregate.count,
postIds: posts.map(post => post.id), // only pass the `postIds` down to the `Feed.posts` resolver
}
},
},
Feed: {
posts({ postIds }, args, ctx, info) {
const postIdsFilter = { id_in: postIds }
return ctx.db.query.posts({ where: postIdsFilter }, info)
},
},
}
此實現(xiàn)修復了上面討論的所有問題。有幾點需要注意:
- 在第8行中,我們現(xiàn)在傳遞一個字符串選擇set(
{ id })作為第二個參數(shù)。這只是為了提高效率,否則所有的標量值都會被提取(這在我們的例子中不會產(chǎn)生很大的不同),我們只需要ID。 - 我們返回的只是一個ID數(shù)組(表示為字符串),而不是
posts從Query.feed解析器返回postIds。 - 在
Feed.posts解析器中,我們現(xiàn)在可以訪問父解析器postIds返回的內容。這次,我們可以使用傳入的對象,并將其簡單地傳遞給綁定函數(shù)。info posts
如果您想要使用此示例,可以查看此存儲庫,其中包含上述示例的運行版本。請隨意嘗試本文中提到的不同實現(xiàn),并親自觀察行為!
摘要
在本文中,您深入了解了info在實現(xiàn)基于GraphQL.js的GraphQL API時使用的對象。
該info對象未正式記錄 - 要了解有關它的更多信息,您需要深入研究代碼。在本教程中,我們首先概述其內部結構并了解其在GraphQL解析器函數(shù)中的作用。然后,我們介紹了一些邊緣情況和潛在的陷阱,需要更深入的了解info。
本文中顯示的所有代碼都可以在相應的GitHub存儲庫中找到,這樣您就可以自己試驗和觀察info對象的行為。