自定义 GraphQL Schema
Gatsby 的主要优势之一是能够以统一的方式使用 GraphQL 查询来自各种数据源的数据。为此,必须生成一个定义数据形状的 GraphQL Schema。
Gatsby 能够从您的数据中自动推断 GraphQL Schema,在许多情况下,这已经足够了。然而,有些情况下您可能希望显式定义数据形状,或者为查询层添加自定义功能 - 这就是 Gatsby 的 Schema 自定义 API 提供的功能。
以下指南将通过一些示例来展示该 API。
本指南面向插件作者、尝试修复由自动类型推断创建的 GraphQL Schema 的用户、优化大型网站构建的开发人员,以及任何对自定义 Gatsby 的 Schema 生成感兴趣的人。因此,本指南假设您对 GraphQL 类型和使用 Gatsby 的 Node API 有一定的了解。有关使用 Gatsby 和 GraphQL 的更高级方法,请参阅 API 参考。
显式定义数据类型
示例项目是一个博客,它从本地 Markdown 文件获取数据,这些文件提供帖子的内容以及 JSON 格式的作者信息。偶尔也有嘉宾贡献者,他们的信息保存在单独的 JSON 文件中。
为了能够用 GraphQL 查询这些文件中的内容,它们需要首先被加载到 Gatsby 的内部数据存储中。这就是 source 和 transformer 插件所做的 - 在本例中是 gatsby-source-filesystem 和 gatsby-transformer-remark 以及 gatsby-transformer-json。每个 markdown 帖子文件由此被转换为内部数据存储中的一个“节点”对象,该对象具有唯一的 id 和 MarkdownRemark 类型。类似地,作者将由 AuthorJson 类型的节点对象表示,贡献者信息将转换为 ContributorJson 类型的节点对象。
Node 接口
Gatsby 的 GraphQL Schema 使用 Node 接口表示此数据结构,该接口描述了由 source 和 transformer 插件创建的节点对象共有的字段集(id、parent、children,以及几个 internal 字段,如 type)。在 GraphQL Schema 定义语言 (SDL) 中,它看起来像这样:
由 source 和 transformer 插件创建的类型实现了此接口。例如,由 gatsby-transformer-json 为 authors.json 创建的节点类型将在 GraphQL Schema 中表示为:
自动类型推断
需要注意的是,author.json 中的数据本身不提供 Author 字段的类型信息。为了将数据形状翻译成 GraphQL 类型定义,Gatsby 必须检查每个字段的内容并检查其类型。在许多情况下,这效果很好,并且仍然是创建 GraphQL Schema 的默认机制。
然而,这种方法存在两个问题:(1) 它非常耗时,因此扩展性不是很好;(2) 如果一个字段的值具有不同的类型,Gatsby 就无法决定哪一个是正确的。其后果是,如果您的数据源发生变化,类型推断可能会突然失败。
通过为 Gatsby 的 GraphQL Schema 提供显式类型定义,可以解决这两个问题。
创建类型定义
先看后一种情况。假设一位新作者加入了团队,但在新的作者条目中,joinedAt 字段有一个拼写错误:“201-04-02”,这不是一个有效的日期。
这将使 Gatsby 的类型推断感到困惑,因为 joinedAt 字段现在将同时具有日期和字符串值。
修复字段类型
为确保该字段始终为 Date 类型,您可以使用 createTypes 操作向 Gatsby 提供显式类型定义。它接受 GraphQL Schema 定义语言中的类型定义。
请注意,其余字段(name、firstName 等)不必提供,它们仍将由 Gatsby 的类型推断处理。
用于自定义 Gatsby Schema 生成的操作在
createSchemaCustomization(Gatsby v2.12 及更高版本可用)和sourceNodesAPI 中提供。
选择退出类型推断
然而,为节点类型提供完整定义并完全绕过类型推断机制也有其优势。对于小型项目,推断通常不是性能问题,但随着项目的增长,检查每个字段类型的性能损失会变得明显。
Gatsby 允许使用 @dontInfer 类型指令选择退出推断 - 这反过来要求您为所有应该可供查询的字段显式提供类型定义。
请注意,您不必显式提供 Node 接口字段(id、parent 等),Gatsby 会自动为您添加它们。
如果您想知道感叹号的含义 - 它们允许 指定可空性,即字段值是否允许为
null。
定义媒体类型
您可以使用 @mimeTypes 扩展来指定节点类型处理的媒体类型。
传入的类型用于确定节点的子关系。
定义子关系
可以使用 @childOf 扩展来显式定义节点是哪个节点类型或媒体类型的子项,并立即在父项上添加 child[MyType] 和 children[MyType] 字段。
types 参数接受一个字符串数组,并确定节点是哪些节点类型的子项。
mimeTypes 参数接受一个字符串数组,并确定节点是哪些媒体类型的子项。
mimeTypes 和 types 参数可以组合使用,如下所示:
嵌套类型
到目前为止,示例项目只处理了标量值(String 和 Date;GraphQL 还支持 ID、Int、Float、Boolean 和 JSON)。字段也可以包含复杂的对象值。要在 GraphQL SDL 中定位这些字段,您可以为嵌套类型提供完整的类型定义,该类型可以任意命名(只要名称在 Schema 中是唯一的)。在示例项目中,MarkdownRemark 节点类型上的 frontmatter 字段是一个很好的例子。假设您想确保 frontmatter.tags 始终是字符串数组。
请注意,使用 createTypes 时,您不能直接定位 Frontmatter 类型而不指定它是 MarkdownRemark 类型上 frontmatter 字段的类型。以下内容将失败,因为 Gatsby 无法知道 Frontmatter 类型应该应用于哪个字段。
从 Node 类型开始思考您的数据和相应的 GraphQL Schema,这些 Node 类型是由 source 和 transformer 插件创建的,这是很有用的。
请注意,
Frontmatter类型不需要实现 Node 接口,因为它不是由 source 或 transformer 插件创建的顶级类型:它没有id字段,而是用于描述嵌套字段上的数据形状。
Gatsby 类型构建器
在许多情况下,GraphQL SDL 提供了一种简洁的方式来为您的 Schema 提供类型定义。但是,如果您需要更大的灵活性,createTypes 也接受借助 Gatsby 类型构建器提供的类型定义,它们比 SDL 语法更灵活,但比 graphql-js 更简洁。它们可以通过传递给 Node API 的 schema 参数访问。
Gatsby 类型构建器允许将类型引用为简单的字符串,并接受完整的字段配置(type、args、resolve)。定义顶级类型时,请勿忘记传递 interfaces: ['Node'],它对类型构建器所起的作用与为 SDL 定义的类型添加 implements Node 所起的作用相同。也可以通过将 infer 类型扩展设置为 false 来选择退出类型推断。
类型构建器也存在于 Input、Interface、Union、Enum 和 Scalar 类型:
buildInputObjectType、buildInterfaceType、buildUnionType、buildEnumType和buildScalarType。请注意,createTypes操作也直接接受graphql-js类型,但通常 SDL 或类型构建器是更好的选择。
外键字段
在示例项目中,MarkdownRemark 节点上的 frontmatter.author 字段用于扩展提供的字段值到一个完整的 AuthorJson 节点。为了实现这一点,必须提供一个自定义字段解析器。(有关 context.nodeModel 的更多信息,请参见下文)。
这里发生的是,您提供了一个自定义字段解析器,该解析器向 Gatsby 的内部数据存储查询具有指定 id 和 type 的完整节点对象。
由于创建外键关系是一个非常常见的用例,Gatsby 幸运地提供了一种更简单的方法来实现这一点 - 借助扩展或指令。看起来像这样:
此示例假定您的 markdown frontmatter 具有以下形状:
您的作者 JSON 看起来像这样:
您在字段上提供一个 @link 指令,Gatsby 将在内部添加一个与上面手动编写的解析器非常相似的解析器。如果未提供参数,Gatsby 将使用 id 字段作为外键,否则必须通过 by 参数提供外键。可选的 from 参数允许获取当前类型上用作外键的字段,以链接到 by 参数中指定的字段。换句话说,您 link on from to by。这使得 from 在添加反向链接字段时特别有用。
对于上面的示例,您可以这样理解 @link:使用 Frontmatter.reviewers 字段的值,并通过 AuthorJson.email 字段进行匹配。
请注意,在上面的示例中,AuthorJson 中的 posts 的链接是通过定义到节点的路径来实现的,因为 frontmatter 和 author 都是对象。如果,例如,Frontmatter 类型有一个 authors 列表(frontmatter.authors.email),您需要使用 elemMatch 这样定义:
您还可以使用 Gatsby 类型构建器 或 createResolvers API 提供自定义解析器来链接数组。
扩展和指令
开箱即用,Gatsby 提供了 四种扩展,允许为字段添加自定义功能,而无需手动编写字段解析器。
- 上面已经讨论了
link扩展。 dateformat允许添加日期格式选项。fileByRelativePath类似于link,但在链接到File节点时会解析相对路径。proxy在处理包含 GraphQL 中无效字符的字段名或别名字段的数据时非常有用。
要将扩展添加到字段,您可以使用 SDL 中的指令,或者在使用 Gatsby 类型构建器时使用 extensions 属性。
上面的示例为 AuthorJson.joinedAt 和 MarkdownRemark.frontmatter.publishedAt 字段添加了 日期格式化选项。这些选项在查询这些字段时作为字段参数可用。
publishedAt 还提供了一个默认的 formatString,当查询中没有提供显式格式化选项时将使用它。
如果 JSON 包含您想要 proxy 到其他名称的键,您可以这样做:
您还可以组合多个扩展(内置的和自定义的)。
字段别名
您可以使用 @proxy 指令为同一节点上的另一个字段设置(嵌套)字段的别名。例如,如果您想保持查询的扁平形状或需要保持向后兼容性,这将很有帮助。
如果您使用 createNodeField 向 MarkdownRemark 节点添加一个新字段(请根据您使用的其他 source/type 修改此检查),如下所示:
查询 Hello World 将在以下位置可用:
要能够像这样查询 someInformation,您必须为 fields.someInformation 字段设置别名。
设置默认字段值
为了设置默认字段值,Gatsby 目前尚未提供开箱即用的扩展,因此将字段解析为默认值(而不是 null)需要手动添加字段解析器。例如,为每个博客文章添加一个默认标签:
创建自定义扩展
通过 createFieldExtension 操作,可以定义自定义扩展,作为向字段添加可重用功能的手段。假设您想为 AuthorJson 和 ContributorJson 添加一个 fullName 字段。
您可以编写一个 fullNameResolver,并在两个地方使用它:
但是,为了将此功能提供给其他插件,并使其可以在 SDL 中使用,您可以将其注册为字段扩展。
字段扩展定义需要一个名称和一个 extend 函数,该函数应返回一个(部分)字段配置(一个对象,包含 type、args、resolve),它将被合并到现有的字段配置中。
当插件提供自定义字段扩展时,这种方法会变得更加强大。例如,一个非常基础的 markdown 转换器插件可以提供一个将 markdown 字符串转换为 HTML 的扩展:
然后,它可以在任何 createTypes 调用中使用,方法是将指令/扩展添加到字段:
请注意,在上面的示例中,通过 args 提供了额外的配置选项。例如,这对于提供默认字段参数非常有用。
另外请注意,字段扩展本身可以决定是包装现有的字段解析器还是覆盖它。上面的示例都决定返回一个新的 resolve 函数。因为 extend 函数接收当前字段配置作为第二个参数,所以扩展也可以决定包装现有的解析器。
如果将多个字段扩展添加到同一个字段,解析器将按此顺序处理:首先运行通过 createTypes(或 createResolvers)添加的自定义解析器,然后从左到右执行字段扩展解析器。
最后,请注意,要获取当前的 fieldValue,您需要使用 context.defaultFieldResolver。
createResolvers API
虽然可以通过 Gatsby 类型构建器直接在类型定义中传递 args 和 resolvers,但一种专门用于向字段添加自定义解析器的替代方法是 createResolvers Node API。
请注意,createResolvers 允许向类型添加新字段、修改 args 和 resolver — 但不能覆盖字段类型。这是因为 createResolvers 在 Schema 生成的最后运行,修改字段类型意味着需要重新生成相应的输入类型(filter、sort),而这是您想避免的。如果可能,应使用 createTypes 操作来指定字段类型。
从字段解析器访问 Gatsby 的数据存储
如上所述,Gatsby 的内部数据存储和查询功能可以通过传递给每个解析器的 context.nodeModel 参数在自定义字段解析器中访问。可以使用 getNodeById 和 getNodesByIds 按 id(和可选的 type)访问节点。要获取所有节点或特定类型的节点,请使用 findAll。并且,使用 findAll 也可以从解析器函数内部运行查询,它接受 filter 和 sort 查询参数。
例如,您可以向 AuthorJson 类型添加一个列出作者所有近期帖子的字段:
在使用 findAll 对查询结果进行排序时,请注意 sort.fields 和 sort.order 都是 GraphQLList 字段。此外,sort.fields 上的嵌套字段必须使用点表示法提供(不能用三个下划线分隔)。例如
自定义查询字段
createResolvers 启用的一种强大方法是添加自定义根查询字段。虽然 Gatsby 添加的默认根查询字段(例如 markdownRemark 和 allMarkdownRemark)提供了全部查询选项,但为您的项目专门设计的查询字段也可能很有用。例如,您可以为示例博客中已收到礼品的所有外部贡献者添加一个查询字段
因为您可能也对反向感兴趣——即哪些贡献者尚未收到礼品——为什么不添加一个(必需的)自定义查询参数呢?
也可以提供更复杂的自定义输入类型,这些类型可以直接在 SDL 中内联定义。例如,您可以向 ContributorJson 类型添加一个字段,该字段计算贡献者的帖子数,然后添加一个自定义根查询字段 contributors,该字段接受 min 或 max 参数,以仅返回撰写了至少 min 篇或最多 max 篇帖子的贡献者
处理热重载
创建自定义字段解析器时,确保 Gatsby 知道页面依赖的数据对于热重载的正常工作至关重要。当您使用 context.nodeModel 方法从 store 中检索节点时,通常不需要手动执行任何操作,因为 Gatsby 会自动为查询结果注册依赖项。如果您想自定义此行为,可以通过以编程方式使用 context.nodeModel.trackPageDependencies,或使用
自定义接口和联合类型
最后,假设您想在示例博客上创建一个页面,列出所有团队成员(作者和贡献者)。您可以执行的操作是进行两个查询,一个用于 allAuthorJson,一个用于 allContributorJson,然后手动合并它们。然而,GraphQL 为这类问题提供了更优雅的解决方案,即使用“抽象类型”(接口和联合类型)。由于作者和贡献者实际上共享大部分字段,您可以将它们抽象到一个 TeamMember 接口中,并添加一个自定义查询字段来获取所有团队成员(以及一个用于全名的自定义解析器)
要在页面查询中使用新添加的根查询字段来获取所有团队成员的全名,您可以这样做
可查询接口
自 Gatsby 3.0.0 起,您可以使用接口继承来实现与上述相同的功能:TeamMember implements Node。这将把接口视为一个普通的顶级类型,实现了 Node 接口,从而自动为接口添加根查询字段。
查询时,请为实现接口的类型的特定字段使用内联片段(即共享字段以外的字段)
包含 __typename 内省字段允许在组件中迭代查询结果时检查节点类型
注意:所有实现可查询接口的类型也必须实现
Node接口