跳至主要内容
版本:v18.0.0

片段

片段是 Relay 的主要特性之一。它们允许每个组件独立声明自己的数据需求,同时保留单个查询的效率。在本节中,我们将展示如何将查询拆分为片段。


首先,假设我们希望 Story 组件显示故事发布的日期。为此,我们需要从服务器获取更多数据,因此我们需要在查询中添加一个字段。

转到 `Newsfeed.tsx` 并找到 `NewsfeedQuery`,以便添加新字段

const NewsfeedQuery = graphql`
query NewsfeedQuery {
topStory {
title
summary
createdAt // Add this line
poster {
name
profilePicture {
url
}
}
image {
url
}
}
}
`;

现在我们更新了查询,我们需要运行 Relay 编译器,以便它了解通过运行 `npm run relay` 更新的 Graphql 查询。

接下来,转到 `Story.tsx` 并修改它以显示日期

import Timestamp from './Timestamp';

type Props = {
story: {
createdAt: string; // Add this line
...
};
};

export default function Story({story}: Props) {
return (
<Card>
<PosterByline poster={story.poster} />
<Heading>{story.title}</Heading>
<Timestamp time={story.createdAt} /> // Add this line
<Image image={story.image} />
<StorySummary summary={story.summary} />
</Card>
);
}

日期现在应该出现了。由于 GraphQL,我们不必编写和部署任何新的服务器代码。

但是,仔细想想,为什么需要修改 `Newsfeed.tsx`?React 组件不应该自包含吗?为什么 Newsfeed 需要关心 Story 特定的数据需求?如果数据是由 Story 的某个子组件在层次结构中更深处需要的呢?如果它是在多个不同地方使用的组件呢?那么,每当它的数据需求发生变化时,我们就必须修改多个组件。

为了避免这些以及许多其他问题,我们可以将 Story 组件的数据需求移到 `Story.tsx` 中。

我们通过将 `Story` 的数据需求拆分为在 `Story.tsx` 中定义的 *片段* 来实现这一点。片段是 GraphQL 的独立部分,Relay 编译器会将它们拼接成完整的查询。它们允许每个组件定义自己的数据需求,而不会在运行时为每个组件运行自己的查询而付出代价。

The Relay compiler combines the fragment into the place it&#39;s spread

现在,让我们将 `Story` 的数据需求拆分为一个片段。


步骤 1 - 定义一个片段

将以下内容添加到 `Story.tsx`(位于 `src/components` 中)的 Story 组件上方

import { graphql } from 'relay-runtime';

