Relay 学习笔记

🌙
手机阅读
本文目录结构

Relay 简介

Relay是一个JavaScript框架,用于构建由GraphQL支持的数据驱动的React应用程序,该应用程序从头开始设计,易于使用,可扩展,并且最重要的是具有高性能。中继通过静态查询和提前代码生成来完成此任务。

React允许将视图定义为组件,其中每个组件都负责呈现UI的一部分。组成其他组件是如何构建复杂的UI。每个React组件不需要知道组成组件的内部工作原理。

继电器将React与GraphQL耦合在一起,并进一步发展了封装的思想。它允许组件指定所需的数据,而Relay框架提供数据。这使内部组件的数据需求变得不透明,并允许组合这些需求。考虑到应用程序需要哪些数据后,该信息便会本地化到组件,从而更容易推断出需要或不再需要哪些字段。

官方资料

前置条件

react反应

Relay是用于数据管理的框架,具有对React应用程序的主要支持绑定,因此我们假设您已经熟悉React

GraphQL

我们还假设对 GraphQL 有基本的了解。为了开始使用中继,您还需要:

GraphQL模式

数据模型的描述以及一组关联的解析方法,这些解析方法知道如何获取应用程序可能需要的任何数据。GraphQL旨在支持多种数据访问模式。为了了解应用程序数据的结构,Relay要求您在定义架构时遵循某些约定。这些在GraphQL服务器规范中有记录。

  • npm上的 graphql-js
    • 使用JavaScript构建GraphQL模式的通用工具
  • npm上的** graphql-relay-js**
  • JavaScript辅助程序,用于以平稳的方式与Relay集成的方式定义数据与变异之间的连接。

GraphQL服务器

可以教会任何服务器加载架构并说出GraphQL。我们的示例使用Express。

安装

使用yarn或安装React和Relay npm:

yarn add react react-dom react-relay

使用单个配置文件设置中继

的下面配置babel-plugin-relay和relay-compiler可使用单个配置文件通过使用被应用relay-config程序包。除了在一个地方统一所有中继配置之外,其他工具也可以利用它来提供零配置设置(例如vscode-apollo-relay)。

安装软件包:

yarn add --dev relay-config

并创建配置文件:

// relay.config.js
module.exports = {
  // ...
  // Configuration options accepted by the `relay-compiler` command-line tool and `babel-plugin-relay`.
  src: "./src",
  schema: "./data/schema.graphql",
  exclude: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"],
}

设置babel-plugin-relay

Relay Modern需要Babel插件才能将GraphQL转换为运行时工件:

yarn add --dev babel-plugin-relay graphql

将"relay"插件添加到插件列表中.babelrc:

{
  "plugins": [
    "relay"
  ]
}

请注意,该"relay"插件应在其他插件或预设之前运行,以确保graphql模板文字正确转换。请参阅有关此主题的 Babel 文档。

或者,babel-plugin-relay可以使用Relay与babel-plugin-macros来代替使用。安装babel-plugin-macros并将其添加到您的Babel配置后:

const graphql = require('babel-plugin-relay/macro');

如果需要babel-plugin-relay进一步配置(例如,启用compat模式),则可以通过多种方式指定选项来进行配置。

例如:

// babel-plugin-macros.config.js
module.exports = {
  // ...
  // Other macros config
  relay: {
    compat: true,
  },
}

设置中继编译器

Relay的提前编译需要Relay Compiler,您可以通过yarn或安装npm:

yarn add --dev relay-compiler

这会将bin脚本安装relay-compiler在您的node_modules文件夹中。建议通过将脚本添加到文件中的yarn/ npm脚本来运行此脚本package.json:

"scripts": {
  "relay": "relay-compiler --src ./src --schema ./schema.graphql"
}

或者,如果您使用的是jsx:

“scripts”: { “relay”: “relay-compiler –src ./src –schema ./schema.graphql –extensions js jsx” } 然后,在对应用程序文件进行编辑之后,只需运行relay脚本以生成新的已编译工件:

yarn run relay

或者,您可以传递该–watch选项以监视源代码中的文件更改,并自动重新生成已编译的工件(注意:需要安装监视程序):

yarn run relay --watch

有关更多详细信息,请查看我们的Relay Compiler文档。

JavaScript环境要求

在NPM上分发的Relay Modern软件包使用广泛支持的JavaScript ES5版本的JavaScript,以支持尽可能多的浏览器环境。

