Relay:一个愿意为你做脏活的 GraphQL 客户端 Relay 快速概览 Relay 的核心:片段 Relay 如何增强 GraphQL 片段 构建页面来呈现博客文章 总结我们对 Relay 的介绍 特别感谢

2025-06-07

Relay:为你做脏活的 GraphQL 客户端

Relay 快速概览

Relay 的核心:片段

Relay 如何增强 GraphQL 片段

构建页面来呈现博客文章

总结一下 Relay 的介绍

特别感谢

本系列文章由Gabriel NordebornSean Grove撰写。Gabriel 是瑞典 IT 咨询公司Arizon的前端开发人员兼合伙人,长期使用 Relay。Sean 是OneGraph.com的联合创始人,致力于使用 GraphQL 统一第三方 API。

这一系列文章将深入探讨 Relay,以明确回答一个问题:

我到底为什么要关心 Relay,这个 Facebook 使用 GraphQL 构建应用程序的 JavaScript 客户端框架?

毫无疑问,这是一个好问题。为了回答这个问题,我们将带您了解构建一个用于渲染博客的简单页面的各个部分。在构建页面时,我们会看到两个主要主题:

  1. 事实上,Relay 是一个十足的苦力,它愿意为你做脏活。
  2. 如果您遵循 Relay 制定的约定,Relay 将为您带来使用 GraphQL 构建客户端应用程序的绝佳开发体验。

我们还将向您展示 Relay 应用程序默认具有可扩展性、高性能、模块化和可变化性并且使用它构建的应用程序可以适应目前正在开发的 React 新功能的未来发展。

Relay 会产生一系列(相对较小的)成本,我们会预先诚实地检查这些成本,以便人们能够很好地理解这些成本的利弊。

设置舞台

本文旨在展示Relay的理念和哲学。虽然我们偶尔会将 Relay 与其他 GraphQL 框架的功能进行比较,但本文的主要目的并非比较 Relay 与其他框架。我们想深入探讨Relay本身,解释它的哲学以及使用它构建应用程序所涉及的概念。

这也意味着本文中的代码示例(有几个!)仅用于说明 Relay 的工作原理,这意味着它们有时可能有点浅显和简单。

我们还将重点介绍Relay 的全新基于 hooks 的 API,这些 API 已完全支持 React 的 Suspense 和并发模式。虽然这些新 API 仍处于实验阶段,但 Facebook 正在使用 Relay 和上述 API 重建 facebook.com,专门用于数据层。

另外,在开始之前,本文假设您已经基本熟悉 GraphQL 以及如何构建客户端 JavaScript 应用程序。如果您觉得还不够熟悉,这里有一篇关于 GraphQL 的精彩介绍。代码示例将使用 TypeScript 编写,因此对 TypeScript 的基本了解也会有所帮助。

最后,这篇文章很长。可以把它当作一篇参考文章,以后可以随时回顾。

了解完所有免责声明后,我们就开始吧!

Relay 快速概览

Relay 由优化 GraphQL 代码的编译器和与 React 一起使用的库组成。

在深入探讨之前,我们先来快速了解一下 Relay。Relay 可以分为两部分:

  1. 编译:负责各种优化、类型生成,并提供出色的开发者体验。在开发过程中,它会在后台运行。
  2. :Relay 的核心,以及使用 Relay 与 React 的绑定。

目前为止,你需要了解的是,编译器是一个由你启动的独立进程,用于监视和编译所有 GraphQL 操作。不过,你很快就会了解更多相关信息。

除此之外,为了使 Relay 能够最佳地工作,它希望你的模式遵循三个约定:

  • 类型的所有id字段都应该是全局唯一的(即,没有两个对象 - 甚至两种不同类型的对象 - 可以共享相同的id值)
  • 接口Node,意思是:图中的对象应该可以通过其id顶级字段获取。点击此处node,了解更多关于全局唯一 ID 和Node接口(以及它的优势!)的信息
  • 分页应该遵循基于连接的分页标准。阅读本文,了解更多关于基于连接的分页是什么以及为什么它是一个好主意。