const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
createdAt
poster {
name
profilePicture {
url
}
}
image {
url
}
}
`;

请注意,我们已经将查询中 `topStory` 内的所有选择复制到了这个新的片段声明中。与查询一样,片段有名称(`StoryFragment`),我们将在稍后使用,但它们也有一个 GraphQL 类型(`Story`),它们是“基于”该类型的。这意味着,只要我们在图中有一个 Story 节点,就可以使用这个片段。

步骤 2 - 扩展片段

转到 `Newsfeed.tsx` 并修改 `NewsfeedQuery`,使其看起来像这样

const NewsfeedQuery = graphql`
query NewsfeedQuery {
topStory {
...StoryFragment
}
}
`;

我们用 `StoryFragment` 替换了 `topStory` 内的选择。Relay 编译器将确保从现在起获取 Story 的所有数据,而无需更改 `Newsfeed`。

步骤 3 - 调用 useFragment

你会注意到,Story 现在呈现了一个空的卡片!所有数据都丢失了!Relay 不应该将片段选择的字段包含在从 `useLazyLoadQuery()` 获取的 `story` 对象中吗?

原因是 Relay 隐藏了它们。除非组件专门要求获取某个片段的数据,否则该数据对组件不可见。这被称为 *数据屏蔽*,它强制执行组件不隐式依赖于另一个组件的数据依赖关系,而是在其自己的片段中声明所有依赖关系。这使得组件保持自包含和可维护性。

如果没有数据屏蔽,你将无法从片段中删除字段,因为很难验证某个其他组件在其他地方是否正在使用它。

要访问片段选择的数据,我们使用一个名为 `useFragment` 的钩子。修改 `Story`,使其看起来像这样

import { useFragment } from 'react-relay';

export default function Story({story}: Props) {
const data = useFragment(
StoryFragment,
story,
);
return (
<Card>
<Heading>{data.title}</Heading>
<PosterByline poster={data.poster} />
<Timestamp time={data.createdAt} />
<Image image={data.image} />
<StorySummary summary={data.summary} />
</Card>
);
}

`useFragment` 接受两个参数

  • 我们想要读取的片段的 GraphQL 带标记的字符串 字面量
  • 与之前相同的 story 对象,它来自我们在 GraphQL 查询中扩展片段的位置。这被称为 *片段键*。

它返回该片段选择的数据。

提示

我们已经将 `story` 重写为 `data`(`useFragment` 返回的数据),在所有 JSX 代码中,请确保在你的组件副本中也这样做,否则它将无法工作。

片段键是 GraphQL 查询响应中扩展片段的位置。例如,给定 Newsfeed 查询

query NewsfeedQuery {
topStory {
...StoryFragment
}
}

然后,如果 `queryResult` 是 `useLazyLoadQuery` 返回的对象,则 `queryResult.topStory` 将是 `StoryFragment` 的片段键。

从技术上讲,`queryResult.topStory` 是一个包含一些隐藏字段的对象,这些字段告诉 Relay 的 `useFragment` 在哪里查找它需要的数据。片段键指定要读取的节点(这里只有一个故事,但很快我们将有多个故事)以及可以读取的字段(由该特定片段选择的字段)。然后,`useFragment` 钩子会从 Relay 的本地数据存储中读取该特定信息。

注意

正如我们在后面的例子中将看到的那样,你可以将多个片段扩展到查询中的同一个位置,也可以将片段扩展与直接选择的字段混合使用。

步骤 4 - 片段引用的 TypeScript 类型

为了完成片段化,我们还需要更改 `Props` 的类型定义,以便 TypeScript 知道此组件期望接收片段键而不是原始数据。

回想一下,当你将片段扩展到查询(或另一个片段)中时,查询结果中对应于你扩展片段的部分将成为该片段的 *片段键*。这是你传递给组件的属性中的对象,以便为它提供图中的特定位置来读取片段。

为了使这在类型上是安全的,Relay 生成了一个表示该特定片段的片段键的类型 - 这样,如果你试图使用一个没有将它的片段扩展到查询中的组件,你将无法提供满足类型系统的片段键。以下是我们需要进行的更改

import type {StoryFragment$key} from './__generated__/StoryFragment.graphql';

type Props = {
story: StoryFragment$key;
};

这样一来,我们就得到了一个 `Newsfeed`,它不再需要关心 `Story` 需要什么数据,但仍然可以在自己的查询中提前获取这些数据。


练习

`Story` 使用的 `PosterByline` 组件呈现了发布者的姓名和个人资料图片。使用相同的步骤对 `PosterByline` 进行片段化。你需要

  • 在 `Actor` 上声明一个 `PosterBylineFragment`,并指定它需要的字段(`name`、`profilePicture`)。`Actor` 类型表示可以发布故事的人或组织。
  • 将该片段扩展到 `StoryFragment` 中的 `poster` 中。
  • 调用 `useFragment` 来检索数据。
  • 更新 Props 以接受 `PosterBylineFragment$key` 作为 `poster` 属性。

重复这些步骤是值得的,以便熟练掌握片段的使用方法。这里有很多部分需要以正确的方式拼凑在一起。

完成这些步骤后,让我们看一下片段如何帮助应用程序扩展的基本示例。


在多个地方重用片段

片段表示,给定 *某个* 特定类型的图节点,要从该节点读取哪些数据。片段键指定图中从哪个节点选择数据。一个指定了片段的可重用组件可以通过传递不同的片段键,从图的不同部分的不同上下文中检索数据。

例如,请注意 `Image` 组件在两个地方使用:直接在 `Story` 中用于故事的缩略图,以及在 `PosterByline` 中用于发布者的个人资料图片。让我们对 `Image` 进行片段化,并看看它如何根据使用位置从图中的不同位置选择它需要的数据。

Fragment can be used in multiple places

步骤 1 - 定义片段

打开 Image.tsx 并添加片段定义

import { graphql } from 'relay-runtime';

const ImageFragment = graphql`
fragment ImageFragment on Image {
url
}
`;

步骤 2 - 展开片段

返回到 StoryFragmentPosterBylineFragment,并将 ImageFragment 展开到 Image 组件使用数据的每个位置

const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
postedAt
poster {
...PosterBylineFragment
}
thumbnail {
...ImageFragment
}
}
`;

步骤 3 - 调用 useFragment

修改 Image 组件以使用其片段读取字段,并修改其 Props 以接受片段键

import { useFragment } from 'react-relay';
import type { ImageFragment$key } from "./__generated__/ImageFragment.graphql";

type Props = {
image: ImageFragment$key;
...
};

function Image({image}: Props) {
const data = useFragment(ImageFragment, image);
return <img key={data.url} src={data.url} ... />
}

步骤 4 - 修改一次,到处享受

现在我们已经将 Image 的数据需求片段化并将其与组件协同定位,我们可以为 Image 添加新的数据依赖关系,而无需修改任何使用它的组件。

例如,让我们为 Image 组件添加一个 altText 标签以实现可访问性。

编辑 ImageFragment 如下

const ImageFragment = graphql`
fragment ImageFragment on Image {
url
altText
}
`;

现在,无需编辑 Story、Newsfeed 或任何其他组件,我们查询中的所有图像都将获取其 alt 文本。所以我们只需要修改 Image 来使用新字段

function Image({image}) {
// ...
<img
alt={data.altText}
//...
}

现在,故事缩略图图像和海报的个人资料图片都将具有 alt 文本。(您可以使用浏览器的“元素”检查器来验证这一点。)