然而,继电器现代预计现代JavaScript的全局类型(Map,Set, Promise,Object.assign)来定义。如果您支持可能还没有原生提供这些功能的旧版浏览器和设备,请考虑在捆绑的应用程序中包括全局polyfill,例如core-js或 @ babel / polyfill。

使用core-js支持旧版浏览器的Relay的多填充环境可能如下所示:

require('core-js/es6/map');
require('core-js/es6/set');
require('core-js/es6/promise');
require('core-js/es6/object');

require('./myRelayApplication');

快速入门

设定

在开始之前,请确保查看我们的先决条件以及安装和设置指南。如前提条件中所述,我们需要确保已设置GraphQL服务器和架构。

幸运的是,我们将使用此示例待办事项列表应用程序,该应用程序已经具有供我们使用的服务器和架构:

# From schema.graphql
# https://github.com/relayjs/relay-examples/blob/master/todo/data/schema.graphql

type Query {
  viewer: User

  # Fetches an object given its ID
  node(
    # The ID of an object
    id: ID!
  ): Node
}

此外,我们将在Javascript代码示例中使用Flow。在您的项目中设置流程是可选的,但是为了完整起见,我们将在示例中包括它。

中继环境

在开始在屏幕上渲染像素之前,我们需要通过Relay Environment配置Relay 。该环境将Relay进行操作所需的配置,缓存存储和网络处理捆绑在一起。

就我们的示例而言,我们将简单地配置环境以与现有的GraphQL服务器通信:

import {
  Environment,
  Network,
  RecordSource,
  Store,
} from 'relay-runtime';

function fetchQuery(
  operation,
  variables,
) {
  return fetch('/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: operation.text,
      variables,
    }),
  }).then(response => {
    return response.json();
  });
}

const environment = new Environment({
  network: Network.create(fetchQuery),
  store: new Store(new RecordSource()),  
});

export default environment;

中继环境至少需要一个存储层和一个网络层。上面的代码使用的默认实现Store,并使用一个简单的函数从我们的服务器中获取GraphQL查询来创建网络层fetchQuery。

通常,我们希望在我们的应用程序中使用一个环境,因此您可以将该环境作为一个单例实例从模块导出,以使其可在您的应用程序中访问。

渲染GraphQL查询

现在我们已经配置了中继环境,我们可以开始获取查询并在屏幕上呈现数据。从GraphQL查询呈现数据的入口点是所QueryRenderer提供的组件react-relay。

首先,假设我们只想在屏幕上呈现用户ID。从我们的架构中,我们知道我们可以User通过该viewer字段获取当前信息,因此让我们编写一个示例查询来获取当前用户ID:

query UserQuery {
  viewer {
    id
  }  
}

现在,让我们看看创建一个组件来获取并呈现上述查询的过程:

// App.js
import React from 'react';
import {graphql, QueryRenderer} from 'react-relay';

const environment = /* defined or imported above... */;

export default class App extends React.Component {
  render() {
    return (
      <QueryRenderer
        environment={environment}
        query={graphql`
          query UserQuery {
            viewer {
              id
            }  
          }
        `}
        variables={{}}
        render={({error, props}) => {
          if (error) {
            return <div>Error!</div>;
          }
          if (!props) {
            return <div>Loading...</div>;
          }
          return <div>User ID: {props.viewer.id}</div>;
        }}
      />
    );
  }
}

我们的应用程序QueryRenderer像其他任何React组件一样,在上面的代码中呈现,但是让我们看看传递给它的道具中发生了什么:

  • 我们正在通过environment我们之前定义的。
  • 我们正在使用该graphql函数来定义GraphQL查询。graphql是一个模板标签,该模板标签永远不会在运行时执行,而是由Relay编译器用来生成Relay需要进行操作的运行时工件。我们现在不必为此担心。有关更多详细信息,请查看Relay docs中的GraphQL。
  • 我们正在传递一组空的variables。在下一节中,我们将研究如何使用变量。
  • 我们正在传递一个render函数;从代码中可以看出,Relay为我们提供了有关是否发生错误或是否仍在获取查询的一些信息。如果一切成功,那么我们请求的数据将在内部可用props,其形状与查询中指定的形状相同。

为了运行此应用程序,我们需要首先使用Relay Compiler编译查询。假设安装是通过Installation and Setup完成的,我们就可以运行了yarn relay。

有关更多详细信息QueryRenderer,请查看文档。

使用查询变量

