ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 解析器 解析器提供将 `GraphQL` 操作(查询、突变或订阅)转换为数据的说明。 它们返回我们在模式中指定的相同形状的数据——同步或作为解析为该形状结果的承诺。 通常,您手动创建解析器映射。 另一方面,`@nestjs/graphql` 包使用用于注释类的装饰器提供的元数据自动生成解析器映射。 为了演示使用包功能创建 `GraphQL API` 的过程,我们将创建一个简单的作者 API。 ### ** 代码优先 ** 在代码优先的方法中,我们不遵循通过手动编写 `GraphQL SDL` 创建 `GraphQL` 模式的典型过程。 相反,我们使用 `TypeScript` 装饰器从 `TypeScript `类定义生成 SDL。 `@nestjs/graphql` 包读取通过装饰器定义的元数据并自动为您生成模式。 ### 对象类型[#](#object-types) GraphQL 模式中的大多数定义都是**对象类型**。您定义的每个对象类型都应该代表一个应用程序客户端可能需要与之交互的域对象。例如,我们的示例 API 需要能够获取作者列表及其帖子,因此我们应该定义`Author`类型和`Post`类型以支持此功能。 如果我们使用模式优先的方法,我们将使用 SDL 定义这样的模式,如下所示: ~~~graphql type Author { id: Int! firstName: String lastName: String posts: [Post!]! } ~~~ 在代码优先方式中,我们不必手动编写SDL。相反,我们只需使用装饰器。 ```typescript import { Field, Int, ObjectType } from 'type-graphql'; import { Post } from './post'; @ObjectType() export class Author { @Field(type => Int) id: number; @Field({ nullable: true }) firstName?: string; @Field({ nullable: true }) lastName?: string; @Field(type => [Post]) posts: Post[]; } ``` >TypeScript 的元数据反射系统有几个限制,例如,无法确定类包含哪些属性或识别给定属性是可选的还是必需的。 由于这些限制,我们必须在架构定义类中显式使用 `@Field() `装饰器来提供有关每个字段的 GraphQL 类型和可选性的元数据,或者使用 [CLI 插件](https://docs.nestjs.com/graphql/cli-plugin)为我们生成这些。 Author 对象类型与任何类一样,由一组字段组成,每个字段都声明一个类型。 字段的类型对应于 GraphQL 类型。 字段的 GraphQL 类型可以是其他对象类型或标量类型。 GraphQL 标量类型是解析为单个值的原语(如 `ID`、`String`、`Boolean` 或 `Int`)。 >除了 GraphQL 的内置标量类型之外,您还可以定义自定义标量类型([阅读更多](https://docs.nestjs.com/graphql/scalars))。 上面的 Author 对象类型定义将导致 Nest 生成我们上面展示的 SDL: ~~~graphql type Author { id: Int! firstName: String lastName: String posts: [Post!]! } ~~~ `@Field() `装饰器接受可选的类型函数(例如,type => Int)和可选的选项对象。 当 TypeScript 类型系统和 GraphQL 类型系统之间可能存在歧义时,需要 type 函数。 具体来说:字符串和布尔类型不需要; 它是数字所必需的(必须映射到 GraphQL Int 或 Float)。 type 函数应该简单地返回所需的 GraphQL 类型(如这些章节中的各种示例所示)。 选项对象可以具有以下任何键/值对: * `nullable`: 用于指定一个字段是否可以为空(在 SDL 中,每个字段默认都是不可为空的);`boolean` * `description`:用于设置字段描述;`string` * `deprecationReason`:用于将字段标记为已弃用;`string` 例如: ~~~typescript @Field({ description: `Book title`, deprecationReason: 'Not useful in v2 schema' }) title: string; ~~~ >您还可以为整个对象类型添加描述或弃用:`@ObjectType({ description: 'Author model' })`. 当字段为数组时,我们必须在`Field()`装饰器的type函数中手动指明数组类型,如下图: ~~~typescript @Field(type => [Post]) posts: Post[]; ~~~ > **提示**使用数组括号表示法 (`[ ]`),我们可以指示数组的深度。例如, 使用`[[Int]]`将表示一个整数矩阵。 要声明数组的项(不是数组本身)可以为空,请将`nullable`属性设置`'items'`为如下所示: ~~~typescript @Field(type => [Post], { nullable: 'items' }) posts: Post[]; ~~~ > **提示**如果数组及其项都可以为空,则改为设置`nullable`为`'itemsAndList'`。 Author 模型已创建。现在,让我们创建缺少的 Post 类。 ```typescript import { Field, Int, ObjectType } from 'type-graphql'; @ObjectType() export class Post { @Field(type => Int) id: number; @Field() title: string; @Field(type => Int, { nullable: true }) votes?: number; } ``` Post 对象类型将导致在 SDL 中生成 GraphQL 模式的以下部分: ~~~graphql type Post { id: Int! title: String! votes: Int } ~~~ ####代码优先解析器[#](#code-first-resolver) 至此,我们已经定义了可以存在于数据图中的对象(类型定义),但是客户端还没有办法与这些对象进行交互。为了解决这个问题,我们需要创建一个解析器类。在代码优先方法中,解析器类既定义解析器函数**又**生成**查询类型**。当我们通过以下示例进行操作时,这一点将很清楚: ```typescript @Resolver(of => Author) export class AuthorResolver { constructor( private readonly authorsService: AuthorsService, private readonly postsService: PostsService, ) {} @Query(returns => Author) async author(@Args({ name: 'id', type: () => Int }) id: number) { return await this.authorsService.findOneById(id); } @ResolveProperty() async posts(@Parent() author) { const { id } = author; return await this.postsService.findAll({ authorId: id }); } } ``` >所有装饰器(例如,`@Resolver`、`@ResolveField`、`@Args` 等)都从 `@nestjs/graphql` 包中导出。 您可以定义多个解析器类。Nest 将在运行时组合这些。有关代码组织的更多信息,请参阅下面的[模块部分。](https://docs.nestjs.com/graphql/resolvers#module) >`AuthorsService `和 `PostsService` 类中的逻辑可以根据需要简单或复杂。 这个例子的主要目的是展示如何构建解析器以及它们如何与其他提供者交互。 在上面的示例中,我们创建了 `AuthorsResolver`,它定义了一个查询解析器函数和一个字段解析器函数。为了创建解析器,我们创建了一个使用解析器函数作为方法的类,并使用 `@Resolver()` 装饰器注释该类。 在此示例中,我们定义了一个查询处理程序,以根据请求中发送的 `id` 获取作者对象。要指定该方法是一个查询处理程序,请使用 `@Query() `装饰器。 传递给 `@Resolver()` 装饰器的参数是可选的,但当我们的图形变得不平凡时就会发挥作用。它用于提供字段解析器函数在遍历对象图时使用的父对象。 在我们的示例中,由于该类包含一个字段解析器函数(对于 `Author` 对象类型的 posts 属性),我们必须为 `@Resolver()` 装饰器提供一个值来指示哪个类是父类型(即对应的 `ObjectType`类名)用于此类中定义的所有字段解析器。从示例中应该可以清楚地看出,在编写字段解析器函数时,有必要访问父对象(被解析的字段所属的对象)。在此示例中,我们使用字段解析器填充作者的帖子数组,该字段解析器调用以作者 ID 作为参数的服务。因此需要在 `@Resolver()`装饰器中识别父对象。注意 `@Parent() `方法参数装饰器的相应使用,然后在字段解析器中提取对该父对象的引用。 我们可以定义多个 `@Query()` 解析器函数(在此类中以及在任何其他解析器类中),它们将与解析器映射中的相应条目一起在生成的 SDL 中聚合到单个 Query 类型定义中。这允许您定义接近他们使用的模型和服务的查询,并在模块中保持良好的组织。 >Nest CLI 提供了一个生成器(原理图),它会自动生成所有样板代码,以帮助我们避免所有这些,并使开发人员的体验更加简单。 在[此处](https://docs.nestjs.com/recipes/crud-generator)阅读有关此功能的更多信息。 ### 查询类型名称[#](#query-type-names) 在上面的示例中,`@Query()`装饰器根据方法名称生成 GraphQL 模式查询类型名称。例如,考虑上面示例中的以下构造: ~~~typescript @Query(returns => Author) async author(@Args('id', { type: () => Int }) id: number) { return this.authorsService.findOneById(id); } ~~~ 这会在我们的架构中为作者查询生成以下条目(查询类型使用与方法名称相同的名称): ~~~graphql type Query { author(id: Int!): Author } ~~~ > **提示**[在此处](https://graphql.org/learn/queries/)了解有关 GraphQL 查询的更多信息。 按照惯例,我们更喜欢将这些名称解耦;例如,我们更喜欢`getAuthor()`为我们的查询处理方法使用一个名称,但仍然使用`author`我们的查询类型名称。这同样适用于我们的字段解析器。我们可以通过将映射名称作为装饰器`@Query()`和`@ResolveField()`装饰器的参数传递来轻松做到这一点,如下所示: ```typescript @Resolver(of => Author) export class AuthorResolver { constructor( private readonly authorsService: AuthorsService, private readonly postsService: PostsService, ) {} @Query(returns => Author, { name: 'author' }) async getAuthor(@Args({ name: 'id', type: () => Int }) id: number) { return await this.authorsService.findOneById(id); } @ResolveProperty('posts') async getPosts(@Parent() author) { const { id } = author; return await this.postsService.findAll({ authorId: id }); } } ``` 上面的`getAuthor`处理程序方法将导致在 SDL 中生成 GraphQL 模式的以下部分: ~~~graphql type Query { author(id: Int!): Author } ~~~ ### 查询装饰器选项[#](#query-decorator-options) `@Query()` 装饰器的选项对象(我们在上面传递 `{name: 'author'} )`接受许多键/值对: - name:查询的名称; 一个字符串 - description:将用于生成 GraphQL 模式文档的描述(例如,在 GraphQL 游乐场中); 一个字符串 - deprecationReason:设置查询元数据以将查询显示为已弃用(例如,在 GraphQL 游乐场中); 一个字符串 - nullable:查询是否可以返回空数据响应; boolean 或 'items' 或 'itemsAndList' (有关 'items' 和 'itemsAndList' 的详细信息,请参见上文) #### args 装饰器选项[#](#args-decorator-options) 使用`@Args()`装饰器从请求中提取参数以在方法处理程序中使用。[这与REST 路由参数参数提取](https://docs.nestjs.com/controllers#route-parameters)非常相似。 通常你的`@Args()`装饰器会很简单,并且不需要像`getAuthor()`上面方法中看到的对象参数。例如,如果标识符的类型是字符串,则以下构造就足够了,并且只需从入站 GraphQL 请求中提取命名字段以用作方法参数。 ```typescript @Args('id') id: string ``` 在 getAuthor() 的情况下,使用数字类型,这是一个挑战。 数字 `TypeScript` 类型没有为我们提供有关预期 GraphQL 表示的足够信息(例如,`Int` 与 `Float`)。 因此我们必须显式地传递类型引用。 我们通过将第二个参数传递给 `Args() `装饰器来做到这一点,其中包含参数选项,如下所示: ~~~typescript @Query(returns => Author, { name: 'author' }) async getAuthor(@Args('id', { type: () => Int }) id: number) { return this.authorsService.findOneById(id); } ~~~ options 对象允许我们指定以下可选的键值对: - `type`: 返回 GraphQL 类型的函数 - `defaultValue`:默认值,`any`类型 - `description`: 描述元数据 ;`string` - `deprecationReason`: 弃用字段并提供描述原因的元数据;`string` - `nullable`:字段是否可以为空 查询处理程序方法可以采用多个参数。 假设我们想根据名字和姓氏来获取作者。 在这种情况下,我们可以调用`@Args` 两次: ~~~typescript getAuthor( @Args('firstName', { nullable: true }) firstName?: string, @Args('lastName', { defaultValue: '' }) lastName?: string, ) {} ~~~ ### 专用参数类[#](#dedicated-arguments-class) 使用内联`@Args()`调用,上述示例的代码会变得臃肿。相反,您可以创建一个专用的`GetAuthorArgs`参数类并在处理程序方法中访问它,如下所示: ```typescript @Args() id: AuthorArgs ``` 使用 `@ArgsType()` 创建 `GetAuthorArgs` 类,如下所示: >authors/dto/get-author.args.ts ~~~typescript import { MinLength } from 'class-validator'; import { Field, ArgsType } from '@nestjs/graphql'; @ArgsType() class GetAuthorArgs { @Field({ nullable: true }) firstName?: string; @Field({ defaultValue: '' }) @MinLength(3) lastName: string; } ~~~ >同样,由于 TypeScript 的元数据反射系统限制,需要使用 `@Field `装饰器手动指示类型和可选性,或者使用 CLI 插件。 这将导致在 SDL 中生成 GraphQL 架构的以下部分: ~~~graphql type Query { author(firstName: String, lastName: String = ''): Author } ~~~ >请注意,像 `GetAuthorArgs` 这样的参数类与 `ValidationPipe` 配合得很好([阅读更多](https://docs.nestjs.com/techniques/validation))。 ### 类继承 您可以使用标准 TypeScript 类继承来创建具有可扩展的通用实用程序类型功能(字段和字段属性、验证等)的基类。 例如,您可能有一组与分页相关的参数,这些参数始终包括标准偏移量和限制字段,还包括其他特定于类型的索引字段。 您可以设置类层次结构,如下所示。 `@ArgsType()` 基类: ~~~typescript @ArgsType() class PaginationArgs { @Field((type) => Int) offset: number = 0; @Field((type) => Int) limit: number = 10; } ~~~ 基类 `@ArgsType() `的类型特定子类: ~~~typescript @ArgsType() class GetAuthorArgs extends PaginationArgs { @Field({ nullable: true }) firstName?: string; @Field({ defaultValue: '' }) @MinLength(3) lastName: string; } ~~~ 可以对`@ObjectType()`对象采取相同的方法。 在基类上定义通用属性: ~~~typescript @ObjectType() class Character { @Field((type) => Int) id: number; @Field() name: string; } ~~~ 在子类上添加特定于类型的属性: ~~~typescript @ObjectType() class Warrior extends Character { @Field() level: number; } ~~~ 您也可以将继承与解析器一起使用。 您可以通过结合继承和 TypeScript 泛型来确保类型安全。 例如,要使用通用 `findAll` 查询创建基类,请使用如下结构: ~~~typescript function BaseResolver<T extends Type<unknown>>(classRef: T): any { @Resolver({ isAbstract: true }) abstract class BaseResolverHost { @Query((type) => [classRef], { name: `findAll${classRef.name}` }) async findAll(): Promise<T[]> { return []; } } return BaseResolverHost; } ~~~ 请注意以下事项: - 需要一个显式的返回类型(以上任何一种):否则 TypeScript 会抱怨使用了私有类定义。 推荐:定义一个接口而不是使用`any`。 - `Type`是从 `@nestjs/common` 包中导入的 - `isAbstract: true `属性指示不应为此类生成 SDL(架构定义语言语句)。 请注意,您也可以为其他类型设置此属性以抑制 SDL 生成。 以下是生成 BaseResolver 的具体子类的方法: ~~~typescript @Resolver((of) => Recipe) export class RecipesResolver extends BaseResolver(Recipe) { constructor(private recipesService: RecipesService) { super(); } } ~~~ 此构造将生成以下 SDL: ~~~graphql type Query { findAllRecipe: [Recipe!]! } ~~~ ### 泛型 我们在上面看到了泛型的一种用法。 这个强大的 TypeScript 功能可用于创建有用的抽象。 例如,这是一个基于此文档的基于光标的分页实现示例: ~~~typescript import { Field, ObjectType, Int } from '@nestjs/graphql'; import { Type } from '@nestjs/common'; interface IEdgeType<T> { cursor: string; node: T; } export interface IPaginatedType<T> { edges: IEdgeType<T>[]; nodes: T[]; totalCount: number; hasNextPage: boolean; } export function Paginated<T>(classRef: Type<T>): Type<IPaginatedType<T>> { @ObjectType(`${classRef.name}Edge`) abstract class EdgeType { @Field((type) => String) cursor: string; @Field((type) => classRef) node: T; } @ObjectType({ isAbstract: true }) abstract class PaginatedType implements IPaginatedType<T> { @Field((type) => [EdgeType], { nullable: true }) edges: EdgeType[]; @Field((type) => [classRef], { nullable: true }) nodes: T[]; @Field((type) => Int) totalCount: number; @Field() hasNextPage: boolean; } return PaginatedType as Type<IPaginatedType<T>>; } ~~~ 定义了上述基类后,我们现在可以轻松创建继承此行为的专用类型。 例如: ~~~typescript @ObjectType() class PaginatedAuthor extends Paginated(Author) {} ~~~ #### ** 架构优先 ** 正如提到[以前的章节](https://docs.nestjs.com/graphql/quick-start),让我们在 SDL 中定义我们的类型(阅读[更多](http://graphql.cn/learn/schema/#type-language)): >为方便本章,我们将所有 SDL 聚合在一个位置(例如,一个 `.graphql` 文件,如下所示)。 在实践中,您可能会发现以模块化方式组织代码是合适的。 例如,在该实体的专用目录中创建具有表示每个域实体的类型定义的单个 SDL 文件以及相关服务、解析器代码和 Nest 模块定义类会很有帮助。 Nest 将在运行时聚合所有单独的模式类型定义。 ```graphql type Author { id: Int! firstName: String lastName: String posts: [Post] } type Post { id: Int! title: String! votes: Int } type Query { author(id: Int!): Author } ``` 架构优先解析器[#](#schema-first-resolver) 我们的 GraphQL 架构包含公开的单个查询 `author(id: Int!): Author` 。 > [在此处](https://graphql.org/learn/queries/)了解有关 `GraphQL` 查询的更多信息。 现在,让我们创建一个 `AuthorResolver` 。 ```graphql @Resolver('Author') export class AuthorResolver { constructor( private readonly authorsService: AuthorsService, private readonly postsService: PostsService, ) {} @Query() async author(@Args('id') id: number) { return await this.authorsService.findOneById(id); } @ResolveProperty() async posts(@Parent() author) { const { id } = author; return await this.postsService.findAll({ authorId: id }); } } ``` >所有装饰器(例如,`@Resolver`、`@ResolveField`、`@Args` 等)都从 `@nestjs/graphql` 包中导出。 > 提示:使用 `@Resolver()` 装饰器则不必将类标记为 `@Injectable()` ,否则必须这么做。 `@Resolver()` 装饰器不影响查询和对象变动 (`@Query()` 和 `@Mutation()` 装饰器)。这只会通知 Nest, 每个 `@ResolveProperty()` 有一个父节点, `Author` 在这种情况下是父节点, Author在这种情况下是一个类型(Author.posts 关系)。基本上,不是为类设置 @Resolver() ,而是为函数: ```typescript @Resolver('Author') @ResolveProperty() async posts(@Parent() author) { const { id } = author; return await this.postsService.findAll({ authorId: id }); } ``` 但当 @ResolveProperty() 在一个类中有多个,则必须为所有的都添加 @Resolver(),这不是一个好习惯(额外的开销)。 > 任何传递给 @Resolver() 的类名参数都不会影响查询(`@Query()` 装饰器)或突变(`@Mutation()` 装饰器)。 >代码优先方法不支持在方法级别使用 `@Resolver `装饰器。 在上面的示例中,`@Query()` 和 `@ResolveField() `装饰器根据方法名称与 `GraphQL` 模式类型相关联。 例如,考虑上面示例中的以下构造: ~~~typescript @Query() async author(@Args('id') id: number) { return this.authorsService.findOneById(id); } ~~~ 这会在我们的架构中为作者查询生成以下条目(查询类型使用与方法名称相同的名称): ~~~graphql type Query { author(id: Int!): Author } ~~~ 按照惯例,我们更愿意将它们解耦,使用诸如 getAuthor() 或 getPosts() 之类的名称作为解析器方法。 我们可以通过将映射名称作为参数传递给装饰器来轻松做到这一点,如下所示: ```typescript @Resolver('Author') export class AuthorResolver { constructor( private readonly authorsService: AuthorsService, private readonly postsService: PostsService, ) {} @Query('author') async getAuthor(@Args('id') id: number) { return await this.authorsService.findOneById(id); } @ResolveProperty('posts') async getPosts(@Parent() author) { const { id } = author; return await this.postsService.findAll({ authorId: id }); } } ``` > 这个 `@Resolver()` 装饰器可以在函数级别被使用。 > Nest CLI 提供了一个生成器(原理图),它会自动生成所有样板代码,以帮助我们避免所有这些,并使开发人员的体验更加简单。 在[此处](https://docs.nestjs.com/recipes/crud-generator)阅读有关此功能的更多信息。 ### 类型生产(Typings) 假设我们使用模式优先的方法并启用了类型生成功能(使用 `outputAs: 'class' `如[上一章](https://docs.nestjs.com/graphql/quick-start)所示),一旦您运行应用程序,它将生成以下文件(在您在 `GraphQLModule` 中指定的位置 `.forRoot()` 方法)。 例如,在 `src/graphql.ts` 中: ```typescript export class Author { id: number; firstName?: string; lastName?: string; posts?: Post[]; } export class Post { id: number; title: string; votes?: number; } export abstract class IQuery { abstract author(id: number): Author | Promise<Author>; } ``` 通过生成类(而不是生成接口的默认技术),您可以将声明性验证装饰器与模式优先方法结合使用,这是一种非常有用的技术([阅读更多](https://docs.nestjs.com/techniques/validation))。 例如,您可以将类验证器装饰器添加到生成的 `CreatePostInput` 类,如下所示,以在标题字段上强制最小和最大字符串长度: ```typescript import { MinLength, MaxLength } from 'class-validator'; export class CreatePostInput { @MinLength(3) @MaxLength(50) title: string; } ``` >要启用输入(和参数)的自动验证,请使用 `ValidationPipe`。 在[此处](https://docs.nestjs.com/techniques/validation)阅读有关验证的更多信息,更具体地在[此处](https://docs.nestjs.com/pipes)阅读有关管道的信息。 尽管如此,如果将装饰器直接添加到自动生成的文件中,它们将在每次连续更改时被丢弃。因此,您应该创建一个单独的文件,并简单地扩展生成的类。 ```typescript import { MinLength, MaxLength } from 'class-validator'; import { Post } from '../../graphql.ts'; export class CreatePostInput extends Post { @MinLength(3) @MaxLength(50) title: string; } ``` ### GraphQL 参数装饰器[#](#graphql-argument-decorators) 在上面的示例中,您可能会注意到我们使用专用装饰器来引用以下参数。下面是提供的装饰器和它们代表的普通 Apollo 参数的比较。 ||| |---|---| | `@Root()` 和 `@Parent()` | `root`/`parent` | | `@Context(param?:string)` | `context`/`context[param]` | | `@Info(param?:string)` | `info`/`info[param]` | | `@Args(param?:string)` | `args`/`args[param]` | 这些参数具有以下含义: * `root`: 一个对象,包含从父字段的解析器返回的结果,或者在顶级`Query`字段的情况下,`rootValue`从服务器配置传递。 * `context`:在特定查询中由所有解析器共享的对象;通常用于包含每个请求的状态。 * `info`:一个对象,其中包含有关查询执行状态的信息。 * `args`: 一个对象,其参数传递到查询中的字段中。 ### 模块 完成上述步骤后,我们已经以声明方式指定了 `GraphQLModule` 生成解析器映射所需的所有信息。 `GraphQLModule` 使用反射来内省通过装饰器提供的元数据,并自动将类转换为正确的解析器映射。 您需要注意的唯一另一件事是提供(即,作为某个模块中的提供者列出)解析器类(`AuthorsResolver`),并在某处导入模块(`AuthorsModule`),因此Nest 将能够利用 它。 例如,我们可以在 `AuthorsModule` 中执行此操作,该模块还可以提供此上下文所需的其他服务。 确保在某处导入 `AuthorsModule`(例如,在根模块中,或由根模块导入的其他模块中)。 ```typescript @Module({ imports: [PostsModule], providers: [AuthorsService, AuthorResolver], }) export class AuthorsModule {} ``` 该 `GraphQLModule` 会考虑反映了元数据和转化类到正确的解析器的自动映射。您应该注意的是您需要在某处 import 此模块,Nest 才会知道 `AuthorsModule` 确实存在。 > 通过所谓的域模型来组织代码会很有帮助(类似于在 `REST API `中组织入口点的方式)。 在这种方法中,将您的模型(`ObjectType` 类)、解析器和服务放在代表领域模型的 Nest 模块中。 将所有这些组件保存在每个模块的单个文件夹中。 当您执行此操作并使用 Nest CLI 生成每个元素时,Nest 会自动为您将所有这些部分连接在一起(在适当的文件夹中定位文件,在提供程序中生成条目并导入数组等)。 > 提示:在[此处](http://graphql.cn/learn/queries/)了解有关 GraphQL 查询的更多信息。