GraphQL & Relay 初探

距离Facebook发布新版Relay(Relay Modern)已经快一年时间了,但相关的中文资料与实践案例依然不是不少。究其缘由,可能和官方文档不够详细有关。本文经过对GraphQL与Relay的浅析,但愿能下降其上手难度,同时也便于判断,本身的业务是否适合使用Relay框架。
什么?直接上代码?猴~能够Github克隆Relay应用模版,里面整合了先后端和路由,基本能知足常见App的需求。html

GraphQL

GraphQL是一套独立的数据查询系统,关于它的介绍与使用,官方网站已有比较详细的介绍,同时,如今已有中文版能够参考。对于基本概念,建议直接阅读官网,本文不作详细介绍。前端

设计思想

之因此名称中包含有Graph,是由于GraphQL采用了图结构的查询方式。以一个例子来看:
咱们想要设计一个公司的内部人员管理系统,假设一种最简单的场景,至少会包含部门和员工两大信息。以图的结构来表示他们的关系的话,可能会是这样:git

回顾经常使用的系统会发现,基本均可以经过图来描述角色关系。这里,咱们能够类比 图数据库的概念。因为图的结构更接近于天然世界,相比关系型数据库,在设计图数据库时,会省去一个图结构向关系型结构的转化工做。关于图数据库的更多介绍,能够参考 neo4j的介绍
回到上图,假设咱们如今要查询员工L某的详细信息,用GraphQL,能够这样来请求(为更加直观,这里以中文来表示):

{
  员工(ID: "022") {
    姓名
    职位
    所属部门 {
      名称
    }
    同事 {
      ID
      姓名
      职位
    }
  }
}
复制代码

正常状况下,服务端返回的结果是:github

{
  员工 {
    姓名: "L某",
    职位: "员工",
    所属部门:{
      名称: "前端部"
    }
    同事:[
      {
        ID: "022",
        姓名: "S某",
        职位: "经理"
      },
      {
        ID: "033",
        姓名: "Y某",
        职位: "总监"
      }
    ]
  }
}
复制代码

能够看到,根据查询请求,员工的信息以Json的格式所有返回出来了。对应到图中,实际上是提取了蓝色的这部分信息: 数据库

查询以员工(L某)为起点,沿着边,将所需的关联数据提取出来,造成了最终的返回的结果。由此能够看出,GraphQL所作的,实际上是将 图结构的数据提取出成为一个树状结构。为了更清晰的体现这一点,咱们将蓝色部分单独取出来,并稍稍换一下节点的位置:
这里,可能会有个疑问:这样查询出的树状结构,必然要求涉及的节点之间有边。 若是我想要同时查询两个节点的信息,但它们间没有边,那该怎么办呢?
仍以以前的例子来看,若是我想查询员工L某和设计部的信息,但它们之间没有边。这种时候,能够构建一个虚拟的节点,并以它为起点,链接起其余须要查询的节点。大概会是这样的一种结构:
查询结构:

{
  员工(ID: "022") {
    姓名
    职位
    ...
  }
  部门(名称: "设计部") {
    名称
  }
}
复制代码

返回结果:redux

{
  员工 {
    姓名: "L某",
    职位: "员工",
    ...
  }
  部门 {
    名称: "设计部"
  }
}
复制代码

在实际的应用开发中,也一般采用以上这样的设计结构。
到此,咱们来总结一下。GraphQL经过查询语句,将图结构的数据,提取成了树状结构做为结果返回。这里的图结构的数据,对应的即是GraphQL的类型系统。而如何将图中的节点,也就是定义的各个类型相互关联,须要经过具体的逻辑代码来实现。官方和社区也提供了各类语言的GraphQL库
那么还有个问题,若是我想修改数据该怎么办呢?
为解决这个问题,GraphQL引入了Mutation的概念。咱们能够把Mutation看做是一种特殊的查询,你须要为它定义名称、参数、返回数据,并在具体的代码逻辑中,完成它的具体数据操做。详细能够参考官方文档segmentfault

优点

官方网站简单介绍了GraphQL的一些优势,不过,你可能更想知道,相比其余的API设计模式,GraphQL有什么优点呢?
根据我实践下来的理解,GraphQL主要解决了面向前端的API在开发和后期维护中,常会遇到的一些矛盾点。下面,咱们以RESTful API为参照,来具体看一下。后端

