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

交互查询

我们已经看到了片段如何在每个组件中指定数据需求,但在运行时只对整个屏幕执行一次查询。在这里,我们将看看一种需要在同一屏幕上进行第二次查询的情况。这将让我们探索 GraphQL 查询的更多功能。

  • 我们将构建一个悬停卡,当您将鼠标悬停在故事发布者的姓名上时,它会显示有关发布者的更多详细信息。
  • 悬停卡将使用第二个查询来获取额外信息,这些信息只有在用户悬停时才需要。
  • 我们将使用查询变量来告诉服务器我们想要了解哪些人的更多详细信息。
  • 我们将看到如何使用预加载查询来提高性能。

在介绍完这些主题后,我们将返回来查看片段的更多高级功能。


在本节中,我们将向 PosterByline 添加一个悬停卡,这样你就可以通过将鼠标悬停在故事发布者的姓名上,查看有关发布者的更多详细信息。

深入探讨: 何时使用二级查询

我们之前提到过,Relay 的设计目的是帮助您预先获取整个屏幕的所有数据需求。但我们可以概括地说,这是一个用户交互,最多应该有一个查询。导航到另一个屏幕只是用户交互的一种常见类型。

在同一屏幕内,一些交互可能会公开比最初显示的更多数据。如果一个交互执行起来比较少,但需要大量额外数据,那么在交互发生时执行第二次查询来获取额外数据可能更明智,而不是在屏幕首次加载时预先获取。这样可以使初始加载更快,成本更低。

还有一些交互,其中获取的数据量是不确定的——例如,悬停卡内的悬停卡——而且无法静态地知道。

如果数据优先级较低,应该在主数据加载后加载,但应该在没有进一步用户输入的情况下自动弹出,Relay 具有一个名为延迟片段的功能。我们将在后面介绍它。

我们已经准备好了一个悬停卡组件,您可以使用它。但是,为了避免编译错误,因为它使用 ImageFragment,它一直放在名为 future 的目录中。现在我们已经到了教程的这一阶段,您可以将 future 中的模块移到 src/components

