【摘要】本系列文章较全面地分析了时下流行的React库、GraphQL服务器以及Relay架构模式各自的功能特征,并通过一个具体的实例向你展示它们是如何相互配合来完成一款Web应用的开发的。本篇属于本系列的上篇,侧重于React库、GraphQL服务器和Relay架构各自功能及其关系解析。
概述
不同于如AngularJS和Ember这样的框架,React是一个客户端库,其中提供了一组有限的函数,而且该库几乎独立于构建应用程序其他功能时所可能需要的其他库。从根本上说,React提供了客户端UI组件功能,其中提供了一种机制来创建组件、管理组件中的数据,渲染组件,以及复合小组件来构建更大的组件。React可以用于多种情景下,无论数据来自何处、 如何检索数据或如何把数据作为一个更大的应用程序的一部分进行管理的。为了有效地对付这些问题,需要利用其他的库和模式。一个用于React应用程序的常见模式是Flux。
人们开发Flux的目的是把它作为MVC(模型-视图-控制器)模式的一种替代方法,来管理响应动作的数据流。与MVC的双向数据流动不同,Flux依赖于系统中的各个部件之间的单向数据流。Flux是Facebook创建的,因为他们的开发人员发现很难了解采用MVC模式开发的大型应用程序中的数据运动规律。
不同于MVC模式所采用的多环路数据流,Flux使用的是单环路数据流方案。数据仅沿着一条线路流动,这条线路是:动作(Action)->调度器(Dispatcher)->存储(Store)(或多个存储)->组件(即视图)->动作。
其中,动作(Action)表示某种进入系统的事件。这可能是用户生成的,如请求刷新数据的按钮单击事件,或者也可能是通过一个Web套接字接收消息事件。然后,这个动作被传递给调度器将它发送到所有的存储。调度器其实只不过是一个转发机制。它不懂动作是什么,动作所传递的数据的含义,而也不懂得与该动作相关的每个存储的功能。它只是把动作调度给所有的存储,然后每个存储决定是否应该处理这一动作。存储负责维护数据的本地副本,强制实施业务规则并通知新数据的组件,以便能刷新它们。你可以认为存储是维护应用程序状态的,而整个Flux流程本质上就是一个状态机。因此,React组件利用了状态机模式。在某种意义上,Flux利用与此相同的状态机模式来架构整个应用程序。
虽然Flux是解决数据流问题的一种模式,但它本身并没有提供实现这种模式的技术。若要使用Flux模式,开发人员不得不创建系统中所有的组件,除了Facebook提供的调度器之外。创建一个Flux系统是相对比较容易的,但它需要大量的样板代码。在这方面,它面临与Backbone.js同样的问题。启动和运行系统是很容易的,但最终需要大量的编码。
Flux进化过程
当开发人员使用Flux时,他们开始想办法来把样板代码重构到可重用的库中。此外,他们还能够确定出Flux中的子模式,这使得对于应用程序中数据流的分析判断更为容易,而且还在而不牺牲Flux一般优点的情况下减少应用程序的复杂性。这些子模式包括:把应用程序中的多存储减少到一个存储;把调度器与存储结合到相同的组件(当只有一个存储时这是很重要的)中;而且,还能够把诸多组件封装到一个容器中,此容器中可以在黑箱中创建动作、调度和进行存储管理。如今,许多Flux的衍生产品不再仅仅局限于纯粹的Flux功能,而且还能够在保留其根本要素的同时避免Flux通常的缺点,即大量的样板代码的问题。
目前,虽然有很多Flux的衍生产品,但是Redux是更受欢迎的模式之一。Redux纯粹基于状态机概念和不可变数据的思想而构建。在该模式中,动作都由单一的“调度器-存储”来处理;这些“调度器-存储”对象使用reducer函数(它们本身是可以组合的)实现把一种状态转换到另一种状态。这就在极大地简化Flux模式的同时引进很多函数编程特征;一旦你彻底掌握这一技术,React应用程序编码将容易许多。
Relay是来自于Facebook团队的另一个Flux的衍生产品,此模式日渐普及。关于Facebook如何使用Relay以及其与Flux关系的处理方案的更多信息,请参考网址https://facebook.github.io/react/blog/2015/02/20/introducing-relay-and-graphql.html。
Relay框架功能分析
虽然Redux简化了应用程序的管理,但是在实际数据定位方面却不可知。它可以使用任何数据存储系统,但是再一次导致更多的样板代码(虽然少于Flux)问题。于是,出现了Relay (这是Facebook创建的另一个产品,其应用了其他大量的JavaScript开源产品,例如React,Relay,Immutable.js,GraphQL,Jest,Flow等),它的宗旨在于通过重构去除数据访问相关的样板代码部分,同时还引进另一种新的数据服务——GraphQL。GraphQL不同于传统REST服务的地方在于,它把数据视为一个图形,并力求以分层方式来描述该图形,从而使数据消费者自己指定他们需要的数据,而不是传统的REST服务中那样提供一组固定的数据集服务而不考虑消费者需求。
那么,Relay到底是做什么的呢?Relay是一个框架,它负责把React组件连接到GraphQL服务器,此连接是通过一个实现了动作、调度器和存储的容器实现的。开发人员不需要对动作、调度器和存储进行编程,而可以触发这些动作并通过Relay API来访问相应的结果。若要配置容器,开发人员必须提供GraphQL查询和突变片段(mutation fragments)向容器描述数据的图结构;此外,Relay还会负责照顾数据管理的所有细节。
Relay的确是一个框架(如Angular),而不是一个库。它的实现可以说是透明的——它需要通过React实现UI组件而且由GraphQL提供数据服务。一旦GraphQL服务器和React组件配置到位,Relay将接管并执行所有需要的操作。因此,使用Relay的关键是掌握配置过程。
此外,与框架Angular(它仅对客户端有专门需求)不同,Relay还要求GraphQL服务器接口来为Relay容器提供数据查询和变异操作。但是,只要通过特定的GraphQL接口提供数据,Relay并不在意如何存储数据。
因此,Relay需要后端和前端两个开发团队都要了解它的工作原理,并要求他们弄清程序的的每个部件(无论前端还是后端)是如何编码和配置的。
Relay与React的配合
本小节中,让我们从React的角度来研究一下Relay。程序员们可以使用多种语言对GraphQL服务器进行编码与配置,并部署到多种平台上。对于Node.js环境下的GraphQL实现来说,有一个称为graphql-relay(https://www.npmjs.com/package/graphql-relay)的包可以用于简化GraphQL服务器的编码和配置要求。在React方面,则存在一个名为relay-react(https://www.npmjs.com/package/react-relay)的包可用于配置Relay容器和路由,还能够激发动作来实现数据变异操作等。
Relay开发入门
入门Relay是有些难度的。因为该技术是如此之新而且有很多的竞争对手,所以,有关如何使用Relay的参考资源目前还相当有限。而提供资源的地方,一般提供的例子也是很有限的;为此,开发人员最终被迫去阅读博客文章、翻阅GitHub问题和正式的产品说明书才能创建一个简单的CRUD应用程序。此外,还需要搭建一个相当复杂的开发环境以及需要有一个正确配置的GraphQL服务器。因此,这样的任务对于JavaScript/前端开发新手而言可能相当艰巨。
作为准备工作,请首先克隆一下GitHub网站上的存储仓库(https://github.com/DevelopIntelligenceBoulder/react-flux-blog)到您的计算机上,并打开相应的文件夹blog-post-5+6。此文件夹包含一个完整的GraphQL/React/Relay应用程序。为了便该应用程序启动并运行起来,请打开一个终端,导航到文件夹blog-post-5+6中,并运行以下Gulp命令。
- $ npm i
- $ npm i -g gulp eslint eslint-config-airbnb eslint-plugin-react@^4.3.0 webpack babel-cli babel-eslint eslint-plugin-jsx-a11y@^0.6.2
- $ gulp
- $ npm run update-schema
- $ gulp
- $ gulp server
现在,请打开微软的Edge浏览器(【译者注】微软专门为Windows 10配置的高性能浏览器),然后导航到下面的URL:http://localhost:3000。
你会注意到,页面中将显示一个小控件列表,并使用Bootstrap 4风格进行修饰,看起来相当漂亮。(【译者注】因某种原因原文并没有提供结果快照)
该项目的基本开发构架是典型的文件夹组织方式。其中,src文件夹中存放可编辑的源代码文件,而最终的部署文件夹是dist(源代码文件都要复制至此处),应用程序正是使用此处的文件执行的。复制过程是使用Gulp命令进行的;具体地说,是通过组合一些简单的复制文件命令、创建一个处理SASS文件的任务,还有一个针对JavaScript的Web打***程完成的。其中,Web打包处理机制使用Babel转译器把RelayQL、JSX和ES2015代码转换为 ES5.1兼容的可以在任何浏览器中执行的JavaScript代码。ES2015和JSX转译已经不是什么新技术,但对于RelayQL的转译却是一个新课题。
RelayQL与Babel-Relay插件
GraphQL服务器能够通过使用内省(introspection)机制自动生成结构(【译者注】原文中用词是schema,这个词在数据库表格设计中常用;而其他许多软件技术中也广泛使用这个词,而且经常译为“模式”或“架构”。为了区别本文中另两个词pattern和architecture,在此特意译为“结构”)。结构(schema)其实是一个JSON文件,其中描述的所有类型由特定的GraphQL服务器使用。结构中可以包含自定义和内置类型。Babel-Relay插件使用此结构来校验使用RelayQL编码形成的GraphQL片段(fragments)。这些片段是使用ES2015字符串模板编码的,一旦它们通过根据结构定义的校验就被转换为JavaScript代码。这种校验可以有效地防止 GraphQL错误的发生。
配置Babel-Relay插件也是生成结构(schema)最简单的方法是,直接使用从Relay网站(https://facebook.github.io/relay/docs/guides-babel-plugin.html)或Relay初学者工具包项目(https://github.com/relayjs/relay-starter-kit)中下载的例子。这些文件正是本文对应的Github存储库中所使用的,并遵从Relay官网上推荐的开发模式。
从Relay初学者工具包项目中,我们需要使用两个文件:build/babelRelayPlugin.js和scripts/updateSchema.js。其中,UpdateSchema.js文件将用于生成结构(schema),而babelRelayPlugin.js文件将使用结构文件来校验证GraphQL片段以及转换RelayQL代码。
GraphQL与Relay协作
通常情况下,要使用Relay需要修改标准的GraphQL服务器实现方案。我们可以使用一个名为graphql-relay(https://www.npmjs.com/package/graphql-relay)的包来帮助把基于Node.js的GraphQL服务器配置为Relay兼容型的。要配置成一个Relay特定类型的GraphQL服务器需要三个主要方面:对象标记(Object Identification)、类型连接(Type Connections)和突变(Mutations)。
通过使用一个全局唯一的ID值,对象标记允许Relay从GraphQL服务器查询实现了节点接口的任何类型。这个全局ID是base64编码的,其中包含类型名称和一个后面跟一个冒号的本地ID值。graphql-relay库提供了分别命名为toGlobalID和fromGlobalID的函数支持在全局 ID之间来回转换。另外,类型名称来自于在类型配置中指定的GraphQL自定义类型名称。通常情况下,本地ID值来自于数据存储机制,例如关系数据库中的标记(Identity)。请参考下面的代码:
- import { nodeInterface } from './../node-definitions';
- export const widgetType = new GraphQLObjectType({
- name: 'Widget',
- description: 'A widget object',
- fields: () => ({
- id: globalIdField('Widget'),
- // more fields
- }),
- interfaces: () => [nodeInterface]
- });
在上面代码中,文件node-definitions.js(及其相关文件type-registry)的作用是:为通过节点接口使对象可用而提供配置与类型注册。
第二个Relay特定的配置,即类型连接(Type Connections),是建立父类型及其子类型之间的一对多关系的连接。这些连接是使用一种特殊的连接类型结构管理的。这些特殊的连接类型结构支持图中的边缘(graph edge)概念和游标(cursor)的概念,用于限制结果集与生成结果页面。可以配置连接和边缘类型以支持如元数据这样的附加属性,从而允许控制连接或边缘特性(例如加权的边缘等)。请参考下面的代码:
- import { widgetType } from './types/widget-type';
- import { connectionDefinitions } from 'graphql-relay';
- export const { connectionType: widgetConnection, edgeType: WidgetEdge } =
- connectionDefinitions({name: 'Widget', nodeType: widgetType});
上面代码中的ConnectionDefinitions函数用于创建Relay期望的结构中的连接类型。请参考下面的代码:
- import { widgetConnection } from '../connections/widget-connection';
- // inside of fields function of viewer type declaration
- widgets: {
- type: widgetConnection,
- description: 'A list of widgets',
- args: connectionArgs,
- resolve: (_, args) => connectionFromPromisedArray(getWidgets(), args)
- }
上面代码中,WidgetConnection类型是从widget-connection.js文件中导入的,用于配置查看器类型中的控件字段。包graphql-relay中还提供了一个名为connectionArgs的对象,该对象中包含通过Relay传递进来的用于处理连接的标准参数。这些参数包含的值用于游标操作。
第三和***一个Relay特定的配置是突变(mutation)配置。graphql-relay包中提供了一个专门的命名为mutationWithClientMutationId的帮助方法用于简化突变配置。有四个字段是必需的:突变名称、输入字段、输出字段和获取有效载荷的字段。在GraphQL中,所有的突变都将伴随一个查询来获知任何数据可能已被更改。Relay通过智能地决定突变后需要刷新哪些数据来进一步扩展了这种能力。
突变的名字是当React-Relay应用程序访问GraphQL服务器时用来调用突变的名字。输入字段对应于GraphQL中突变的args参数。输出字段描述了要从突变返回的类型的字段。***一个获取有效载荷的字段将执行实际的数据库操作并返回一个promise对象,该对象将推迟从GraphQL到应用程序的响应时间,直到该promise被解析结束为止。
小结
React和GraphQL联手,并辅助以Relay,将为构建Web应用程序提供一个很有前途的框架。虽然开发过程中需要不少的安装及配置工作,但是一旦这些工作完毕,开发过程将流畅地进行下去,消除了样板代码,并智能地处理数据管理问题。实践将会证明Relay框架很可能会成为构建下一代Web应用程序的游戏规则改变者。在本系列下篇文章中,我们将探讨使用React并配合以Relay来控制GraphQL资源的问题。