让我们假设一下,在我们的应用程序中,我们希望能够查看不同用户的数据,因此我们将以某种方式需要按ID查询用户。从我们的模式中,我们知道我们可以查询给定id的节点,因此让我们编写一个参数化查询以按id获取用户:

query UserQuery($userID: ID!) {
  node(id: $userID) {
    id
  }
}

现在,让我们看看如何使用来获取上述查询QueryRenderer

// UserTodoList.js
// @flow
import React from 'react';
import {graphql, QueryRenderer} from 'react-relay';

const environment = /* defined or imported above... */;

type Props = {
  userID: string,
};

export default class UserTodoList extends React.Component<Props> {
  render() {
    const {userID} = this.props;

    return (
      <QueryRenderer
        environment={environment}
        query={graphql`
          query UserQuery($userID: ID!) {
            node(id: $userID) {
              id
            }  
          }
        `}
        variables={{userID}}
        render={({error, props}) => {
          if (error) {
            return <div>Error!</div>;
          }
          if (!props) {
            return <div>Loading...</div>;
          }
          return <div>User ID: {props.node.id}</div>;
        }}
      />
    );
  }
}

上面的代码所做的事情与我们之前的示例非常相似。但是,我们现在通过prop 将$userID变量传递给GraphQL查询variables。这有两个重要的含义:

  • 鉴于这userID也是我们组件所采用的一种支持,它随时可能会userID从其父组件接收到新的支持。发生这种情况时,new variables将会传递给我们QueryRenderer,它将自动导致它使用具有的新值重新获取查询$userID。
  • $userID现在,该变量将在该查询内的任何位置可用。使用片段时,记住这一点将变得很重要。

既然我们已经更新了查询,请不要忘记运行yarn relay。

使用片段

现在我们知道了如何定义和获取查询,让我们开始构建待办事项列表。

首先,让我们从底部开始。假设我们要渲染一个仅显示给定待办事项的文本和完成状态的组件:

// Todo.js
import React from 'react';

type Props = {
  todo: {
    complete: boolean,
    text: string,
  },
};

export default class Todo extends React.Component<Props> {
  render() {
    const {complete, text} = this.props.todo;

    return (
      <li>
        <div>
          <input
            checked={complete}
            type="checkbox"
          />
          <label>
            {text}
          </label>
        </div>
      </li>
    );
  }
}

从我们的架构中,我们知道可以查询该Todo类型的数据。但是,我们不想为每个待办事项发送单独的查询;这将无法在传统的REST API上使用GraphQL的目的。我们可以直接在QueryRenderer查询中手动查询这些字段,但这会损害可重用性:如果我们要查询同一组字段作为不同查询的一部分,该怎么办?另外,我们不知道哪个组件需要查询的数据,这是Relay直接尝试解决的问题。

相反,我们可以定义一个可重用的Fragment,它允许我们在类型上定义一组字段,并在需要以下位置的查询中重用它们:

fragment TodoItemFragment on Todo {
  complete
  text
}

然后,我们的组件可以使用此片段来声明其对TodoGraphQL类型的数据依赖关系:

// Todo.js

// OPTIONAL: Flow type generated after running `yarn relay`, defining an Object type with shape of the fragment:
import type {Todo_todo} from './__generated__/Todo_todo.graphql';

import React from 'react';
import {graphql, createFragmentContainer} from 'react-relay'

type Props = {
  todo: Todo_todo
}

class Todo extends React.Component<Props> {
  render() {
    const {complete, text} = this.props.todo;

    return (
      <li>
        <div>
          <input
            checked={complete}
            type="checkbox"
          />
          <label>
            {text}
          </label>
        </div>
      </li>
    );
  }
}

export default createFragmentContainer(
  Todo,
  // Each key specified in this object will correspond to a prop available to the component
  {
    todo: graphql`
      # As a convention, we name the fragment as '<ComponentFileName>_<propName>'
      fragment Todo_todo on Todo {
        complete
        text
      }
    `
  },
)

上面的代码强调了Relay最重要的原理之一,即组件与数据依赖项的共置。这有几个方面的好处:

  • 一目了然,呈现给定组件所需的数据一目了然,而不必在我们的应用程序中搜索哪个查询正在获取所需的数据。
  • 因此,该组件与呈现它的查询是分离的。我们可以更改组件的数据依赖性,而不必更新呈现它们的查询或担心破坏其他组件。

请查看我们的“中继中思考”指南,以了解有关中继原理的更多详细信息。