您可以想象,随着代码库的扩大,这将多么有利。每个组件都是自包含的,无论它被使用多少次!即使一个组件在数百个地方使用,您也可以随意添加或删除其数据依赖关系中的字段。这是 Relay 帮助您随着应用程序规模扩大而扩展的主要方式之一。

Field added to one fragment is added in all places it&#39;s used

片段是 Relay 应用程序的构建块。因此,许多 Relay 功能都是基于片段的。我们将在下一节中介绍其中的一些。


片段参数和字段参数

目前,Image 组件以其全尺寸获取图像,即使它们将以较小的尺寸显示也是如此。这很低效!Image 组件接受一个表示显示图像尺寸的 prop,因此它由使用 Image 的组件控制。我们希望以类似的方式让使用 Image 的组件在其片段中说明要获取的图像尺寸。

GraphQL 字段可以接受 *参数*,这些参数为服务器提供额外信息以满足我们的请求。例如,Image 类型上的 url 字段接受 heightwidth 参数,服务器会将这些参数合并到 URL 中——如果我们有这个片段

fragment Example1 on Image {
url
}

我们可能会得到像 /images/abcde.jpeg 这样的 URL

——而如果我们有这个片段

fragment Example2 on Image {
url(height: 100, width: 100)
}

我们可能会得到一个像 /images/abcde.jpeg?height=100&width=100 这样的 URL

当然,我们不希望只是将特定尺寸硬编码到 ImageFragment 中,因为我们希望 Image 组件在不同上下文中获取不同尺寸的图像。为此,我们可以让 ImageFragment 接受 *片段参数*,以便父组件可以指定应该获取的图像尺寸。这些 *片段参数* 然后可以作为 *字段参数* 传递给特定字段(在本例中为 url)。

为此,请编辑 ImageFragment 如下

const ImageFragment = graphql`
fragment ImageFragment on Image
@argumentDefinitions(
width: {
type: "Int",
defaultValue: null
}
height: {
type: "Int",
defaultValue: null
}
)
{
url(
width: $width,
height: $height
)
altText
}
`;

让我们分解一下

  • 我们在片段声明中添加了一个 @argumentDefinitions 指令。这表示片段接受什么参数。对于每个参数,我们给出
    • 参数的名称
    • 它的类型(可以是任何 GraphQL 标量类型
    • 可选的 默认值——在本例中,默认值为 null,这使我们能够以其固有尺寸获取图像。如果没有给出默认值,则在每个使用片段的地方都需要该参数。
  • 然后,我们通过使用片段参数作为变量来填充 GraphQL 字段的参数。这里字段参数和片段参数具有相同的名称(通常情况下),但请注意:width: 是字段参数,而 $width 是由片段参数创建的变量。

现在,片段接受一个参数,它通过其选择的字段之一传递给服务器。

深入了解:GraphQL 指令

片段参数的语法可能看起来很笨拙。这是因为它基于 *指令*,这是一种扩展 GraphQL 语言的系统。在 GraphQL 中,任何以 @ 开头的符号都是指令。它们的含义不是由 GraphQL 规范定义的,而是由特定客户端或服务器实现决定的。

Relay 定义了 多个指令 来支持其功能——片段参数就是一个例子。这些指令不会发送到服务器,而是为构建时 Relay 编译器提供指令。

GraphQL 规范实际上确实定义了三个指令的含义

  • @deprecated 用于模式定义,并将字段标记为已弃用。
  • @include@skip 可用于使字段的包含条件化。

除了这些,GraphQL 服务器还可以将其模式的一部分指定为额外的指令。Relay 也有自己的构建时指令,使我们能够在不改变语法的情况下扩展语言。

步骤 2

现在,使用 Image 的不同片段可以传递每个图像的适当尺寸

const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
postedAt
poster {
...PosterBylineFragment
}
thumbnail {
...ImageFragment @arguments(width: 400)
}
}
`;

现在,如果您查看应用程序下载的图像,您会看到它们是较小的尺寸,从而节省了网络带宽。请注意,虽然我们使用整数字面量作为片段参数的值,但我们也可以使用运行时提供的变量,正如我们将在后面的部分中看到的那样。

字段参数(例如 url(height: 100))是 GraphQL 本身的功能,而片段参数(如 @argumentDefinitions@arguments)是 Relay 特定的功能。Relay 编译器在将片段组合成查询时处理这些片段参数。


摘要

片段是 Relay 使用 GraphQL 的最独特之处。我们建议每个显示数据并关心该数据语义的组件(因此不仅仅是排版或格式组件)使用 GraphQL 片段来声明其数据依赖关系。

  • 片段可以帮助您扩展:无论一个组件被使用多少次,您都可以在一个地方更新其数据依赖关系。
  • 需要使用 useFragment 读取片段数据。
  • useFragment 接受一个 *片段键*,它表示要读取的图中的位置。
  • 片段键来自 GraphQL 响应中展开该片段的位置。
  • 片段可以定义在展开时使用的参数。这使它们能够适应每个使用情况。

我们将重新审视片段的许多其他功能,例如如何在不重新获取整个查询的情况下重新获取单个片段的内容。但是,首先,让我们通过了解数组来使这个新闻提要应用程序更像新闻提要。