灵活性

再来看上一节的例子,像获取员工所有信息这样的场景在应用开发中仍是比较常见的,若是换成RESTful的API,会是怎么样的呢?我想,通常会有两种作法:设计模式

  • 一、单独的API来拉取所有信息;
  • 二、将“同事”、“部门”这些信息做为独立的API,在前端经过多条API来组合。

咱们先看第一种作法。这种方式有几点明显的弊端:
首先是信息的冗余。若是我在其余地方只须要显示员工基础信息,不包含具体的部门或同事信息,那就存在了数据的冗余。
其次,从后端角度,会带来维护上的成本。因为前端的展现需求相对多变,极可能会形成许多再也不使用的API,而这些API又每每不敢轻易移除。又或者,在新需求中,极可能会新增与现有API重复度较高的API,形成后端业务代码的冗余。
再者,即便考虑经过参数的方式,能使得返回值有可选择性,确实能够增长必定的灵活度,但又不可避免的增长了参数的复杂性。
再看第二种作法。经过这种细粒度的模块划分方式,相对第一种来讲,减轻了后端代码的维护成本,但却对前端极不友好。好比,一个列表中的某个字段,后端只提供了单独查询的方式,当列表数据量特别大的时候,请求数也大大增长,将直接影响前端性能与用户体验。此外,大量的异步请求无疑增长了前端代码复杂度,从而提升了前端的维护成本。
而GraphQL只须要定义好类型及对应的数据处理方式,暴露给查询根节点,前端能够随意按需请求,很好的解决了以上的矛盾。数组

前端友好性

GraphQL用来做为BFF层(Backend For Frontend)有其先天的优点,最主要在于其面向前端的友好性设计。
除了上面提到的,在查询请求中,请求数的减小对前端体验上的提高以外,Mutation一样减小了请求次数。在Restful API中,除了GET请求,其它的请求完成后,前端一般还须要再发出一个GET请求,来拉取变动后的数据。若是在返回结果中,为前端的显示界面而增长了一部分数据,又会破坏后端代码的可复用性。而在GraphQL的Mutation中,返回的数据彻底能够根据前端界面的数据需求来决定,并且只需一次请求便可。结合Relay框架,还能够定义理想化更新来减小Loading界面出现的次数,进一步提升用户体验。(这一点会在下一节具体展开)

下降沟通成本

在当下流行的先后端分离开发模式下,先后端开发者的沟通成本也是影响项目进度的一大要素。一般,为了下降沟通成本,后端开发者须要提早定义API文档,前端会根据API文档来MOCK数据以开发前端界面。后端开发者还须要经过各类工具,如PostMan来进行测试。在先后端完成开发后,还须要作联调对接。
对于GraphQL来讲,schema自身就是很好的文档。同时,官方还提供了一个相似于PostMan的工具GraphiQL,能够有助于开发中的调试。

其余

GraphQL与Relay框架结合后,还能发挥出更大优点,好比先后端一致的类型校验、前端缓存等,具体请看下一节内容。

Relay

Relay是一套基于GraphQL和React的框架,它将这二者结合,在原来React组件的基础上,进一步将请求封装进组件。
官方提供了一个TodoMVC的demo能够参考,基本涵盖了CRUD操做。

QueryRenderer

Relay框架提供了QueryRenderer这样一个高阶组件(HOC)来封装React组件和GraphQL请求。这个组件接受四个Props:environment、query、variables以及render。 environment须要配置网络请求Store;query接受的即是GraphQL请求;variables接受GraphQL请求中须要的变量,最后render用来定义组件的渲染。
假设咱们要开发一个显示员工基本信息的Relay组件,那么它可能会是这样的:

<QueryRenderer 
  environment={environment}
  query={graphql`
    query StaffQuery($id: ID!) {
      员工(ID: $id) {
        ID
        姓名
        职位
      }
    }
  `}
  variables={{ id: '011' }}
  render={({error, props}) => {
    if (error) {
      return <div>{error.message}</div>;
    } else if (props) {
      return <div>工号:{props["员工"]["ID"]};姓名:{props["员工"]["姓名"]};职位:{props["员工"]["职位"]};</div>;
    }
      return <div>Loading...</div>;
    }
  }
/>
复制代码

Fragment