在继续之前,请不要忘记使用来运行Relay Compiler yarn relay。

组成片段

鉴于片段容器只是React组件,我们可以这样构成它们。我们甚至可以在其他碎片容器中重用碎片容器。作为示例,让我们看一下如何定义一个TodoList仅呈现待办事项列表的组件,以及是否所有组件都已完成:

// TodoList.js

// OPTIONAL: Flow type generated after running `yarn relay`, defining an Object type with shape of the fragment:
import type {TodoList_userTodoData} from './__generated__/TodoList_userTodoData.graphql';

import React from 'react';
import {graphql, createFragmentContainer} from 'react-relay';

type Props = {
  userTodoData: TodoList_userTodoData,
}

class TodoList extends React.Component<Props> {
  render() {
    const {userTodoData: {totalCount, completedCount, todos}} = this.props;

    return (
      <section>
        <input
          checked={totalCount === completedCount}
          type="checkbox"
        />
        <ul>
          {todos.edges.map(edge =>
            <Todo
              key={edge.node.id}
              {/*We pass the data required by Todo here*/}
              todo={edge.node}
            />
          )}
        </ul>
      </section>
    );
  }
}

export default createFragmentContainer(
  TodoList,
  {
    userTodoData: graphql`
      # As a convention, we name the fragment as '<ComponentFileName>_<PropName>'
      fragment TodoList_userTodoData on User {
        todos(
          first: 2147483647  # max GraphQLInt, to fetch all todos
        ) {
          edges {
            node {
              id,
              # We use the fragment defined by the child Todo component here
              ...Todo_todo,
            },
          },
        },
        id,
        totalCount,
        completedCount,
      }
    `,
  },
);

与我们定义的第一个片段容器一样,TodoList通过片段声明其数据依赖性。但是,此组件还重新使用了该Todo组件先前定义的片段,并在呈现子Todo组件(也称为片段容器)时传递了适当的数据。

组成片段容器时要注意的最后一件事是,父容器将无权访问子容器定义的数据。中继仅允许组件访问在GraphQL片段中明确要求的数据-仅此而已。这称为“ 数据屏蔽”,其目的是防止组件依赖于未声明为依赖项的数据。

渲染片段

现在,我们有了一些声明其数据依赖项的组件(也称为片段容器),我们需要将它们连接到a,QueryRenderer以便实际获取和呈现数据。请记住,片段容器不会直接获取数据。相反,容器声明了渲染所需数据的规范,Relay保证在渲染之前此数据可用。

一个QueryRenderer渲染这些片段容器可能看起来像下面这样:

// ViewerTodoList.js
import React from 'react';
import {graphql, QueryRenderer} from 'react-relay';
import TodoList from './TodoList'

const environment = /* defined or imported above... */;

export default class ViewerTodoList extends React.Component {
  render() {
    return (
      <QueryRenderer
        environment={environment}
        query={graphql`
          query ViewerQuery {
            viewer {
              id
              # Re-use the fragment here
              ...TodoList_userTodoData  
            }
          }
        `}
        variables={{}}
        render={({error, props}) => {
          if (error) {
            return <div>Error!</div>;
          }
          if (!props) {
            return <div>Loading...</div>;
          }
          return (
            <div>
              <div>Todo list for User {props.viewer.id}:</div>
              <TodoList userTodoData={props.viewer} />
            </div>
          );
        }}
      />
    );
  }
}

请查看我们的“ 片段容器”文档以获取更多详细信息,以及有关“ Refetch和分页”的指南以更高级地使用容器。

变异数据

现在我们知道了如何查询和呈现数据,让我们继续更改数据。我们知道,要更改服务器中的任何数据,我们需要使用GraphQL Mutations。

从我们的模式中,我们知道我们可以使用一些变体,因此让我们从编写一个变体开始以更改complete给定待办事项的状态(即,将其标记或取消标记为完成):

mutation ChangeTodoStatusMutation($input: ChangeTodoStatusInput!) {
  changeTodoStatus(input: $input) {
    todo {
      id
      complete
    }
  }
}

这种突变使我们能够查询到一些数据,作为突变的结果,因此我们将要查询complete待办事项的更新状态。

为了在Relay中执行此变异,我们将使用Relay的commitMutationapi 编写一个新的变异:

// ChangeTodoStatusMutation.js
import {graphql, commitMutation} from 'react-relay';

