Thinking In Relay

Relay 的数据获取方法极大地受到我们使用 React 的经验启发。尤其是,React 把复杂的界面拆成可重复使用的 组件允许开发者能独立的去思考一个应用程序的个别部分,并减少应用程序不同部分之间的耦合。更重要的是,这些 组件 的声明: 它们允许开发者指定 哪些 用户界面应该看起来像一个给定的状态,而不需要 如何 去担心如何去渲染那个 UI。不同于以往那些使用命令式的指令去操作原生的视图(例如,DOM) 的方法,React 使用对 UI 的描述来自动地判断需要的指令。

让我们看一些产品的使用案例来了解我们如何把这些想法加进 Relay。我们会假设你对 React 基本熟悉。

获取视图数据 #

根据我们的经验,绝大多数的产品都想要一个特定的行为:在为视图层获取全部的数据时显示一个加载指示符,然后在数据准备就绪后渲染整个视图。

一个解决方案是让根组件为其所有子项提取数据。 然而,这将引入耦合:对组件的每个更改都需要更改可能渲染它的 任何 根组件 ,并且通常是其与根之间的一些组件。 这种耦合可能意味着更大的错误机会,并减慢开发速度。最终,这种方法没有利用React的组件模型。T指定数据依赖关系最自然的地方就在 组件 中。

下一个逻辑方法是 render() a用作启动数据获取的手段。我们可以简单地渲染应用程序一次,看看它需要什么数据,获取数据,再次渲染。这听起来很棒,但问题是 组件使用数据来弄清楚要呈现的内容! 换句话说,这将强制数据获取进行分级:首先渲染根目录并查看需要的数据,然后渲染其子代,并将其所需的内容一直沿着树形结构。如果每个阶段引起网络请求, 如果需要连续的请求,渲染会很慢。我们需要一个方法来预先或 静态地 决定所有的数据需求。

我们最终定下静态方法; 组件将有效地返回与查看树分离的查询树,描述其数据依赖关系。 Relay 然后可以使用此查询树来获取单个阶段所需的所有信息,并使用它来呈现组件。问题是找到一种适当的机制来描述查询树,以及一种从服务器(即在单个网络请求中)有效地获取它的方法。 这是GraphQL的完美用例,因为它提供了一种用于 将数据依赖性描述为数据的语法,而不需要任何特定的API。请注意,Promises和Observables通常被推荐为替代方案,但它们表示 不透明的命令 ,并排除各种优化,如查询批处理。

数据组件又名容器 #

Relay 允许开发人员通过创建 容器通过数据依赖来注释它们的React组件。这些是包装原件的常规React组件。一个关键的设计约束是React组件是可重复使用的,因此 Relay 也必须也是。例如, <Story> 件可能会实现用于渲染任何 Story 项目的视图。 要渲染的实际故事将由传递给组件的数据确定: <Story story={ ... } />。在 GraphQL 中的等价概念是fragment: 指定要抓取什么数据 给一个给定 type 的对象的命名 query 片段。我们可以描述 <Story>的数据如下:

fragment on Story {
  text,
  author {
    name,
    photo
  }
}

然后可以使用此片段来定义Story容器:

// Plain React component.
// Usage: `<Story story={ ... } />`
class Story extends React.Component { ... }

// "Higher-order" component that wraps `<Story>`
var StoryContainer = Relay.createContainer(Story, {
  fragments: {
    // Define a fragment with a name matching the `story` prop expected above
    story: () => Relay.QL`
      fragment on Story {
        text,
        author { ... }
      }
    `
  }
})

渲染 #

在React中,渲染视图需要两个输入:要渲染的 组件 , 和要渲染的 DOM (UI) 节点。 渲染 Relay 容器是类似的: 我们需要一个 容器 来 渲染,我们需要一个容器来呈现,并且在图形中创建一个 根目录 来开始我们的查询。我们还必须确保对容器的查询执行,并且可能希望在获取数据时显示加载指示符。 ReactDOM.render(component, domNode) 类似于Relay provides <Relay.Renderer Container={...} queryConfig={...}> 这个目的。 容器是要渲染的项目,queryConfig提供了指定要获取 哪个 项目的查询。以下是我们如何渲染<StoryContainer>:

ReactDOM.render(
  <Relay.Renderer
    Container={StoryContainer}
    queryConfig={{
      queries: {
        story: () => Relay.QL`
          query {
            node(id: "123") /* our `Story` fragment will be added here */
          }
        `
      },
    }}
  />,
  rootEl
)

Relay.Renderer 然后可以编排查询的获取; 将它们与缓存数据分开,获取任何丢失的信息,更新缓存,并在数据可用后最后渲染StoryContainer 默认是在数据获取时不显示任何内容,但加载视图可以通过 render 支持进行自定义。正如React允许开发人员渲染视图而不直接操纵底层视图, Relay 和 Relay.Renderer 删除直接与网络通信的需要。

数据屏蔽 #

通过数据获取的典型方法,我们发现两个组件通常具有 隐式依赖 关系。例如, <StoryHeader>可能会使用某些数据而不直接确保数据已被获取。 该数据通常由系统的其他部分获取,例如 <Story>。那么当我们改变<Story>和删除数据获取逻辑时, <StoryHeader> 会突然莫名其妙的坏掉。这些类型的错误并不总是很明显,特别是在大型团队开发的大型应用中。手动和自动化测试的帮助有限: 这正是系统性问题的一种更好的解决方案。.

我们已经看到, Relay 容器确保 渲染组件 之前 获取GraphQL fragment。 但容器也提供了另外一个好处,并不能立即显现: 数据屏蔽. Relay 只允许组件访问他们专门要求的数据 fragments — 没有更多。所以,如果一个组件查询,以故事的 text, 另一个是它的 author, 每个人都可以看到 只有 他们要求的领域。 事实上,组件甚至看不到它们 children 请求的数据:因为那也会破坏封装。

Relay 会更进一步: 它使用不透明标识符 props 来验证我们在呈渲染组件之前已经显式地提取了组件的数据。如果 <Story> 渲染 <StoryHeader> 但忘记包含其片段, Relay 将警告数据<StoryHeader> is 丢失。事实上,即使 某些其他组件发生了相同的数据,Relay也会发出警告 <StoryHeader>. 。这个警告告诉我们,虽然事情现在 可能 会起作用, 但它们之后很有可能会坏掉。

总结 #

声明式数据提取提供框架。 通过分离要获取 哪些 数据和 如何 获取它, Relay 助开发者们建置强大、清楚、且高性能的应用程序。 这对 React 倡导的以组件 - 为中心的思维是一个很好的补充。当这每一个技术,React、Relay、以及 GraphQL — 都各自很强大,它们的组合是一个让我们能 快速前进释出高质量且有规模 的应用程序的UI 平台