如今咱们已有了一个展现员工基本信息的组件,若是咱们如今要在这个组件的基础上,进一步封装出一个员工列表的组件,该怎么办呢?
参照React组件的方式,能够建立一个新的组件,接收一个包含员工ID数组的props,在这个新的组件内部,根据ID数组Map多个员工信息的Relay组件。
这样彷佛能够,但问题是,若是有10个ID,那这样一个组件也就会发出10个GraphQL请求,显然违背了GraphQL的设计理念。
固然也能够建立一个新的Relay组件:query中直接请求一组员工数据,渲染出列表。但这样就失去了组件的复用性,由于很显然,这个新组件中,显示每条员工信息的逻辑和样式,跟单个员工信息的组件是一致的。
这里,Relay提供了一个Fragment的HOC组件,它接受两个Props:component和fragmentSpec。
component接受React组件,用来处理具体的组件视图和逻辑;fragmentSpec则是接受一段GraphQL Fragment。所谓Fragment,对应到上一节的图中,就是节点的某一部分。好比:

fragment 员工信息 on 员工 {
  ID
  姓名
  职位
}
复制代码

在请求中,就能够这样引入Fragment:

{
  员工(ID: "022") {
    ...员工信息
  }
}
复制代码

那么回到Relay中,能够这样建立一个员工信息的Fragment组件:

createFragmentContainer(
  class 员工信息 extends React.Component {
    render() {
      return <div>工号:{this.props.data["员工"]["ID"]};姓名:{this.props.data["员工"]["姓名"]};职位:{this.props.data["员工"]["职位"]};</div>;
    }
  },
  graphql`
    fragment 员工信息 on 员工 {
      ID
      姓名
      职位
    }
  `,
)
复制代码

有了这样一个员工信息的Fragment组件后,咱们能够再建立员工信息列表的组件:

<QueryRenderer 
  environment={environment}
  query={graphql`
    query StaffListQuery($ids: [ID]!) {
      员工(IDs: $ids) {
        ...员工信息
      }
    }
  `}
  variables={{ id: ['011', '022', '033'] }}
  render={({error, props}) => {
    if (error) {
      return <div>{error.message}</div>;
    } else if (props) {
      return <员工信息 data={this.props.data} />;
    }
      return <div>Loading...</div>;
    }
  }
/>
复制代码

这样一来,组件实际的请求仍是只有一条,但员工信息的组件获得了成功复用,若是在其余组件中,须要显示员工信息,也一样只须要将该Fragment组件引入便可。
除了基本的Fragment Container,Relay还提供了Refetch ContainerPagination Container组件,前者在原Fragment组件的基础上,注入了refetch方法,以便知足组件须要更新数据的场景(如:用户主动点击数据列表的刷新按钮);然后者,则添加了若干分页的操做,这里就不具体展开了。

Relay Store

在QueryRenderer中配置的environment里面,主要包含的是网络请求和Store。这里的Store于Redux的Store不太一致。Redux中主要用来统一管理组件的State,而Relay Store则记录的是Record。这里的Record,其实就是GraphQL的每一个Type,或者对应于上一节的图中的每一个节点。
当Relay框架收到GraphQL返回的数据后,会为每个节点数据记录一个ID,并在Relay Store中存为一个Record。同时,Relay也为这些Record提供了CRUD的方法。具体能够参考官方文档

Mutations

为了便于在组件中发起GraphQL Mutation操做,Relay提供了commitMutation方法。除了发起Mutation以外,利用Relay Store,能够方便的定位页面数据并进行更新,还可以实现理想更新,进一步提高用户体验。

优点

到这里,基本已经涵盖了Relay的所有功能。从上文能够看出,在GraphQL原有的优点基础上,Relay还带来了如下两点优点:

  • 一、实现了数据查询与组件的结合,进一步提升了前端模块化程度,提升组件复用性。
  • 二、优秀的客户端缓存,提高用户体验。

除此以外,结合Flow框架的类型检测,Relay能够很好地根据后端提供的schema作类型校验,避免一些潜在的Bug。

总结

经过以上的介绍和分析,相信你对GraphQL与Relay已经有了大体的了解,我认为,Relay比较适合的场景,是那种前端数据展现类别众多,且变化较大的应用,好比社交网站。但具体是否在项目中应用,仍是须要结合需求实际来决定。

参考资料

GraphQL Concepts Visualized
GraphQL and Relay 浅析

相关文章
相关标签/搜索