// We start by defining our mutation from above using `graphql`
const mutation = graphql`
  mutation ChangeTodoStatusMutation($input: ChangeTodoStatusInput!) {
    changeTodoStatus(input: $input) {
      todo {
        id
        complete
      }
    }
  }
`;

function commit(
  environment,
  complete,
  todo,
) {
  // Now we just call commitMutation with the appropriate parameters
  return commitMutation(
    environment,
    {
      mutation,
      variables: {
        input: {complete, id: todo.id},
      },
    }
  );
}

export default {commit};

每当我们调用时ChangeTodoStatusMutation.commit(…),Relay都会将变异发送到服务器,在我们这种情况下,在收到响应后,Relay会自动使用服务器的最新数据更新本地数据存储。这也意味着,在收到响应后,中继将确保重新呈现依赖于更新数据的任何组件(即容器)。

为了在组件中实际使用此突变,我们可以Todo通过以下方式更新组件:

// Todo.js

// ...

class Todo extends React.Component<Props> {
  // Add a new event handler that fires off the mutation
  _handleOnCheckboxChange = (e) => {
    const complete = e.target.checked;
    ChangeTodoStatusMutation.commit(
      this.props.relay.environment,
      complete,
      this.props.todo,
    );
  };

  render() {
    // ...
  }
}

// ...

乐观更新

在上面的示例中,complete在我们从服务器返回响应之前,不会更新和重新呈现组件中的状态,这不会带来出色的用户体验。

为了使体验更好,我们可以将突变配置为进行乐观更新。乐观更新意味着如果我们从服务器获得成功的响应,则立即以我们期望的状态更新本地数据,即假设突变请求成功后立即更新数据。如果请求没有成功,我们可以回滚更新。

在Relay中,我们可以传递几个选项commitMutation来启用乐观更新。让我们看看我们的样子ChangeTodoStatusMutation:

// ChangeTodoStatusMutation.js

// ...

function getOptimisticResponse(complete, todo) {
  return {
    changeTodoStatus: {
      todo: {
        complete: complete,
        id: todo.id,
      },
    },
  };
}

function commit(
  environment,
  complete,
  todo
) {
  // Now we just call commitMutation with the appropriate parameters
  return commitMutation(
    environment,
    {
      mutation,
      variables: {
        input: {complete, id: todo.id},
      },
      optimisticResponse: getOptimisticResponse(complete, todo),
    }
  );
}

export default {commit};

在上面最简单的情况下,我们只需要传递一个optimisticResponse选项,该选项应该指向具有与变异响应有效载荷相同形状的对象。当我们通过此选项时,Relay将知道立即使用乐观响应更新我们的本地数据,然后使用实际服务器响应进行更新,或者在发生错误时将其回滚。

请注意,实际的查询和响应有效负载的形状可能与代码中选择的形状不完全相同,因为有时Relay会在编译步骤中为您添加额外的字段,因此您需要将这些字段添加到乐观响应中。例如:

  • id如果用于缓存目的的类型上存在中继,中继将添加一个字段。
  • __typename如果类型是联合或接口,则中继将添加一个字段。

您可以检查网络请求或响应以查看确切的形状。

通过突变响应更新本地数据

默认情况下,Relay将知道更新由突变有效负载引用的记录上的字段(todo例如,在我们的示例中)。但是,这只是最简单的情况。在某些情况下,更新本地数据并不像仅更新记录中的字段那样简单。

例如,我们可能正在更新项目的集合,或者我们可能会完全删除一条记录。对于这些更高级的方案,Relay允许我们传递一组选项以控制我们如何从服务器响应中更新本地数据,包括一组configs和一个updater用于完全控制更新的功能。

有关突变和更新的更多详细信息和高级用例,请查看我们的突变文档。

AXIHE / 精选资源

浏览全部教程

面试题

学习网站

前端培训
自己甄别

前端书籍

关于朱安邦

我叫 朱安邦,阿西河的站长,在杭州。

以前是一名平面设计师,后来开始接接触前端开发,主要研究前端技术中的JS方向。

业余时间我喜欢分享和交流自己的技术,欢迎大家关注我的 Bilibili

关注我: Github / 知乎

于2021年离开前端领域,目前重心放在研究区块链上面了

我叫朱安邦,阿西河的站长

目前在杭州从事区块链周边的开发工作,机械专业,以前从事平面设计工作。

2014年底脱产在老家自学6个月的前端技术,自学期间几乎从未出过家门,最终找到了满意的前端工作。更多>

于2021年离开前端领域,目前从事区块链方面工作了