目前我们不会深入探讨这些惯例,但如果您有兴趣,我们鼓励您查看上面链接的文章。

Relay 的核心:片段

我们先来聊聊 Relay 与 GraphQL 集成的核心概念:片段 (Fragments)。毕竟,它是 Relay(以及 GraphQL!)强大功能的关键之一。

简而言之,GraphQL 中的片段是一种将特定 GraphQL 类型的常见选择组合在一起的方法。以下是示例:

    fragment Avatar_user on User {
      avatarUrl
      firstName
      lastName
    }
Enter fullscreen mode Exit fullscreen mode

好奇的读者:Avatar_userRelay 强制执行片段命名的惯例。Relay 要求所有片段名称都全局唯一,并遵循 的结构。您可以在此处<moduleName>_<propertyName>阅读有关片段命名惯例的更多信息,我们稍后会讨论为什么这很有用。

这定义了一个名为 的片段Avatar_user,可以与 GraphQL 类型 一起使用User。该片段选择渲染头像通常所需的内容。然后,您可以在整个查询中重复使用该片段,而不必在每个需要渲染头像的地方显式地选择所有需要的字段:

    # Instead of doing this when you want to render the avatar for the author 
    # and the first two who liked the blog post...
    query BlogPostQuery($blogPostId: ID!) {
      blogPostById(id: $blogPostId) {
        author {
          firstName
          lastName
          avatarUrl
        }
        likedBy(first: 2) {
          edges {
            node {
              firstName
              lastName
              avatarUrl
            }
          }
        }
      }
    }

    # ...you can do this
    query BlogPostQuery($blogPostId: ID!) {
      blogPostById(id: $blogPostId) {
        author {
          ...Avatar_user
        }
        likedBy(first: 2) {
          edges {
            node {
              ...Avatar_user
            }
          }
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

这很方便,因为它允许重复使用定义,但更重要的是,它允许您在应用程序演变时在单个地方添加和删除渲染头像所需的字段

片段允许您定义 GraphQL 类型上可重复使用的字段选择。

Relay 加倍关注碎片

为了能够随着时间的推移扩展 GraphQL 客户端应用程序,最好尝试将数据需求与渲染数据的组件放在一起。这将使组件的维护和扩展更加容易,因为组件及其使用的数据都在一个地方进行推理。

由于 GraphQL 片段允许您在特定 GraphQL 类型上定义字段的子选择(如上所述),因此它们完全符合共置理念。

因此,一个很好的做法是定义一个或多个片段来描述组件需要渲染的数据。这意味着组件可以说:“User无论我的父组件是谁,我都依赖于该类型的这三个字段。” 在上面的例子中,有一个名为 的组件<Avatar />将使用片段中定义的字段来显示头像Avatar_user

现在,大多数框架都允许你以某种方式使用 GraphQL 片段。但 Relay 更进一步。在 Relay 中,几乎所有内容都围绕着片段展开

Relay 如何增强 GraphQL 片段

Relay 的核心理念是希望每个组件都能拥有一份完整、明确的数据需求清单,并将其与组件本身一并列出。这使得 Relay 能够与 Fragment 深度集成。让我们来详细分析一下这意味着什么,以及它能够实现哪些功能。

共置数据要求和模块化

使用 Relay,您可以使用片段将组件的数据需求放在实际使用它的代码旁边。遵循 Relay 的约定可以保证每个组件都明确列出其需要访问的每个字段。这意味着任何组件都不会依赖于其未明确请求的数据,从而使组件在重用和重构方面具有模块化、自包含和弹性。

Relay 还通过使用片段做了很多额外的事情来实现模块化,我们将在本文稍后讨论。

表现

在 Relay 中,组件仅当其使用的字段发生变化时才会重新渲染- 您无需执行任何操作!这是因为每个片段只会订阅其所选数据的更新。

这使得 Relay 能够优化视图的默认更新方式,确保性能不会随着应用规模的增长而出现不必要的下降。这与其他 GraphQL 客户端的操作方式截然不同。如果您还不太理解,也不用担心,我们将在下面展示一些很棒的示例,并说明它对于可扩展性的重要性。

考虑到所有这些,让我们开始构建我们的页面!

Relay 加倍强调了片段的概念,并使用它们来实现数据需求的共置、模块化和出色的性能。

构建页面来呈现博客文章

以下是显示单篇博客文章的页面线框图:

首先,我们来思考一下如何通过一个顶级查询获取此视图的所有数据。一个满足线框需求的合理查询可能如下所示:

    query BlogPostQuery($blogPostId: ID!) {
      blogPostById(id: $blogPostId) {
        author {
          firstName
          lastName
          avatarUrl
          shortBio
        }
        title
        coverImgUrl
        createdAt
        tags {
          slug
          shortName
        }
        body
        likedByMe
        likedBy(first: 2) {
          totalCount
          edges {
            node {
              firstName
              lastName
              avatarUrl
            }
          }
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

一次查询就能获取我们需要的所有数据!太棒了!

反过来,UI 组件的结构可能看起来像这样:

    <BlogPost>
      <BlogPostHeader>
        <BlogPostAuthor>
          <Avatar />
        </BlogPostAuthor>
      </BlogPostHeader>
      <BlogPostBody>
        <BlogPostTitle />
        <BlogPostMeta>
          <CreatedAtDisplayer />
          <TagsDisplayer />
        </BlogPostMeta>
        <BlogPostContent />
        <LikeButton>
          <LikedByDisplayer />
        </LikeButton>
      </BlogPostBody>
    </BlogPost>
Enter fullscreen mode Exit fullscreen mode

让我们看看如何在 Relay 中构建它。

在 Relay 中查询数据

在 Relay 中,呈现博客文章的根组件通常看起来像这样:

    // BlogPost.ts
    import * as React from "react";
    import { useLazyLoadQuery } from "react-relay/hooks";
    import { graphql } from "react-relay";
    import { BlogPostQuery } from "./__generated__/BlogPostQuery.graphql";
    import { BlogPostHeader } from "./BlogPostHeader";
    import { BlogPostBody } from "./BlogPostBody";

    interface Props {
      blogPostId: string;
    }

    export const BlogPost = ({ blogPostId }: Props) => {
      const { blogPostById } = useLazyLoadQuery<BlogPostQuery>(
        graphql`
          query BlogPostQuery($blogPostId: ID!) {
            blogPostById(id: $blogPostId) {
              ...BlogPostHeader_blogPost
              ...BlogPostBody_blogPost
            }
          }
        `,
        {
          variables: { blogPostId }
        }
      );

      if (!blogPostById) {
        return null;
      }

      return (
        <div>
          <BlogPostHeader blogPost={blogPostById} />
          <BlogPostBody blogPost={blogPostById} />
        </div>
      );
    };
Enter fullscreen mode Exit fullscreen mode

让我们一步一步分析一下这里发生的事情。

      const { blogPostById } = useLazyLoadQuery<BlogPostQuery>(
        graphql`
          query BlogPostQuery($blogPostId: ID!) {
            blogPostById(id: $blogPostId) {
              ...BlogPostHeader_blogPost
              ...BlogPostBody_blogPost
            }
          }
        `,
        {
          variables: { blogPostId }
        }
      );
Enter fullscreen mode Exit fullscreen mode

首先要注意的是useLazyLoadQuery来自 Relay 的 React hook:。
const { blogPostById } = useLazyLoadQuery<BlogPostQuery>在组件渲染后立即useLazyLoadQuery开始获取。BlogPostQuery

为了确保类型安全,我们使用注释明确声明从 导入的useLazyLoadQuery类型。该文件由 Relay 编译器自动生成(并与查询定义的更改保持同步),包含查询所需的所有类型信息——返回的数据是什么样子的,以及查询需要哪些变量。BlogPostQuery./__generated__/BlogPostQuery.graphql

免责声明时间!:如上所述,useLazyLoadQuery将在渲染后立即开始获取查询。但是,请注意, Relay 实际上不希望您像这样在渲染时延迟获取数据。相反,Relay 希望您尽快开始加载查询,例如在用户点击新页面的链接时,而不是在页面渲染时。为什么这一点如此重要,在本博客文章本演讲中进行了详细讨论,我们强烈建议您阅读和观看。
但我们仍然在本文中使用延迟加载变体,因为它对大多数人来说是一种更熟悉的心理模型,并且尽可能使事情保持简单易懂。但是,请注意,如上所述,这不是在使用 Relay 进行实际构建时应该获取查询数据的方式。

接下来,我们有实际的查询:

    graphql`
      query BlogPostQuery($blogPostId: ID!) {
        blogPostById(id: $blogPostId) {
          ...BlogPostHeader_blogPost
          ...BlogPostBody_blogPost
      }
    }`
Enter fullscreen mode Exit fullscreen mode

定义我们的查询,上面演示的示例查询其实就剩下不多了。除了通过 id 选择博客文章之外,就只有两个选择—— for<BlogPostHeader /><BlogPostBody />on片段BlogPost

请注意,我们不需要导入正在使用的片段。Relay 编译器会自动包含这些片段。稍后会详细介绍。

像这样将片段组合在一起来构建查询非常重要。另一种方法是让组件定义自己的查询,并完全负责获取自己的数据。虽然这种方法有一些有效的用例,但这会带来两个主要问题:

  • 大量查询(而不仅仅是一个)被发送到您的服务器。
  • 每个组件在执行查询时都需要等到实际渲染完成后才能开始获取数据。这意味着视图的加载速度可能会比预期慢很多,因为请求可能会以瀑布式的方式发出。

在 Relay 中,我们通过组合组件来构建 UI。这些组件以不透明的方式自行定义所需的数据。

Relay 如何强制模块化

以下是理解上述代码时需要牢记的思维模型:

作为BlogPost组件,我只知道要渲染两个子组件BlogPostHeaderBlogPostBody。我不知道它们需要什么数据(我为什么要知道?这是它们的责任!)。
相反,它们告诉我它们所需的所有数据都在一个名为BlogPostHeader_blogPost和 的GraphQL 类型片段BlogPostBody_blogPostBlogPost。只要我在查询中包含它们的片段,即使我不知道任何具体细节,我也知道一定能获得它们所需的数据。当我获得它们所需的数据后,我就可以渲染它们了。

我们通过组合组件来构建 UI,这些组件各自独立定义数据需求。然后,这些组件可以与其他具有各自数据需求的组件组合在一起。然而,除了组件需要从哪个 GraphQL 源(类型)获取数据之外,没有任何组件真正了解其他组件需要什么数据。Relay 负责处理这些繁琐的工作,确保正确的组件获取正确的数据,并确保在发送到服务器的查询中选择所有需要的数据。

这允许您(开发人员)单独地考虑组件片段,而 Relay 会为您处理所有管道。

继续前进!

Relay 编译器知道你在项目中定义的所有 GraphQL 代码

请注意,虽然查询引用了两个片段,但无需告知查询这些片段的定义位置或文件,也无需手动将它们导入查询。这是因为 Relay 强制每个片段使用全局唯一的名称,以便 Relay 编译器可以自动将片段定义包含在发送到服务器的任何查询中。

手动引用片段定义是另一个不方便、手动且容易出错的步骤,但使用 Relay 后,这不再是开发人员的责任。

使用与组件紧密耦合的片段允许 Relay 对外界隐藏组件的数据需求,从而实现高度的模块化和安全的重构。

最后,我们来呈现结果:

      // Because we spread both fragments on this object
      // it's guaranteed to satisfy both `BlogPostHeader`
      // and `BlogPostBody` components.
      if (!blogPostById) {
        return null;
      }

      return (
        <div>
          <BlogPostHeader blogPost={blogPostById} />
          <BlogPostBody blogPost={blogPostById} />
        </div>
      );
Enter fullscreen mode Exit fullscreen mode

这里我们渲染<BlogPostHeader /><BlogPostBody />。仔细观察,你会发现我们通过向它们传递blogPostById对象来渲染它们。这就是查询中用于展开片段的对象。这是使用 Relay 传输片段数据的方式——将已展开片段的对象传递给使用片段的组件,然后组件使用该片段获取实际的片段数据。别担心,Relay 不会让您失望。通过类型系统,Relay 将确保您传递正确的对象及其上展开的正确片段。稍后将详细介绍这一点。

呼,这可真是新东西!不过,我们已经看到并扩展了 Relay 为我们提供的诸多帮助——这些事情通常需要我们手动完成,而且没有任何额外的好处。

遵循 Relay 的约定可以确保组件在未获得所需数据的情况下无法渲染。这意味着您将很难将有问题的代码交付到生产环境。

让我们继续沿着组件树向下移动。

使用片段构建组件

这是的代码<BlogPostHeader />

    // BlogPostHeader.ts
    import * as React from "react";
    import { useFragment } from "react-relay/hooks";
    import { graphql } from "react-relay";
    import {
      BlogPostHeader_blogPost$key,
      BlogPostHeader_blogPost
    } from "./__generated__/BlogPostHeader_blogPost.graphql";
    import { BlogPostAuthor } from "./BlogPostAuthor";
    import { BlogPostLikeControls } from "./BlogPostLikeControls";

    interface Props {
      blogPost: BlogPostHeader_blogPost$key;
    }

    export const BlogPostHeader = ({ blogPost }: Props) => {
      const blogPostData = useFragment<BlogPostHeader_blogPost>(
        graphql`
          fragment BlogPostHeader_blogPost on BlogPost {
            title
            coverImgUrl
            ...BlogPostAuthor_blogPost
            ...BlogPostLikeControls_blogPost
          }
        `,
        blogPost
      );

      return (
        <div>
          <img src={blogPostData.coverImgUrl} />
          <h1>{blogPostData.title}</h1>
          <BlogPostAuthor blogPost={blogPostData} />
          <BlogPostLikeControls blogPost={blogPostData} />
        </div>
      );
    };
Enter fullscreen mode Exit fullscreen mode

我们这里的示例每个组件仅定义一个片段,但一个组件可以在任意数量的 GraphQL 类型上定义任意数量的片段,包括同一类型的多个片段。

让我们来分析一下。

    import {
      BlogPostHeader_blogPost$key,
      BlogPostHeader_blogPost
    } from "./__generated__/BlogPostHeader_blogPost.graphql";
Enter fullscreen mode Exit fullscreen mode

我们从文件中导入两个类型定义BlogPostHeader_blogPost.graphql,由 Relay 编译器为我们自动生成。

Relay 编译器将从此文件中提取 GraphQL 片段代码,并从中生成类型定义。实际上,它会为您在项目中编写的所有与 Relay 一起使用的 GraphQL 代码(查询、突变、订阅和片段)执行此操作。这也意味着编译器会自动将类型与片段定义的任何更改保持同步。

BlogPostHeader_blogPost包含片段的类型定义,我们将其传递给useFragmentuseFragment我们很快会详细讨论)以确保与片段数据的交互是类型安全的。

但是BlogPostHeader_blogPost$key第 12 行到底是什么interface Props { … }?!嗯,它和类型安全有关。你现在真的不必担心这个,但为了满足你的好奇心,我们还是把它分解一下(剩下的可以直接跳到下一个标题):

该类型定义通过某种暗黑魔法确保你只能将正确的对象(BlogPostHeader_blogPost片段展开的位置)传递给useFragment,否则你将在构建时(在编辑器中!)遇到类型错误。如你所见,我们从 props 中获取值并将其作为第二个参数blogPost传递给。如果没有正确的片段()展开,我们就会收到类型错误。useFragmentblogPostBlogPostHeader_blogPost

即使该对象上已经存在具有完全相同数据选择的另一个片段,Relay 也会确保它就是您想要使用的那个片段。这一点很重要,因为 Relay 以另一种方式保证您可以更改片段定义,而不会隐useFragment地影响任何其他组件。

Relay 消除了另一个潜在错误来源:传递包含正确片段的正确对象。

您只能使用明确要求的数据

BlogPostHeader_blogPost我们在 上定义片段BlogPost。请注意,我们为此组件明确选择了两个字段:

- `title`
- `coverImgUrl`
Enter fullscreen mode Exit fullscreen mode

这是因为我们在这个特定的组件中使用这些字段。这凸显了 Relay 的另一个重要特性——数据屏蔽。即使BlogPostAuthor_blogPost我们接下来要扩展的片段也选择了titlecoverImgUrl(这意味着它们必须在我们获取它们的确切位置的查询中可用),除非我们通过我们自己的片段明确请求它们,否则我们无法访问它们。

这在类型级别(生成的类型不会包含它们)运行时都强制执行 - 即使绕过类型系统,这些值也不会存在。

乍一看可能有点奇怪,但实际上这是 Relay 的另一个安全机制。如果你知道其他组件不可能隐式依赖你选择的数据,那么你就可以重构组件,而不会冒着以奇怪的、意想不到的方式破坏其他组件的风险。随着应用的增长,这非常有用——再次强调,每个组件及其数据需求都将变得完全独立。

强制明确定义组件所需的所有数据意味着您不会通过从其他组件所依赖的查询或片段中删除字段选择而意外破坏您的 UI。

      const blogPostData = useFragment<BlogPostHeader_blogPost>(
        graphql`
          fragment BlogPostHeader_blogPost on BlogPost {
            title
            coverImgUrl
            ...BlogPostAuthor_blogPost
            ...BlogPostLikeControls_blogPost
          }
        `,
        blogPost
      );
Enter fullscreen mode Exit fullscreen mode

在这里我们使用 React hookuseFragment来获取片段的数据。useFragment知道如何获取片段定义(在标签内定义的片段定义)和该片段所传播的graphql对象这里来自),并使用它来获取该特定片段的数据。blogPostprops

只是重申这一点 - 该片段(title/ coverImgUrl)没有blogPost来自 props 的数据 - 该数据仅在我们useFragment使用片段定义和blogPost片段已传播的对象进行调用时可用。

而且,就像以前一样,我们传播我们想要渲染的组件的片段 - 在这种情况下,BlogPostAuthor_blogPost因为BlogPostLikeControls_blogPost我们正在渲染<BlogPostAuthor /><BlogPostLikeControls />

好奇的读者可能会问:由于片段仅描述要选择的字段,因此useFragment不会向 GraphQL API 发出实际的数据请求。相反,片段必须在某个时刻出现在查询(或其他 GraphQL 操作)中,才能获取其数据。话虽如此,Relay 有一些非常酷的功能,可以让您自行重新获取片段。这是因为 Relay 可以自动生成查询,以便您根据其 重新获取特定的 GraphQL 对象id。好了,我们跑题了……

此外,如果您了解 Redux,您可以将其比作useFragment一个选择器,它可以让您从状态树中仅获取所需的内容。

      return (
        <div>
          <img src={blogPostData.coverImgUrl} />
          <h1>{blogPostData.title}</h1>
          <BlogPostAuthor blogPost={blogPostData} />
          <BlogPostLikeControls blogPost={blogPostData} />
        </div>
      );
Enter fullscreen mode Exit fullscreen mode

然后,我们渲染明确请求的数据(coverImgUrltitle),并将两个子组件的数据传递给它们,以便它们进行渲染。再次注意,我们将对象传递给了我们展开其片段的组件,该对象位于此组件定义和使用的片段的根目录BlogPostHeader_blogPost

Relay 如何确保您保持高性能

使用片段时,每个片段将仅订阅其实际使用的数据的更新。这意味着,上面的组件仅在其渲染的特定博客文章更新时才会自行重新渲染。如果选择<BlogPostHeader />其他字段并且这些字段也进行了更新,则此组件仍然不会重新渲染。数据的更改是在片段级别订阅的。coverImgUrltitleBlogPostAuthor_blogPost

这乍一听可能有点令人困惑,而且可能没什么用,但它对性能至关重要。让我们通过对比客户端处理 GraphQL 数据时通常的处理方式来更深入地了解这一点。

使用 Relay,只有使用已更新数据的组件才会在数据更新时重新渲染。

你认为数据从何而来?Relay 与其他框架的对比

您在视图中使用的所有数据都必须源自从服务器获取数据的实际操作,例如查询。您定义一个查询,让您的框架从服务器获取它,然后在视图中渲染所需的任何组件,并向下传递它们所需的数据。大多数 GraphQL 框架的数据源是查询。数据从查询流向组件。以下是其他 GraphQL 框架中通常如何执行此操作的示例(箭头表示数据流向):

注意:框架数据存储通常被称为许多框架中的缓存。本文假设"framework data store" === cache

该流程看起来类似于:

  1. <Profile />并向query ProfileQueryGraphQL API 发出请求
  2. 响应以某种方式存储在特定于框架的数据存储中(读取:缓存)
  3. 数据被传递到视图进行渲染
  4. 然后,视图继续将数据片段传递给需要它的后代组件(AvatarNameBio等等)。最后,您的视图被渲染

Relay 如何实现

现在,Relay 的做法截然不同。我们来看看 Relay 的示意图:

有何不同?

  • 初始流程大部分相同——向 GraphQL API 发出查询,数据最终进入框架数据存储。但随后情况开始发生变化。
  • 请注意,所有使用数据的组件都直接从 数据存储(缓存)获取数据。这是由于 Relay 与 Fragment 的深度集成 - 在您的 UI 中,每个 Fragment 都直接从框架数据存储中获取自己的数据,而不依赖 从其数据来源的查询中传递给它的实际数据。
  • 箭头从查询组件向下指向其他组件。我们仍然会将查询中的一些信息传递给片段,以便它从数据存储中查找所需的数据。但我们不会将任何实际数据传递给片段,所有实际数据都由片段自己从数据存储中检索。

好了,以上内容已经深入介绍了 Relay 和其他 GraphQL 框架的工作原理。为什么需要关注这一点?因为这个设置可以实现一些非常巧妙的功能。

其他框架通常使用查询作为数据源,并依赖于您将数据沿着树向下传递到其他组件。Relay 则相反,它允许每个组件从数据存储本身获取所需的数据。

免费演出

试想一下:当查询作为数据源时,任何影响该查询数据的数据存储更新都会强制重新渲染包含该查询的组件,因此更新后的数据可以流向任何可能使用它的组件。这意味着对数据存储的更新会导致重新渲染,而这些重新渲染必须通过层层递进的组件,而这些组件实际上与更新无关,只是从父组件获取数据并传递给子组件。

Relay 的方法是每个组件直接从存储中获取所需的数据,并仅订阅其使用的精确数据的更新,这确保了即使我们的应用程序的规模和复杂性不断增长,我们也能保持良好的性能。

在使用订阅时,这一点也很重要。Relay 确保来自订阅的更新数据只会导致实际使用该更新数据的组件重新渲染。

使用Query作为数据源意味着当 GraphQL 缓存更新时,整个组件树将被强制重新渲染。

模块化和隔离意味着您可以安全地重构

将数据从查询路由到实际需要该数据的组件的责任从开发人员身上移除,也消除了开发人员搞砸事情的另一个机会。如果您无法访问数据,就根本不可能意外地(或者更糟的是,故意地)依赖那些应该在组件树中传递的数据。Relay 再次确保它在可能的情况下为您完成繁重的工作。

使用 Relay 及其片段优先方法意味着很难弄乱组件树中的数据流。

当然,需要注意的是,“查询作为数据源”方法的大多数缺点可以通过老式的手动优化等方式来缓解React.memoshouldComponentUpdate但这本身就可能存在性能问题,而且容易出错(任务越繁琐,人为操作最终就越有可能搞砸)。而 Relay 则可以确保您保持高性能,而无需您费心。

每个从缓存中接收自己的数据的组件还可以启用 Relay 的一些非常酷的高级功能,例如使用存储中已有的数据部分渲染视图,同时等待视图的完整数据返回。

总结片段

让我们在这里停下来,消化一下 Relay 为我们做了什么类型的工作:

  • 通过类型系统,Relay 确保了该组件在没有来自 GraphQL 的正确对象(包含其数据)的情况下无法渲染。这样一来,我们就能少犯一次错。
  • 由于每个使用片段的组件仅在其使用的精确数据更新时才会更新,因此在 Relay 中对缓存的更新默认是高效的。
  • 通过类型生成,Relay 确保与此片段数据的任何交互都是类型安全的。值得强调的是,类型生成是 Relay 编译器的核心功能。

Relay 的架构和理念充分利用了计算机所能获取的组件信息,从组件的数据依赖关系到服务器提供的数据及其类型。它利用所有这些信息以及更多资源来完成我们这些本来就忙得不可开交的开发者通常需要处理的各种工作。

人们很容易低估视图复杂度的提升速度。Relay 默认通过强制遵循的约定来处理复杂性和性能问题。

这为开发人员带来了一些真正的力量:

  • 您可以构建几乎完全隔离的可组合组件。
  • 重构您的组件将是完全安全的,并且 Relay 将确保您不会遗漏任何东西或搞乱一切。

一旦开始构建大量可复用组件,这一点就显得尤为重要。确保大量代码库中使用的重构组件的安全,对于开发人员的开发速度至关重要。

随着应用程序的增长,重构的简易性和安全性对于继续快速发展变得至关重要。

总结一下 Relay 的介绍

我们已经在本文中涵盖了很多内容。如果您能从中有所收获,那就是 Relay能够帮助您构建可扩展、高性能、类型安全的应用程序,并且这些应用程序易于维护和重构,并且安全可靠。

Relay 确实能帮你完成这些繁琐的工作。虽然我们展示的很多功能可以通过其他框架的不懈努力来实现,但我们希望我们已经展示了实施这些模式所能带来的强大优势。它们的重要性怎么强调都不为过。

一款出色的软件

Relay 确实是一款了不起的软件,它是通过长期使用 GraphQL 运送和维护产品所付出的血汗、泪水,以及最重要的经验和深刻洞察力构建而成的。

虽然这篇文章很长,内容也相当丰富,但我们对 Relay 的功能还只是略知皮毛。最后,让我们列出一些 Relay 的功能,看看它们是否能帮到我们:

  • 乐观且复杂的缓存更新突变
  • 订阅
  • 与 Suspense 和 Concurrent Mode 完全集成(并充分利用)——为下一代 React 做好准备
  • 使用 Relay 通过 Relay 管理您的本地状态,享受使用 Relay 进行本地状态管理的一般好处(例如与 Suspense 和 Concurrent Mode 集成!)
  • 通过以下方式流式传输列表结果@stream
  • 推迟可能需要很长时间才能加载的服务器响应部分@defer,以便其余 UI 可以更快地呈现
  • 自动生成用于重新获取片段和分页的查询
  • 复杂的缓存管理;控制允许获取的缓存大小,以及视图的数据是否应该从缓存或网络(或两者,或先从缓存,然后从网络)解析
  • 稳定、成熟、灵活的缓存,Just Works (tm)
  • 当用户指示导航即将发生时,立即预加载新视图的查询 _ 使用存储中已有的数据部分渲染视图,同时等待查询数据到达
  • 定义片段的参数(就像组件的 props 一样),将组件的可组合性提升到一个新的水平
  • 教会 Relay 更多关于图表中数据如何连接而不是从架构中得出的内容,这样它就可以从缓存中解析更多数据(想想“这些带有这些变量的顶级字段解析同一个用户”)

本文到此结束,但我们强烈建议您继续阅读有关 Relay 分页功能的文章。Relay 分页功能以精美的方式整合了 Relay 的强大功能,展示了当框架完成所有繁重工作时,可以实现多么高的自动化程度以及令人难以置信的 DX 能力。点击此处阅读

您还可以继续阅读以下其他几篇文章:

感谢您的阅读!

特别感谢

非常感谢 Xavier Cazalot、Arnar Þór Sveinsson、Jaap Frolich、Joe Previte、Stepan Parunashvili 和 Ben Sangster 对本文草稿的详细反馈!

文章来源:https://dev.to/zth/relay-the-graphql-client-that-wants-to-do-the-dirty-work-for-you-55kd
PREV
是时候告别 Google 字体了:缓存性能
NEXT
使用 RobotJS 实现 NodeJS 桌面自动化(但这个程序可能会让你被录用或被解雇😄)