随着多终端、多平台、多业务形态、多技术选型等各方面的发展,先后端的数据交互,日益复杂。前端
同一份数据,可能以多种不一样的形态和结构,在多种场景下被消费。node
在理想状况下,这些复杂性能够所有由后端承担。前端只管从后端接口里,拿到已然整合完善的数据。git
然而,无论是由于后端的领域模型,仍是由于微服务架构。做为前端,咱们感觉到的是,后端提供的接口,愈加不够前端友好。咱们必须自行组合多个后端接口,才能获取到完整的数据结构。github
面向领域模型的后端需求,跟面向页面呈现的前端需求,出现了不可调和的矛盾。数据库
这种背景下,本着谁受益谁开发的原则。咱们最后选择使用 Node.js 搭建专门服务于前端页面呈现的后端,亦即 Backend-For-Frontend,简称 BFF。express
咱们面临了不少不一样的技术选型,主要围绕在权衡 RESTful API 和 GraphQL。npm
面向前端页面的数据聚合层,其接口很容易在迭代过程当中,变得越发复杂;最终发展成一个超级接口。编程
它有不少调用方,各类不一样的调用场景,甚至多个不一样版本的接口并存,同时提供数据服务。json
全部这些复杂性,都会反映到接口参数上。后端
接口调用的场景越多,它对接口参数结构的表达能力,要求越高。若是只有一个 boolean 类型的参数,只能知足 true | false 两种场景罢了。
以产品详情接口为例,一种很天然的请求参数结构以下:
里面包含 ChannelCode 渠道信息,IsOp 身份信息,MarketingInfo 营销相关的信息,PlatformId 平台信息,QueryNode 查询的节点信息,以及 Version 版本信息。最核心的参数 ProductId,被大量场景相关的参数所围绕。
审视一下 QueryNode 参数,很容易能够发现,它正是 GraphQL 的雏形。只不过它用的是更复杂的 JSON 来描述查询字段,而 GraphQL 用更简洁的查询语句,完成一样的目的。
而且,QueryNode 参数,只支持一个层级的字段筛选;而 GraphQL 则支持多层级的筛选。
GraphQL 能够看做是 QueryNode 这种形式的参数设计的专业化。相比用 JSON 来描述查询结果,GraphQL 设计了一个更完整的 DSL,把字段、结构、参数等,都整合到一块儿。
仿照格林斯潘第十定律:
任何C或Fortran程序复杂到必定程度以后,都会包含一个临时开发的、不合规范的、充满程序错误的、运行速度很慢的、只有一半功能的Common Lisp实现。
https://zh.wikipedia.org/wiki/格林斯潘第十定律
或许能够说:
任何接口设计复杂到必定程度后,都会包含一个临时开发的、不合规范的、只有一半功能的 GraphQL 实现。
从 SearchParams, FormData 到 JSON,再到 GraphQL 查询语句,咱们看到不断有新的数据通信方式出现,知足不一样的场景和复杂度的要求。
站在这个层面上看,GraphQL 模式的出现,有必定的必然性。
做为一个查询相关的 DSL,GraphQL 的语言设计,也不是随意的。
咱们能够作一个思想实验。
假设你是一名架构师,你接到一项任务,设计一门前端友好的查询语言。要求:
查询语法跟查询结果相近
能精确查询想要的字段
能合并多个请求到一个查询语句
无接口版本管理问题
代码即文档
咱们知道查询结果是 JSON 数据格式。而 JSON 是一个 key-value pair 风格的数据表示,所以能够从结果倒推出查询语句。
上图是一个查询结果。很显然,它的查询语句不可能包含 value 部分。咱们删去 value 后,它变成下面这样。
查询语句跟查询结果拥有相同的 key 及其层次结构关系。这是咱们想要的。
咱们能够再进一步,将冗余的双引号,逗号等部分删掉。
咱们获得了一个精简的写法,它已是一段合法的 GraphQL 查询语句了。
其中的设计思路和过程是如此简单直接,很难想象还有别的方案比目前这个更知足要求。
固然,只有字段和层级,并不足够。符合这种结构的数据太多了,不可能把整个数据库都查询出来。咱们还须要设计参数传递的方式,以便能缩小数据范围。
上图是一个天然而然的作法。用括号表示函数调用,里面能够添加参数,可谓经典的设计。
它跟 ES2015 里的 (Method Definitions Shorthand) 也高度类似。以下所示:
前面演示的 GraphQL 参数写法,参数值用的是字面量 userId: 123。这不是一个特别安全的作法,开发者会在代码里,用拼接字符串的方式将字面量值注入到查询语句,也就给了恶意攻击者注入代码的机会。
咱们须要设计一个参数变量语法,明确参数位置和数量。
咱们能够选用 $xxx 这种常见的标记方法,它被不少语言采用来表示变量。沿用这种风格,能够大大减小开发者的学习成本。
先后端通信的另外一个痛点是,命名。前端常常吐槽后端的字段名过于冗长,或者不知所云,或者拼写错误,或者不符合前端表述习惯。最多见的状况是,后端字段名以大写字母开头,而前端习惯 Class 或者 Component 是大写字母开头,实例和数据,则以小写字母开头。
咱们指望有机会进行字段名调整。
别名映射(Alias)语法,正是为了这个目的而出现的。
上面这种别名映射的语法,在其它语言里也很常见。若是不这样写,顶多就是变成:
uid as Uid 或者 uid = Uid 这类作法,差异不大。我认为选用冒号更佳,它跟 ES2015 的解构语法很接近。
至此,咱们拥有了 key 层级结构,参数传递,变量写法,别名映射等语法,能够编写足够复杂的查询语句了。不过,还有几个小欠缺。
好比对字段的条件表达。假设有两次查询,它们惟一的差异就是,一个有 A 字段,另外一个没有 A 字段,其它字段及其结构都是相同的。为了这么小的差异 ,前端难道要编写两个查询语句?
这显然不现实,咱们须要设计一个语法描述和解决这个问题。
它就是——指令(Directive)。
指令,能够对字段作一些额外描述,好比
@include,是否包含该字段;
@skip,是否不包含该字段;
@deprecate,是否废弃该字段;
除了上述默认指令外,咱们还能够支持自定义指令等功能。
指令的语法设计,在其它语言里也能够找到借鉴目标。Java,Phthon 以及 ESNext 都用了 @ 符号表示注解、装饰器等特性。
有了指令,咱们能够把两个高度类似的查询语句,合并到一块儿,而后经过条件参数来切换。这是一个不错的作法。不过,指令是跟着单个字段走的,它不能解决多字段的问题。
好比,字段 A 和字段 B,拥有相同的整体结构,仅仅只有 1 个字段名的差别。前端并不想编写同样的 key 值重复屡次。
这意味着,咱们须要设计一个片断语法(Fragment)。
如上所示,用 fragment 声明一个片断,而后用三个点表示将片断在某个对象字段里展开。咱们能够只编写一次公共结构,而后轻易地在多个对象字段里复用。
这种设计也是一个经典作法,跟 JavaScript 里的 Spread Properties 很相近。
至此,咱们获得了一个相对完整的,对前端友好的查询语言设计。它几乎就是 GraphQL 当前的形态。
如你所见,GraphQL 的查询语言设计,借鉴了主流开发语言里的众多成熟设计。使得任何拥有丰富的编程经验的开发者,很容易上手 GraphQL。
按照一样的要求,从新来一遍,大几率获得跟当前形态高度接近的设计。这是我理解的 GraphQL 语言设计里包含的必然性。
查询语法,是 GraphQL 面向前端,或者说面向数据消费端的部分。
除此以外,GraphQL 还提供了面向后端,或者说面向数据提供方的部分。它就是基于 GraphQL 的 Type System 构建的 Schema。
一个 GraphQL 服务和查询的链路,大体以下:
首先,服务端编写数据类型,构建一个数据结构之间的关联网络。其中 Query 对象是数据消费的入口。全部查询,都是对 Query 对象下的字段的查询。能够把 Query 下的字段,理解为一个个 RESTful API。好比上图中的,Query.post 和 Query.author,至关于 /post 和 /author 接口。
GraphQL Schema 描述了数据的类型与结构,但它只是形状(Shape),它不包含真正的数据。咱们须要编写 Resolver 函数,在里面去获取真正的数据。
Resolver 的简单形式以下
每一个 Query 对象下的字段,都有一个取值函数,它能获取到前端传递过来的 query 查询语句里包含的参数,而后以任意方式获取数据。Resolver 函数能够是异步的。
有了 Resolver 和 Schema,咱们既定义了数据的形状,也定义了数据的获取方式。能够构建一个完整的 GraphQL 服务。
但它们只是类型定义和函数定义,若是没有调用函数,就不会产生真正的数据交互。
前端传递的 query 查询语句,正是触发 Resolver 调用的源头。
如上所示,咱们发起了查询,传递了参数。GraphQL 会解析咱们的查询语句,而后跟 Schema 进行数据形状的验证,确保咱们查询的结构是存在的,参数是足够的,类型是一致的。任何环节出现问题,都将返回错误信息。
数据形状验证经过后,GraphQL 将会根据 query 语句包含的字段结构,一一触发对应的 Resolver 函数,获取查询结果。也就是说,若是前端没有查询某个字段,就不会触发该字段对应的 Resolver 函数,也就不会产生对数据的获取行为。
此外,若是 Resolver 返回的数据,大于 Schema 里描绘的结构;那么多出来的部分将被忽略,不会传递给前端。这是一个合理的设计。咱们能够经过控制 Schema,来控制前端的数据访问权限,防止意外的将用户帐号和密码泄露出去。
正是如此,GraphQL 服务能实现按需获取数据,精确传递数据。
有至关多的开发者,对 GraphQL 有各类各样的误解。在这里挑选几个重要的例子,加以澄清,帮助你们更全面的认识 GraphQL。
有一些开发者认为 GraphQL 须要操做数据库,所以实现起来,几乎等于要推翻当先后端的全部架构。这是一个重大误解。
GraphQL 不只能够不操做数据库,它甚至能够不从其它地方获取数据,而直接写死数据在 Resolver 函数里。查看 graphql.js 的官方文档,咱们轻易能够找到案例:
上图定义了一个 schema,只有一个类型为 String 的 hello 字段,它的 resolver 函数里,无视全部参数,直接 return 一个 hello world 字符串。
能够看到,GraphQL 只是关于 schema 和 resolver 的一一对应和调用,它并未对数据的获取方式和来源等作任何假设。
在网络上,有至关多的 GraphQL 文章,将它跟 RESTful API 对立起来,仿佛要么全盘 GraphQL,要么全盘 RESTful API。这也是一个重大误解。
GraphQL 和 RESTful API 不只不对立,仍是互相协做的关系。
在前面关于 Resolver 函数的图片中,咱们看到,能够在 GraphQL Schema 的 Resolver 函数里,调用 RESTful API 去获取数据。
固然,也能够调用 RPC 或者 ORM 等方式,从别的数据接口或者数据库里获取数据。
所以,实现一个 GraphQL 服务,并不须要挑战当前整个后端体系。它具备高度灵活的适配能力,能够低侵入性的嵌入当前系统中。
尽管绝大多数 GraphQL,都以 server 的形式存在。可是,GraphQL 做为一门语言,它并无限制在后端场景。
上图仍是前面展现过的 graphql.js 的官方文档,最下面一行,就是一个普通的函数调用,它发起了一次 graphql 查询,其 response 结果以下:
这段代码,不仅能在 node.js 里运行,在浏览器里也能够运行(可访问:https://codesandbox.io/s/hidden-water-zfq2t 查看运行结果)
所以,咱们彻底能够将 GraphQL 用在纯前端,去实现 State Management 状态管理。Relay 等框架,即包含了用在前端的 graphql。
这是一个有趣的事实,GraphQL 语言设计里的两个组成部分:
数据提供方编写 GraphQL Schema;
数据消费方编写 GraphQL Query;
这种组合,是官方提供的最佳实践。但它并非一个实践意义上的最低配置。
GraphQL Type System 是一个静态的类型系统。咱们能够称之为静态类型 GraphQL。此外,社区还有一种动态类型的 GraphQL 实践。
graphql-anywhere: Run a GraphQL query anywhere, without a GraphQL server or a schema.
https://github.com/apollographql/apollo-client/tree/master/packages/graphql-anywhere
它跟静态类型的 GraphQL 差异在于,没有了基于 Schema 的数据形状验证阶段,而是直接无脑地根据 query 查询语句里的字段,去触发 Resolver 函数。
它也无论 Resolver 函数返回的数据类型对不对,获取到什么,就是什么。一个字段,没必要先定义好,才能被前端消费,它能够动态的计算出来。
在某些场景下,动态类型的 GraphQL 有必定的便利性。不过,它同时丧失了 GraphQL 的部分精髓,这块后面将会详细描述。
值得一提的是,无论是静态类型的 GraphQL 仍是动态类型的 GraphQL,都是既能够运行在服务端,也能够运行在前端。
这是另外一个有趣的事实。最初咱们演示了,如何基于 JSON 数据结果,反推出 GraphQL 查询语法的设计。而如今,咱们却说 GraphQL 能够不返回 JSON 数据格式。
没错。当一个新事物出现以后,随着它的不断发展,它能够脱离其初衷,衍生出不一样的形态。
上图仍是来自 graphql-anywhere 里的例子。
在这里,它实现了一个 gqlToReact 的 Resolver,能够把一个 graphql 查询转换为 ReactElement 结构。
不仅是动态类型的 GraphQL 有这个能力,静态类型的 GraphQL 也有可能实现同样的效果。
不过这种作法,目前仅仅停留在能力演示阶段。其妙用还有待社区去挖掘和探索。
到目前为止,咱们见识到了 GraphQL 的高自由度和灵活性。在搭建 GraphQL Server 时,也能够根据实际需求和场景,采用不一样的模式。
这个模式就是简单粗暴的把 RESTful API 服务,替换成 GraphQL 实现。以前有多少 RESTful 服务,重构后就有多少 GraphQL 服务。它是一个简单的一对一关系。
默认状况下,面向两个 GraphQL 服务发起的查询是两次请求,而不是一次。举个例子:
前端须要产品数据时,从以前调用产品相关的 RESTful API,变成查询产品相关的 GraphQL。不过,须要订单相关的数据时,可能要查询另外一个 GraphQL 服务。
有一些公司拿 GraphQL 小试牛刀时,采起了这个作法;将 GraphQL 用在特定服务里。
不过,这种模式难以发挥 GraphQL 合并请求和关联请求的能力。只是起到了按需查询,精确查询字段的做用,价值有限。
所以,他们在实践后,发现收效甚微;认为 GraphQL 不过如此,还不如 RESTful API 架构简单和成熟。
其实这是一种选型上的失误。
在这个模式里,GraphQL 接管了前端的一整块数据查询需求。
前端再也不直接调用具体的 RESTful 等接口,而是经过 GraphQL 去间接获取产品、订单、搜索等数据。
在 GraphQL 这个中间层里,咱们将各个微服务,按照它们的数据关联,整合成一个基于 GraphQL Schema 的数据关系网络。前端能够经过 GraphQL 查询语句,同时发起对多个微服务的数据的获取、筛选、裁剪等行为。
值得一提的是,做为 API Gateway 的 GraphQL 服务,能够在其 Resolver 内,向前面提到的 RESTful-like 的 GraphQL 发起查询请求。
如此,既避免了前端须要一对多的问题,也解决了 API Gateway GraphQL 须要请求 RESTful 全量数据接口的内部冗余问题。让服务到服务之间的数据调用,也能够作到更精确。
GraphQL 服务是一个对数据消费方友好的模式。而数据消费方,既能够是前端,也能够是其它服务。
当数据消费方是其它服务时,经过 GraphQL 查询语句,彼此之间能够更精确获取数据,避免冗余的数据传输和接口调用。
当数据消费方是前端时,因为前端须要跟多个数据提供方打交道,若是每一个数据提供方都是单独的 GraphQL,那并不能获得本质上的改善。此时如有一个 Gateway 角色的 GraphQL,能够真正减小前端调用的复杂度。
一样是 API Gateway 角色的 GraphQL 服务,在实现方式上也有不一样的分类。
包含大量真实的数据操做和处理的 GraphQL
转发数据请求,聚合数据结果的 GraphQL
第一类,是传统意义上的后端服务;第二类,则是咱们今天的重点,GraphQL as BFF。
这两类 GraphQL 服务的要求是不一样的,前者可能包含大量 CPU 密集的计算,然后者整体而言主要是 Network I/O 相关的行为。
许多公司并不提倡使用 Node.js 构建第一种服务,无论是构建 RESTful 仍是 GraphQL。咱们也同样。
所以,后面咱们讨论的 GraphQL,若是没有特别声明,均可以理解为上面所说的第二种类型。
在澄清关于 GraphQL 的迷思时,咱们指出,GraphQL 能够不做为 Server。
这意味着,一个包含 GraphQL 实现的 Server,不必定经过 GraphQL 查询语句进行先后端数据交互,它能够继续沿用 RESTful API 风格。
也就是说,咱们能够把 GraphQL 看成一个服务端开发框架,而后在 RESTful 的各个接口里,发起 graphql 查询。
无论是前端跟其它后端服务,都没必要知道 GraphQL 的存在。前端的调用方式,仍是 RESTful API,在 RESTful 服务内部,它本身向本身发起了 GraphQL 查询。
那么,这个模式有什么好处跟价值?
设想一下,你用 RESTful API 风格实现 BFF。因为 PC 端和移动端的场景不一样,它们对同一份数据的消费方式差别很大。
在 PC 端,它能够一次请求全量数据。
在移动端,由于它屏幕小,它要分屡次去请求数据。首屏一次,非首屏一次,滚动按需加载 N 次,多个 2 级页面里 M 次。
咱们要么实现一个超级接口,根据请求参数适配不一样场景(即实现一个半吊子的 GraphQL);要么实现多个功能类似,但又不一样的 RESTful 接口。
其中的差别太大了,因此不少公司索性就把 BFF 分红,PC-BFF 和 Mobile-BFF 两个 BFF 服务。
咱们能够把 PC-BFF 和 Mobile-BFF 整合成一个 GraphQL-BFF 服务。即使先后端不经过 GraphQL 查询语句进行交互,咱们也能够在各个接口里,编写相对简单的查询语句,代替更高成本的接口实现。
也便是说,使用 GraphQL 搭建 BFF,若是出现先后端分工、沟通等方面的矛盾。咱们能够将 GraphQL 服务降级为 RESTful 服务,无非就是把须要前端编写的查询语句,写死在后端接口里面罢了。
若是实现的是 RESTful 服务,要转换成 GraphQL 服务,就没有那么简单了。
有了这种优雅降级的能力,咱们能够更加放心大胆的推进 GraphQL-BFF 方案。
理解 GraphQL 的精髓所在,能够帮助咱们更正确地实践 GraphQL。
首先来想一下,GraphQL 为何要叫 GraphQL,其中的 Graph 体如今什么地方?
GraphQL 的查询语句,看起来是 JSON 写法的一种简化。而 JSON 是一个 Tree 树形数据结构。为何不叫 TreeQL,而是 GraphQL 呢?
一个重要的前置知识是,什么是 Tree,什么是 Graph,它们有什么关系?
下图是一个 Tree 的结构示意图。
Tree 有且只有一个 Root 节点,而且对于每一个非 Root 节点,有且只有一个父节点;它们组成了一个层次结构。其中任意两个节点,有且只有一条链接路径;没有循环,也没有递归引用。
下图是一个 Graph 的结构示意图。
而 Graph 里的节点之间,可能存在不仅一种链接路径,可能存在循环,可能存在递归引用,可能没有 Root 节点。它们组成了一个网络结构。
咱们能够把 Graph 这种网络结构,经过裁剪链接路径,把它压缩成任意节点只有惟一链接路径的简化形态。如此网络结构退化成层次结构,它变成了 Tree。
也就是说,Graph 是比 Tree 更复杂的数据结构,后者是它的简化形式。拥有 Graph,咱们能够按照不一样的裁剪方式,衍生出不一样的 Tree。而 Tree 里包含的信息,若是不增长其它额外数据,不足以构建足够复杂的 Graph 结构。
在 GraphQL 里,承担构建网络结构的,并不是 GraphQL 查询语句,而是基于 GraphQL Type System 构建的 Schema。
上图是一个 GraphQL Schema,定义了 A, B, C, D 和 E 五种数据类型,它们分别挂载到 入口类型 Query 里的 a, b, c, d 和 e 字段里。
A, B, C, D, E 里面,包含着递归的结构。A 里面包含 B 和 C,B 里面包含 C 和 D,D 里面包含 E,E 里面又包含 A,又回到了 A。
这是一个复杂的关系网络。要构建递归关联,并不须要这么复杂。直接 A 里包含 B,和 B 里包含 A 也行,此处是一个演示。
有了这个基于数据类型的 Graph 关系网络,咱们能够实现从 Graph 中派生出 JSON Tree 的能力。
上图是一个 GraphQL 的查询语句,它是一个包含不少 key 的层次结构,亦即一个 Tree。
它从跟节点里取 a 字段,而后向下分层,找到了 e。而 e 节点里也包含一个跟根节点同类型的 a 字段,所以它能够继续向下分层,重来一遍,又到了 e 节点,此时它只取了 data 字段,查询停止。
我编写了一个简单的 Resolver 函数,用来演示查询结果。
它很简单。Query 里返回跟字段名同样的字母,任何子节点的数据,都是拼接父节点的字母串。如此咱们能够从查询结果看出数据流动的层次。
查询结果以下:
第一个 e 节点的 data 字段里,拿到了父节点里的 data 数据,其父节点的 data 数据又是经过它的父节点里获取的,所以有一个数据链条。
而第二个 e 节点同理,它有两段链条。
只要不编写后续字段,咱们能够停留在任意节点的 data 字段里。
也就是说,咱们用做为 Tree 的 Query 语句,去裁剪了做为 Graph 的 Schema 数据关联网络,获得了咱们想要的 JSON 结构。
经过这个角度,咱们能够理解为何 GraphQL 不容许 Query 语句停留在 Object 类型,必定要明确的写出对象内部的字段,直到全部 Leaf Node 都是 Scalar 类型。
这不只仅是一个所谓的最佳实践,这也是 Graph 自己的特征。对象节点里,可能经过循环或者递归关系,拓展出无限大的数据结构。Query 语句必须写清楚,才能帮助 GraphQL 去裁剪掉没必要要的数据关联路径。
前面的 A, B, C, D, E 案例,并不能直观的让你们感觉到,Graph 网络结构的实际价值。它看起来像一个连线游戏。
放到 Facebook 的社交网络场景下,其必要性和价值就凸显了。
假设咱们要一次性获取用户的好友的好友的好友的好友的好友,基于 RESTful API 咱们有什么特别好的方法吗?很难说。
而 Graph 这种递归关联的结构,实现这种查询垂手可得。
咱们定义了一个 User 类型,挂到 Query 入口上的 user 字段里。Use 类型的 friends 字段又是一个 User 类型的列表。这样就构建了一个递归关联。
getFriends 查询语句,能够不断地从任意用户开始,关联其 friends,获得 friends 数组结果。任意一个 friend 也是 User,它也有本身的 friends。查询依据在最外层的 friends 停了下来,它只查询了 id 和 name 字段。
看到这里,另外一个经典的关于 GraphQL 的误解出现了:只有像 Facebook,Twitter 这类社交关系网络,才适合 GraphQL,而咱们的场景下,GraphQL 并不适用。
其实否则,社交关系网络里使用 GraphQL 特别有效,不意味着其它场景下,GraphQL 不能带来收益。
设想一个电商平台的场景,它有用户、产品和订单这组铁三角,其它库存、价格,优惠券,收藏等先不提。在最简单的场景下,GraphQL 依然能够发挥做用。
咱们构建了 User,Product 和 Order 三个类型,它们彼此之间有字段上的递归关联关系,是一个 Graph 结构。在 Query 入口类型上,分别有 user, product 和 order 三个字段。
据此,咱们能够实现从 user, product 和 order 任意维度出发,经过它们的关联关系,实现丰富而灵活的查询。
好比,查看用户的全部订单及其跟订单相关的产品,Query 语句以下:
咱们查询了 id 为 123 的用户,他的名字和订单列表,对于每一个订单,咱们获取该订单的建立时间,购买价格和关联产品,对于订单关联的产品,咱们获取了产品 id,产品标题,产品描述和产品价格。
当咱们的后端人员组织架构是按照领域模型来划分时,用户,产品和订单,一般是 3 个团队,他们各自提供领域相关的接口。经过 GraphQL 咱们能够很容易将它们整合到一块儿。
再好比,查看一个产品下的全部订单及其关联用户,Query 语句以下:
咱们查询了 id 为 123 的产品,它的产品标题,产品描述和价格,以及关联的订单。对于每一个关联订单,咱们查询了订单的建立时间,购买价格以及下订单的用户,对于下订单的用户,咱们查询了他的用户 id 和名称。
如你所见,只要构建出了 Graph 结构的数据网络,它不像 Tree 那样有惟一的 Root 节点。从任意入口出发,它均可以经过关联路径,不断的衍生出数据,获得 JSON 结果。
咱们没必要疲于编写面向产品详情页的接口,面向订单详情页的接口,面向用户信息的接口。咱们编写了一个数据关系网络,就足以适配不一样的场景。
此处演示的,只是用户,产品和订单这三个资源的关系网络,已经能够看出 GraphQL 的适用性。在实际场景中,咱们能搭建出更复杂的数据网络,它具有更强大的数据表达能力,能够给咱们的业务带来更多收益。
在掌握上述关于 GraphQL 的纲领知识后,咱们来看一下在实践中 ,GraphQL-BFF 的一种实际作法。
首先是技术选型,咱们主要采用了以下技术栈。
开发语言选用了 TypeScript,跑在 Node.js v10.x 版本上,服务端框架是 Koa v2.x 版本,使用 apollo-server-koa 模块去运行 GraphQL 服务。
Apollo-GraphQL 是 Node.js 社区里,比较知名和成熟的 GraphQL 框架。作了不少的细节工做,也有一些相对前沿的探索,好比Apollo Federation 架构等。
不过,有两点值得一提:
1)Apollo-GraphQL 属于 GraphQL 社区的一部分,而非 Facebook 官方的 GraphQL 开发团队。Apollo-GraphQL 在官方 GraphQL 的基础上进行了带有他们自身理念特色的封装和设计。像 Apollo Federation 这类目前看来比较激进的方案,即便是 GraphQL 官方的开发人员,对此也持保留态度。
2)Apollo-GraphQL 的重心是前文所说的第一类 API Gateway 角色的 GraphQL 服务,本文探讨的是第二类。所以,Apollo-GraphQL 里有不少功能对咱们来讲不必,有一些功能的使用方式,跟咱们的场景也不契合。
咱们主要使用的是 Apollo-GraphQL 的 graphql-tools 和 apollo-server-koa 两个模块,并在此基础上,进行了符合咱们场景的设计和改编。
GraphQL-BFF 的核心思路是,将多个 services 整合成一个中心化 data graph。
每一个 service 的数据结构契约,都放入了一个大而全的 GraphQL Schema 里;若是不作任何模块化和解耦,开发体验将会很是糟糕。每一个团队成员,都去修改同一份 Schema 文件。
这明显是不合理的。GraphQL-BFF 的开发模式,应该跟 service 的领域模型,有一一对应的关系。而后经过某种形式,多个 services 天然整合到一块儿。
所以,咱们设计了 GraphQL-Service 的概念。
GraphQL-Service 是一个由 Schema + Resolver 两部分组成的 JS 模块,它对应基于领域模型的后端的某个 Servcie。每一个 GraphQL-Service 应该能够按照模块化的方式编写,跟其它 GraphQL-Service 组合起来后,构建出更大的 GraphQL-Server。
GraphQL-Service 经过 GraphQL 的 Type Extensions 构建数据关联关系。
如上所示,咱们的 UserService 里面,只涉及到了 User 相关的类型处理。它定义了本身的基本字段,id 和 name。经过 extend type 定义了它在 Order 和 Product 数据里的关联字段,以及定义在 Query 里的入口字段。
从 User Schema 里咱们能够看到,User 有两类查询路径。
1)经过根节点 Query 以传递参数的方式,获取到 User 信息。
2)经过 Product 或 Order 节点,以数据关联的方式,获取到 User 信息。
上图是 OrderService 的 Schema,它也只涉及了 Order 相关的类型逻辑。一样是经过 extend type 定义了在 User 和 Product 里的关联字段,以及定义了在根节点 Query 里的入口字段。
Order 数据跟 User 同样,有两种消费路径。一种经过 Query 节点,另外一种是经过数据关联节点。
前面咱们演示 User, Order 和 Product 铁三角关系时,是在同一个 Schema 里编写它们的关联。咱们把多个 GraphQL-Service 的 Schema 整合到一块儿后,能够生成一样的结果:
上图不是咱们手动编写的,而是 merge 多个 GraphQL-Service 的 Schema 后生成的结果。能够看出来,跟以前手写的版本,整体上是同样的。
有了解耦的 Schema 并不足够,它只定义了数据类型及其关联。咱们须要 Resolver 去定义数据的具体获取方式,Resolver 也须要解耦。
无论是在官方的 GraphQL 文档里,仍是 Apollo-GraphQL 的文档里,Resolver 都是以普通函数的形态出现。
这在简单场景下,没有什么问题。正如在简单场景下,用 node.js 的 http.createServer 就能够建立一个 server。
如上,设置状态码,设置响应的 Content-Type,返回内容便可。
然而,在更复杂的真实项目中,咱们实际上须要 express、koa 等服务端框架,用中间件的模式编写咱们的服务端处理逻辑,由框架将它们整合为一个requestListener 函数,注册到 http.createServer(requestListener) 里。
在 GraphQL Server 里,虽然 endpoint 只有 /graphql 一个,但不表明它只须要一组 Koa 中间件。
正如一开始咱们指出的,每一个超级接口里都包含一半功能的 GraphQL 实现。GraphQL 是往超级接口的方向更进一步,不能简单地以普通接口的眼光去看待它。
在 Query 下的每一个字段,均可能对应 1 到多个内部服务的 API 的调用和处理。只用普通函数构成的 resolverMap,不足以充分表达其中的逻辑复杂度。
无论是用 endpoint 来表示资源,仍是用 GraphQL Field 字段来表示资源,它们只是外在形式略有不一样,不会改变业务逻辑的复杂度。
所以,采用比普通函数具备更好的表达能力的中间件,组合出一个个 Resolver,再整合到一个 ResolverMap 里。能够更好的解决以前解决不了,或者很难的问题。
所谓的架构能力,体如今理解咱们面对的问题的复杂度及其本质特征,并能选择和设计出合适的程序表达模型。
后面咱们将演示,正确的架构,如何轻易地克服以前难以解决的问题。
或许不少同窗并不清楚,express 或 koa 里的中间件模式,能够脱离做为服务端框架的它们而单独使用。正如 GraphQL 能够单独不做为 server,在任意支持 JavaScript 运行的地方使用同样。
咱们将使用 koa-compose 这个 npm 模块,去构造咱们的 Resolver。
前文里提到的 gql 函数,接受一个 Schema 返回一个 GraphQL-Service,每一个 GraphQL-Service 都有一个 resolve 方法:
resolve 方法,接受两个参数。第一个是 typeName,对应 GraphQL-Schema 里的 Object Type 的类型名称;第二个是 fieldHandlers,每一个 handler 支持中间件模式,最终它们将被 koa-compose 整合成一个 Resolver。
以 UserService 为例,其 Resolver 写起来以下:
做为普通函数的 Resolver 接收的全部参数,都被整合到了 ctx 里面。ctx.result 则是该字段的最终输出,相似于 koa server 里的 ctx.body。咱们刻意采用了 ctx.result 这个不一样于 ctx.body 的属性,明确区分咱们处理的是一个接口仍是一个字段。
在简单场景下,中间件模式的 Resolver 跟普通函数的 Resolver,仅仅是参数的数量和返回值的方式不一样。并不会增长大量的代码复杂度。
当咱们多个字段要复用相同的逻辑时,编写成中间件,而后将 handler 变成数组形式便可。(在代码里咱们用 json 模拟了数据库表,因此是同步代码,实际项目里,它能够是异步的调用接口或者查询数据库)。
上面的 logger,只是一个简单案例。除此以外,咱们能够编写 requireLogin 中间件,决定一个字段是否只对登录用户可用。咱们能够编写不一样的工具型中间件,注入 ctx.fetch, ctx.post, ctx.xxx 等方法,以供后续中间件使用。
每一个 GraphQL Field 字段,都拥有独立的一组中间件和 ctx 对象,跟其余字段互相不影响。咱们同时,能够把全部字段共享的中间件,放到 koa server 里的中间件里。
如上图所示,绿框是 endpoint,能够编写 koa server 层面的 middleware。而蓝框是 GraphQL Field 字段,能够编写 Resolver 层面的 middleware。endpoint 层面的 middleware 对 ctx 的修改,会影响到后面全部字段。
也就是说,咱们能够像上面那样。挂接口层面的 logger,能够知道整个 graphql 查询的耗时。编写一个中间件,在 next 以前,挂载一些方法,供后续中间件使用;在 next 以后,拿到 graphql 的查询结果,进行额外的处理。
GraphQL 是天生 mock 友好的模式,由于其 Schema 里已经指明了全部数据的类型及其关联;很容易能够经过 faker data 之类的手段,自动根据类型生成假数据。
然而,在实践中,实现 GraphQL Mocking 仍是有很多的挑战。
上图所示,在 Apollo GraphQL 里,mock 看似很简单,只须要在建立服务时,设置 mock 为 true,或者提供一个 mock resolver 便可。可是,一个全局的,跟着服务建立走的 mock,太过粗暴。
mock 的价值在于提供更好的数据灵活性以加速开发效率。它既能够在没有数据时,提供假数据;也能够在真数据的接口有问题时,不用重启服务,也能降级为假数据。它既能够是整个 GraphQL 查询级别的 mock,也能够是字段级别的 mock。
做为超级接口的 GraphQL 服务,全局的,在启动阶段就固化的 mocking,意义不大。
Apollo GraphQL 的 mocking 实践问题,正是它采用普通函数来描述 Resolver 所带来的;它很难简单的经过拓展某个 resolver 而支持 mocking。它不得不在建立服务时,额外新增一个 mock resolver map 去承担 mocking 职能。
而咱们的 composed resolver 处理动态 mocking 却异常简单。
它不只能够在运行时动态肯定,它不只能够细化到字段级别,它甚至能够跟着某次查询走 mock 逻辑(经过添加 @mock 指令)。
上图是默认状况下,基于 faker 这个 npm 包,根据数据类型生成的 mock data。
在咱们的设计里,默认的 mocking,其内部实现方式很简单。咱们先是编写了上图,根据 GraphQL Type 调用 faker 模块对应的方法,生成假数据。
而后在 createResolver 这个将中间件整合成 resolver 的函数里,先判断中间件里是否存在自定义的 mock handler 函数,若是没有,就追加前面编写的 mocker 处理函数。
咱们还提供了 mock 中间件,让开发者能指定 mock 数据来源,好比指定 mock json 文件。
mock 中间件,接收字符串参数时,它会搜寻本地的 mock 目录下是否有同名文件,做为当前字段的返回值。它也接收函数做为参数,在该函数里,咱们能够手动编写更复杂的 mock 数据逻辑。
有趣的地方是,mock/user.json 文件里,只包含上图红框的数据,其关联出来的 collections 字段,是真实的。这是合理的作法,mock 应该跟着 resolver 走。关联字段拥有本身的 resolver,可能调用本身的接口;不该该由于父节点是 mock 的,子节点也进入 mock 模式。
如此,咱们能够在父节点 resolver 对应的后端接口挂掉后,mock 它,让没挂掉的子节点 resolver 正常运行。若是咱们但愿子节点 resolver 也进入 mock。很简单,添加一个 @mock 指令便可。
如上所示,user 字段和 collections 字段的 resolver 都进入了 mock 模式。
自定义 mock resolver 函数的方式如上图所示,mock 中间件保证了,只有在该字段进入 mock 模式时,才执行 mock resolver function。而且,mock resolver function 内部依然有机会经过调用 next 函数,触发后面的真实数据获取逻辑。
以上全部这些灵活性,都来自于咱们选用了表达能力和可组合性更好的中间件模式,代替普通该函数,承担 resolver 的职能。
至此,咱们获得了一个简单而灵活的实践模式。咱们用 Schema 去构建 Data Graph 数据关联图,咱们用 Middleware 去构建 Resolver Map,它们都具有很强的表达能力。
在开发 GraphQL-BFF 时,咱们的 GraphQL-Service 跟后端基于领域模型的 Service,具备整体上的一一对应关系。不会产生后端数据层解耦后,在 GraphQL 层从新耦合的尴尬现象。
咱们对 GraphQL 的指望,不只仅停留在 BFF 层。咱们但愿经过积累在 BFF 层使用 GraphQL 的成功经验,帮助咱们摸索出在 Micro Frontend 架构上使用 GraphQL 模式的合理设计。
如前面所演示的,像 User,Product 和 Order 这种公共级别的数据类型,不可能只由一个团队去维护,它们须要被其它团队所拓展。使得咱们能够经过用户,找到它关联的订单,收藏,优惠券等由其它团队维护的数据。
放到 Micro Frontend 架构上,一个支付按钮,也夹杂了多种类型的数据,产品信息,用户信息,库存信息,UI 展现信息,交互状态信息等等,综合了这些信息,支付按钮被点击时,才获得了充分的数据,能够决定是否去支付。
朴素 Micro Frontend 的设计,用 Vue, React, Angular 不一样框架,分别维护不一样组件,经过 router/message-passing 等方式互相通信。在我看来,这是对后端微服务架构的拙劣模仿。
后端服务,各自部署在独立环境中,对体积不敏感;于是能够采用不一样的语言和技术栈。这不意味着将它简单的放到前端里同样成立。没法共享前端开发的基础设施,这不是微前端,这是一种人员组织架构上的混乱。
GraphQL 让咱们看到,基于领域模型的微前端架构,多是更好的方向。一个简单的支付按钮,也综合了多个领域模型,由多个开发者有组织的协同开发。并不由于它表面上看起来是一个 Button 组件,就由某个团队单独维护。
固然,探索 GraphQL 的其它方向的前提是,GraphQL-BFF 架构获得成功的验证。就现阶段的实践成果来看,咱们对此充满了信心。
尽管咱们的代码暂无开源计划,不过相信这篇文章,足够完整和清楚地介绍了咱们的 GraphQL-BFF 方案。但愿它能给你们带来一点帮助。