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>
.
。这个警告告诉我们,虽然事情现在 可能 会起作用, 但它们之后很有可能会坏掉。