片段
在操作之间共享字段
AGraphQL 片段是可以在多个查询和mutations之间共享的逻辑片段。
这是一个可以与任何NameParts
fragment使用的声明示例:Person
对象:
fragment NameParts on Person {firstNamelastName}
每一个片段都包含属于其关联类型的子集字段。在上面的示例中,Person
类型必须声明firstName
和lastName
字段,以便NameParts
片段有效。
现在,我们可以将NameParts
片段包含在任何数量的查询和突变中中,这些操作都涉及Person
对象,如下所示:
query GetPerson {people(id: "7") {...NamePartsavatar(size: LARGE)}}
根据我们的NameParts
定义,上述查询等同于:
query GetPerson {people(id: "7") {firstNamelastNameavatar(size: LARGE)}}
如果我们以后更改包含在NameParts
片段中的字段,我们将自动更改使用该片段的操作包含的字段。这减少了在一系列操作中保持字段一致所需的努力。
示例使用
假设我们有一个博客应用程序,它执行多个与评论相关的GraphQL操作(提交评论、获取文章评论等)。这些操作可能都包含Comment类型的某些字段。
为了指定这个核心字段集,我们可以在Comment
类型上定义一个片段,例如:
import { gql } from '@apollo/client';export const CORE_COMMENT_FIELDS = gql`fragment CoreCommentFields on Comment {idpostedBy {usernamedisplayName}createdAtcontent}`;
您可以在应用程序的任何文件中声明片段。上面的示例export
将片段从fragments.js
文件导出。
然后我们可以将CoreCommentFields
片段在以下操作中使用:
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) {titlebodyauthorcomments {...CoreCommentFields}}}`;// ...PostDetails component definition...
- 我们首先
import
CORE_COMMENT_FIELDS
因为它们是在另一个文件中声明的。 - 我们将我们的片段定义添加到
GET_POST_DETAILS
的gql
模板字面量中,通过占位符(${CORE_COMMENT_FIELDS}
) - 我们在我们的查询中使用标准的
...
符号来包含CoreCommentFields片段。
使用 createFragmentRegistry
注册命名的片段
从 Apollo Client 3.7 开始,片段可以与您的InMemoryCache
注册,这样它们可以在查询或任何InMemoryCache
操作(如cache.readFragment
、cache.readQuery
和cache.watch
)中通过名称引用,而无需在声明中插值。
让我们看看React中的示例。
import { ApolloClient, gql, InMemoryCache } from "@apollo/client";import { createFragmentRegistry } from "@apollo/client/cache";const client = new ApolloClient({uri: "https://127.0.0.1:4000/graphql",cache: new InMemoryCache({fragments: createFragmentRegistry(gql`fragment ItemFragment on Item {idtext}`)})});
由于ItemFragment
已经在InMemoryCache
中注册,所以它可以用名称引用,如下面片段在GetItemList
查询中展开所示。
const listQuery = gql`query GetItemList {list {...ItemFragment}}`;function ToDoList() {const { data } = useQuery(listQuery);return (<ol>{data?.list.map(item => (<Item key={item.id} text={item.text} />))}</ol>);}
使用本地版本覆盖已注册的片段
查询可以声明它们自己的本地版本命名的片段,这将优先级高于通过createFragmentRegistry
注册的片段,即使本地片段只是由其他已注册片段间接引用。以下是一个例子:
import { ApolloClient, gql, InMemoryCache } from "@apollo/client";import { createFragmentRegistry } from "@apollo/client/cache";const client = new ApolloClient({uri: "https://127.0.0.1:4000/graphql",cache: new InMemoryCache({fragments: createFragmentRegistry(gql`fragment ItemFragment on Item {idtext...ExtraFields}fragment ExtraFields on Item {isCompleted}`)})});
在ItemList.jsx
中声明的ExtraFields
片段的本地版本优先于最初与InMemoryCache
一起注册的ExtraFields
。因此,当在注册的ItemFragment
中解析扩展的ExtraFields
片段时,只会使用其定义(仅在执行GetItemList
查询时),因为显式定义优先于已注册片段。
const listQuery = gql`query GetItemList {list {...ItemFragment}}fragment ExtraFields on Item {createdBy}`;function ToDoList() {const { data } = useQuery(listQuery);return (<ol>{data?.list.map((item) => ({/* `createdBy` exists on the returned items, `isCompleted` does not */}<Item key={item.id} text={item.text} author={item.createdBy} />))}</ol>);}
并列放置片段
GraphQL响应的树形结构类似于前端渲染组件的层次结构。由于这种相似性,您可以使用片段片段将查询逻辑拆分在组件之间,以便每个组件正好请求它使用的字段。这有助于您使组件逻辑更加简洁。
考虑以下应用程序的可视层次结构
FeedPage└── Feed└── FeedEntry├── EntryInfo└── VoteButtons
在这个应用程序中,FeedPage
组件执行一个查询来获取FeedEntry
对象的列表。子组件EntryInfo
和VoteButtons
需要从包围的FeedEntry
对象中获取特定的字段。
创建联动物件片段
携手合作片段就像任何其他片段一样,只是它附加到使用片段字段的特定组件。例如,VoteButtons
是FeedPage
的子组件可能会使用来自FeedEntry
对象的score
和vote { choice }
字段:
VoteButtons.fragments = {entry: gql`fragment VoteButtonsFragment on FeedEntry {scorevote {choice}}`,};
在子组件中定义片段之后,父组件可以在其自己的联同伴分中引用它,如下所示:
FeedEntry.fragments = {entry: gql`fragment FeedEntryFragment on FeedEntry {commentCountrepository {full_namehtml_urlowner {avatar_url}}...VoteButtonsFragment...EntryInfoFragment}${VoteButtons.fragments.entry}${EntryInfo.fragments.entry}`,};
关于VoteButtons.fragments.entry
或EntryInfo.fragments.entry
的命名没有什么特别之处。任何命名约定都行,只要您能根据组件检索到组件的片段。
使用Webpack时导入片段
当使用graphql-tag/loader加载.graphql
文件时,我们可以使用import
语句来包含片段。例如:
#import "./someFragment.graphql"
这使当前文件可用someFragment.graphql
的内容。有关详细信息,请参阅Webpack Fragments部分。
使用联合和接口上的片段
您可以在联合和接口上定义片段。
以下是一个包含三个内联片段的查询示例:
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
选项。
手动定义 possibleTypes
该 possibleTypes
选项在 Apollo Client 3.0 及更高版本中可用。
您可以将 possibleTypes
选项传递给 InMemoryCache
构造函数以指定架构中的超类与子类之间的关系。此对象将一个接口或联合类型的名称(超类)映射到实现或属于它的类型(子类)。
以下是一个 possibleTypes
声明的示例:
const cache = new InMemoryCache({possibleTypes: {Character: ["Jedi", "Droid"],Test: ["PassingTest", "FailingTest", "SkippedTest"],Snake: ["Viper", "Python"],},});
该示例列出了三个接口(Character
,Test
和 Snake
)以及实现它们的对象类型。
如果您的架构只包含少数几个联合和接口,您可能可以手动指定 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 {kindnamepossibleTypes {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!');}});});
然后您可以将生成的possibleTypes
JSON模块导入到创建您的InMemoryCache
的地方:
import possibleTypes from './path/to/possibleTypes.json';const cache = new InMemoryCache({possibleTypes,});
useFragment
The useFragment
hook将轻量级的实时绑定到 Apollo 客户端的缓存。它允许 Apollo 客户端向单个组件广播特定的fragment结果。此 hook 返回缓存当前为给定fragment包含的任何数据的始终最新的视图。useFragment
永远不会触发它自己的网络请求。
useQuery
hook 仍然负责在缓存中查询和填充数据的 主要 hook(查看 API 参考)。因此,通过 fragment数据通过useFragment
读取的组件仍然订阅query数据中的所有变更,但只有在特定 fragment的数据变更时才会收到更新。
注意:此 hook 在3.7.0
作为实验性功能引入,但在3.8.0
中稳定下来。在3.7.x
和3.8.0-alpha.x
版本中,此 hook 以useFragment_experimental
的形式导出。从3.8.0-beta.0
及更高版本开始,其命名导出中不再包含_experimental
后缀。
示例
考虑到以下fragment定义:
const ItemFragment = gql`fragment ItemFragment on Item {text}`;
我们可以首先使用useQuery
hook 获取具有id
的项以及在任何命名的ItemFragment
fragment 上选择的任何字段。通过在ListQuery
中的list
内部散播ItemFragment
done,可以完成此操作。
const listQuery = gql`query GetItemList {list {id...ItemFragment}}${ItemFragment}`;function List() {const { loading, data } = useQuery(listQuery);return (<ol>{data?.list.map(item => (<Item key={item.id} id={item.id}/>))}</ol>);}
注意:我们可以在每次query中内插fragments的界面,我们还可以使用 Apollo 客户端的createFragmentRegistry
方法预先将命名的fragments注册到我们的InMemoryCache
中。这允许 Apollo 客户端在网络请求之前将注册的fragment的定义包括在要发送的文档中。有关更多信息,请参阅使用createFragmentRegistry
注册命名的fragments。
然后,我们可以在<Item>
组件内部使用useFragment
,通过提供fragment
document、fragmentName
及通过 from
的对象引用,为每个项创建实时绑定。
function Item(props: { id: number }) {const { complete, data } = useFragment({fragment: ItemFragment,fragmentName: "ItemFragment",from: {__typename: "Item",id: props.id,},});return <li>{complete ? data.text : "incomplete"}</li>;}