使用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コンポーネントの定義...
    1. 在外部文件 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,
});
广告
将在 10 秒后关闭
bannerAds