使用React和TypeScript:在Apollo Client的片段中共享操作之间的字段
Apollo Client是适用于React的状态管理库。它可以使用GraphQL处理本地和远程数据。本文是根据官方网站的「Fragments Share fields between operations」撰写的,旨在解释如何在多个查询和变更操作之间共享字段。学习如何在Apollo Client中使用查询是前提(如果尚未学习,请先阅读「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」)。这不是文档的翻译,而是用日语重新解释了原文。省略了一些内容,并补充了一些不太容易理解的地方。
GraphQL的片段是可在多个查询和更改之间共享的逻辑的一部分。下面的代码示例是声明NameParts片段,可在任何Person对象中使用。
fragment NameParts on Person {
firstName
lastName
}
片段中包含属于相关类型的字段子集。在上述例子中,要使NameParts片段有效,Person类型必须声明firstName和lastName作为字段。
通过这样做,会在多个查询和修改Person对象的操作中添加NameParts部分。
query GetPerson {
people(id: "7") {
...NameParts
avatar(size: LARGE)
}
}
【注】在碎片之前添加的…具有与JavaScript的扩展语法相同的功能。
根据前述NameParts的规定,上面的查询将等同于以下代码。
query GetPerson {
people(id: "7") {
firstName
lastName
avatar(size: LARGE)
}
}
使用片段进行字段更新是自动的。如果NameParts片段的字段后来被覆写,则会反映在使用片段操作的字段上。这样可以保持跨多个操作的字段一致性,节省了麻烦。
用法示例
假设有一个博客应用程序,并且需要执行一些与评论相关的GraphQL操作(例如发布评论、获取文章的评论等)。这些操作可能都涉及到Comment类型的字段。
为了指定这个核心领域的集合,我们定义了Comment类型的碎片(CoreCommentFields),具体如下。
import { gql } from '@apollo/client';
export const CORE_COMMENT_FIELDS = gql`
fragment CoreCommentFields on Comment {
id
postedBy {
username
displayName
}
createdAt
content
}
`;
[注意] 如果是在应用程序中,片段(fragment)可以在任何文件中声明,都没有问题。上述示例是从文件fragments.js中导出片段。
然后,同一个应用程序中的文件PostDetails.jsx导入CORE_COMMENT_FIELDS,并将CoreCommentFields片段添加到GraphQL操作中。
import { gql } from '@apollo/client';
import { CORE_COMMENT_FIELDS } from './fragments';
export const GET_POST_DETAILS = gql`
${CORE_COMMENT_FIELDS}
query CommentsForPost($postId: ID!) {
post(postId: $postId) {
title
body
author
comments {
...CoreCommentFields
}
}
}
`;
// ...PostDetailsコンポーネントの定義...
-
- 在外部文件 fragments.js 中声明并导出的 CORE_COMMENT_FIELDS 进行导入。
在 GET_POST_DETAILS 的 gql 模板字面量中,使用占位符(${CORE_COMMENT_FIELDS})来添加片段的定义。
使用标准的… 语法将 CoreCommentFields 片段包含在查询中。
放置碎片的位置
GraphQL响应的树状结构类似于前端渲染组件的层次结构。由于这种相似性,可以使用片段来划分查询逻辑在组件之间。每个组件都可以精确地请求它们所使用的字段。组件逻辑也将变得更简洁。
我们来考虑下面这样的视图层次结构应用程序。
FeedPage
└── Feed
└── FeedEntry
├── EntryInfo
└── VoteButtons
在这个应用程序中,执行查询的是根FeedPage组件。它获取FeedEntry对象的列表。EntryInfo和VoteButtons组件需要从上层的FeedEntry对象中获取所需的字段。
協同創建了放置在一起的片段
一起配置的碎片和普通的碎片并没有太大的不同。不同之处在于被添加到特定组件中使用碎片字段。例如,FeedPage的子组件VoteButtons可以从FeedEntry对象中接收字段score和vote { choice }。
VoteButtons.fragments = {
entry: gql`
fragment VoteButtonsFragment on FeedEntry {
score
vote {
choice
}
}
`,
};
在子组件VoteButtons.jsx中定义了一个片段之后,父组件FeedEntry.jsx可以在自己的片段中以以下方式引用它(EntryInfo的片段代码已被省略)。
FeedEntry.fragments = {
entry: gql`
fragment FeedEntryFragment on FeedEntry {
commentCount
repository {
full_name
html_url
owner {
avatar_url
}
}
...VoteButtonsFragment
...EntryInfoFragment
}
${VoteButtons.fragments.entry}
${EntryInfo.fragments.entry}
`,
};
VoteButtons.fragments.entry和EntryInfo.fragments.entry的命名方式没有特别规定。只要指定组件并获取到该片段,可以使用任何命名规则。
在使用Webpack时导入片段
如果使用graphql-tag/loader来加载.graphql文件,则可以通过导入语句来包含片段,如下所示。
#import "./someFragment.graphql"
这样一来,someFragment.graphql的内容就可以从当前文件中使用了。详细内容请参考「使用Webpack加载查询」中的「片段」部分。
在中文中,我们可以通过使用联合类型和接口来使用片段。
片段可以在联合类型和接口中定义。以下示例是一个包含三个内联片段的查询。
query AllCharacters {
all_characters {
... on Character {
name
}
... on Jedi {
side
}
... on Droid {
model
}
}
}
上述的代码示例中,查询all_characters将返回一个Character对象的列表。Character类型是Jedi和Droid都实现的接口。列表中的每个条目都包含了Jedi类型对象的side字段以及Droid类型对象的model字段。
然而,为了使此查询正常运作,客户端必须了解接口Character和实现它的类型之间的多态关系。为了告知客户端这种关系,可以在初始化InMemoryCache时传递possibleTypes选项。
手动地确定可能类型。
【注】在Apollo Client 3.0之后,可以使用possibleTypes选项。
possibleTypes是在传递给InMemoryCache构造函数的选项中,用于指定超类型/子类型与模式的关系。这个对象将接口或联合类型的名称(超类型)映射到实现或所属的类型(子类型)。
以下是一个声明possibleTypes变量的例子代码。
const cache = new InMemoryCache({
possibleTypes: {
Character: ["Jedi", "Droid"],
Test: ["PassingTest", "FailingTest", "SkippedTest"],
Snake: ["Viper", "Python"],
},
});
在这个代码示例中,展示了三个接口(Character和Test,以及Snake)及它们的实现对象类型。
如果模式中包含的union类型和接口很少,可能手动指定possibleTypes也没有问题。但是,当模式的大小和复杂性增加时,应该考虑像下面解释的那样从模式中“自动生成possibleTypes”的方法。
自动产生possibleTypes。
下面的代码示例将GraphQL的introspection查询转换为possibleTypes配置对象。
const fetch = require('cross-fetch');
const fs = require('fs');
fetch(`${YOUR_API_HOST}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
variables: {},
query: `
{
__schema {
types {
kind
name
possibleTypes {
name
}
}
}
}
`,
}),
}).then(result => result.json())
.then(result => {
const possibleTypes = {};
result.data.__schema.types.forEach(supertype => {
if (supertype.possibleTypes) {
possibleTypes[supertype.name] =
supertype.possibleTypes.map(subtype => subtype.name);
}
});
fs.writeFile('./possibleTypes.json', JSON.stringify(possibleTypes), err => {
if (err) {
console.error('Error writing possibleTypes.json', err);
} else {
console.log('Fragment types successfully extracted!');
}
});
});
只需要一种选择的话,以下是对原文的中文本地化改写:
如果这样做的话,生成的possibleTypesJSON模块就可以通过import语句导入到创建InMemoryCache的文件中了。
import possibleTypes from './path/to/possibleTypes.json';
const cache = new InMemoryCache({
possibleTypes,
});