mv future/* src/components

现在,如果您做了将 PosterByline 使用片段的练习,PosterByline 组件应该看起来像这样

export default function PosterByline({ poster }: Props): React.ReactElement {
const data = useFragment(PosterBylineFragment, poster);
return (
<div className="byline">
<Image image={data.profilePicture} width={60} height={60} className="byline__image" />
<div className="byline__name">{data.name}</div>
</div>
);
}

要使用悬停卡组件,您可以进行以下更改

import Hovercard from './Hovercard';
import PosterDetailsHovercardContents from './PosterDetailsHovercardContents';
const {useRef} = React;

...

export default function PosterByline({ poster }: Props): React.ReactElement {
const data = useFragment(PosterBylineFragment, poster);
const hoverRef = useRef(null);
return (
<div
ref={hoverRef}
className="byline">
<Image image={data.profilePicture} width={60} height={60} className="byline__image" />
<div className="byline__name">{data.name}</div>
<Hovercard targetRef={hoverRef}>
<PosterDetailsHovercardContents />
</Hovercard>
</div>
);
}

现在您应该会看到,当您将鼠标悬停在某个人的姓名上时,会弹出一个包含更多信息的悬停卡。如果您查看 PosterDetailsHovercardContents.tsx 内部,您会发现它使用 useLazyLoadQuery 执行了第二个查询,以在该组件挂载时获取额外信息。

只有一个问题:无论您将鼠标悬停在哪个发布者身上,它始终显示同一个人的信息!

Hovercard showing the wrong person


查询变量

我们需要告诉服务器我们想要了解哪些人的更多信息。GraphQL 允许我们定义查询变量,这些变量可以作为参数传递给特定字段。然后,这些参数在服务器上可用。

在上一节中,我们看到了字段如何接受参数,但参数值是硬编码的,例如 url(width: 200, height: 200)。使用查询变量,我们可以在运行时确定这些值。它们从客户端传递到服务器,与查询本身一起传递。GraphQL 变量总是以 $ 美元符号开头。

查看 PosterDetailsHovercardContents.tsx 内部:您应该会看到类似于下面这样的查询

const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery {
node(id: "1") {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
node 字段 是在我们 schema 中定义的顶级字段,它允许我们通过唯一 ID 获取任何图节点。它以 ID 作为参数,该参数当前是硬编码的。在本练习中,我们将用 UI 状态提供的变量替换这个硬编码的 ID。

这个奇怪的 ... on Actor 是一个类型细化。我们将在下一节中详细介绍它们,现在可以忽略它们。简而言之,因为我们可以向 node 字段提供任何 ID,所以没有办法静态地知道我们将要选择哪种类型的节点。类型细化指定了我们期望的类型,允许我们使用 Actor 类型的字段

在其中,我们只是扩展了一个包含我们想要显示的字段的片段——关于这个片段的更多内容稍后再说。现在,以下是在用我们正在悬停的发布者的 ID 替换这个硬编码 ID 的步骤

步骤 1 - 定义一个查询变量

首先,我们需要编辑我们的查询,以声明它接受一个查询变量。以下是更改内容

const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery(
$posterID: ID!
) {
node(id: "1") {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
  • 变量名为 $posterID。这是我们在 GraphQL 查询的其余部分中使用的符号,用于引用从 UI 传递的值。
  • 变量具有一个类型——在本例中为 ID!ID 类型是 String 的同义词,用于节点 ID,以帮助将它们与其他字符串区分开来。!ID! 中表示该字段是不可为空的。在 GraphQL 中,字段通常是可为空的,不可为空是例外。

步骤 2 - 将变量作为字段参数传递

现在我们用新的变量替换硬编码的 "1"

const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery($posterID: ID!) {
node(
id: $posterID
) {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
注意

您不仅可以使用查询变量作为字段参数,还可以作为片段的参数。

步骤 3 - 向 useLazyLoadQuery 提供要使用的参数值

现在我们需要在运行时从 UI 中传递实际值。useLazyLoadQuery 钩子的第二个参数是一个包含变量值的 对象。我们将向我们的组件添加一个新的 prop,并将它的值传递到那里

export default function PosterDetailsHovercardContents({
posterID,
}: {
posterID: string;
}): React.ReactElement {
const data = useLazyLoadQuery<QueryType>(
PosterDetailsHovercardContentsQuery,
{posterID},
);
return <PosterDetailsHovercardContentsBody poster={data.node} />;
}

步骤 4 - 从父组件中传递 ID

现在我们需要从悬停卡的父组件 PosterByline 中提供 posterID prop。转到该文件,并向它的片段中添加 id——然后将 ID 作为 prop 传递

const PosterBylineFragment = graphql`
fragment PosterBylineFragment on Actor {
id
...
}
`;

export default function PosterByline({ poster }: Props): React.ReactElement {
...
return (
...
<PosterDetailsHovercardContents
posterID={data.id}
/>
...
);
}

此时,悬停卡应该会显示我们悬停的每个发布者的相应信息。

Hovercard showing the correct person

如果您在浏览器中使用网络检查器,您应该能够找到与查询一起传递的变量值

Network request inspector showing variable being sent to the server

您可能还会注意到,此请求只在您第一次将鼠标悬停在特定发布者身上时才会发出。Relay 会缓存查询的结果,并在之后重新使用它们,直到最终在最近未使用时删除缓存的数据。

深入探讨: 缓存和 Relay 存储

与大多数其他系统不同,Relay 的缓存不是基于查询,而是基于图节点。Relay 维持所有已获取节点的本地缓存,称为 Relay 存储。存储中的每个节点都通过其 ID 进行标识和检索。如果两个查询请求相同的信息(通过节点 ID 标识),则第二个查询将使用为第一个查询检索的缓存信息来完成,而不是被获取。确保配置 丢失字段处理程序 以利用此缓存行为。

如果节点从任何已使用的查询或任何已挂载组件最近使用的查询中“不可访问”,Relay 将从存储中回收这些节点。

深入探讨: 为什么 GraphQL 需要一个变量语法

您可能想知道,为什么 GraphQL 会有变量的概念,而不是简单地将变量的值插值到查询字符串中。嗯,正如之前提到的,GraphQL 查询字符串的文本在运行时不可用,因为 Relay 会用更有效的 数据结构来替换它。您还可以配置 Relay 以使用预准备查询,其中编译器在构建时将每个查询上传到服务器并为其分配一个 ID——在这种情况下,在运行时,Relay 只是告诉服务器“给我查询 #1337”,所以字符串插值是不可能的,因此变量必须通过带外方式提供。即使查询字符串可用,单独传递变量值也能消除对任何 HTTP 请求所需的任意值序列化和字符串转义的任何问题。


预加载查询

这个示例应用程序非常简单,所以性能不是问题。(实际上,服务器被人工减速,以便使加载状态可感知。)但是,Relay 的主要关注点之一是在真实应用程序中尽可能提高性能。

目前,悬停卡使用 useLazyLoadQuery 钩子,该钩子在组件渲染时获取查询。这意味着时间线看起来像这样

Network doesn&#39;t start until render

理想情况下,我们应该尽早开始网络获取,但在这里,我们直到 React 完成渲染才会开始。如果我们使用 React.lazy 在交互发生时加载悬停卡组件本身的代码,那么这个时间线可能看起来更糟糕。在这种情况下,它看起来像这样

Network doesn&#39;t start until component is fetched and then rendered

注意,我们在开始获取 GraphQL 查询之前一直在等待。如果查询获取在 React 组件甚至渲染之前开始,就在鼠标事件处理程序本身的开头,那就更好了。然后时间线看起来像这样

Network and component fetch happen concurrently

当用户交互时,我们应该立即开始获取我们需要的查询,同时开始渲染组件(如果需要,先获取其代码)。一旦这两个异步进程完成,我们就可以使用可用的数据渲染组件并将其显示给用户。

Relay 提供了一个名为预加载查询的功能,它允许我们做到这一点。

让我们修改悬停卡以使用预加载查询。

步骤 1 - 将 useLazyLoadQuery 更改为 usePreloadedQuery

提醒一下,这是目前延迟获取数据的 PosterDetailsHovercardContents 组件

export default function PosterDetailsHovercardContents({
posterID,
}: {
posterID: string;
}): React.ReactElement {
const data = useLazyLoadQuery<QueryType>(
PosterDetailsHovercardContentsQuery,
{posterID},
);
return <PosterDetailsHovercardContentsBody poster={data.node} />;
}

它调用 useLazyLoadQuery,该钩子接受变量作为其第二个参数。我们想把它改为 usePreloadedQuery。但是,对于预加载查询,变量实际上是在获取查询时确定的,这将是在此组件甚至渲染之前。因此,此钩子不接受变量,而是接受一个查询引用,其中包含检索查询结果所需的信息。查询引用将在步骤 2 中获取查询时创建。

按如下方式更改组件

import {usePreloadedQuery} from 'react-relay';
import type {PreloadedQuery} from 'react-relay';
import type {PosterDetailsHovercardContentsQuery as QueryType} from './__generated__/PosterDetailsHovercardContentsQuery.graphql';

export default function PosterDetailsHovercardContents({
queryRef,
}: {
queryRef: PreloadedQuery<QueryType>,
}): React.ReactElement {
const data = usePreloadedQuery(
PosterDetailsHovercardContentsQuery,
queryRef,
);
...
}

步骤 2: 导出查询以供父组件访问

我们将修改父组件 PosterByline,使其启动 PosterDetailsHovercardContentsQuery 查询。它需要对该查询的引用,因此我们需要导出它

export const PosterDetailsHovercardContentsQuery = graphql`...

步骤 3: 在父组件中调用 useQueryLoader

现在 PosterDetailsHovercardContents 期望一个查询引用,我们需要创建该查询引用并将其从父组件 PosterByline 传递下来。我们使用名为 useQueryLoader 的钩子创建查询引用。此钩子还返回一个函数,我们在事件处理程序中调用该函数来触发查询获取。

import {useQueryLoader} from 'react-relay';
import type {PosterDetailsHovercardContentsQuery as HovercardQueryType} from './__generated__/PosterDetailsHovercardContentsQuery.graphql';
import {PosterDetailsHovercardContentsQuery} from './PosterDetailsHovercardContents';

export default function PosterByline({ poster }: Props): React.ReactElement {
...
const [
hovercardQueryRef,
loadHovercardQuery,
] = useQueryLoader<HovercardQueryType>(PosterDetailsHovercardContentsQuery);
return (
...
<PosterDetailsHovercardContents
queryRef={hovercardQueryRef}
/>
...
);
}

useQueryLoader 钩子返回我们需要使用的两件事

  • 查询引用是一段不透明的信息,usePreloadedQuery 将使用它来检索查询的结果。
  • loadHovercardQuery 是一个将启动请求的函数。

步骤 4: 在事件处理程序中获取查询

最后,我们需要在卡显示时发生的事件处理程序中调用 loadHovercardQuery。幸运的是,Hovercard 组件有一个 onBeginHover 事件,我们可以使用它

export default function PosterByline({ poster }: Props): React.ReactElement {
...
const [
hovercardQueryRef,
loadHovercardQuery,
] = useQueryLoader<HovercardQueryType>(PosterDetailsHovercardContentsQuery);
function onBeginHover() {
loadHovercardQuery({posterID: data.id});
}
return (
<div className="byline">
...
<Hovercard
onBeginHover={onBeginHover}
targetRef={hoverRef}>
<PosterDetailsHovercardContents queryRef={hovercardQueryRef} />
</Hovercard>
</div>
);
}

注意,查询变量现在在这里传递,我们在启动请求时传递。

此时,您应该看到与以前相同的行为,但现在会稍微快一点,因为 Relay 可以更早地启动查询。

提示

虽然我们为了简单起见引入了使用 useLazyLoadQuery 的查询,但预加载查询始终是使用 Relay 中查询的首选方法,因为它们可以在现实世界中显着提高性能。通过与您的服务器和路由器系统进行适当的集成,您甚至可以在下载或运行任何客户端代码之前在服务器端预加载网页的主要查询。


总结

  • 虽然最初显示在屏幕上的所有数据都应该合并到一个查询中,但需要更多信息的 用户交互可以使用辅助查询处理。
  • 查询变量允许您将信息与查询一起传递给服务器。
  • 查询变量通过将它们传递到字段参数中来使用。
  • 预加载查询始终是最佳选择。对于用户交互查询,在事件处理程序中启动获取。对于屏幕的初始查询,尽早地在您的特定路由系统中启动获取。仅将延迟加载查询用于快速原型设计,或根本不使用。

接下来,我们将简要介绍一种通过不同方式处理不同类型的海报来增强悬停卡的方法。之后,我们将看到如何处理需要更新和重新获取具有不同变量的初始查询的一部分信息的场景。