echarts前后端交互数据_GraphQL:前后端数据交互方案

随着多终端、多平台、多业务形态、多技术选型等各方面的发展,前后端的数据交互,日益复杂。

同一份数据,可能以多种不同的形态和结构,在多种场景下被消费。

在理想情况下,这些复杂性可以全部由后端承担。前端只管从后端接口里,拿到已然整合完善的数据。

然而,不管是因为后端的领域模型,还是因为微服务架构。作为前端,我们感受到的是,后端提供的接口,越发不够前端友好。我们必须自行组合多个后端接口,才能获取到完整的数据结构。

面向领域模型的后端需求,跟面向页面呈现的前端需求,出现了不可调和的矛盾。

在这种背景下,本着谁受益谁开发的原则。我们最后选择使用Node.js 搭建专门服务于前端页面呈现的后端,亦即 Backend-For-Frontend,简称 BFF。

我们面临了很多不同的技术选型,主要围绕在权衡RESTful API 和GraphQL。

正如标题所示,我们最终选用的是GraphQL。

本文将介绍我们对GraphQL 所作的考察、探索、权衡、技术选型与设计等多方面的内容,希望能给大家带来一些启发。


一、GraphQL 模式出现的必然性

面向前端页面的数据聚合层,其接口很容易在迭代过程中,变得愈加复杂;最终发展成一个超级接口。

它有很多调用方,各种不同的调用场景,甚至多个不同版本的接口并存,同时提供数据服务。

所有这些复杂性,都会反映到接口参数上。

接口调用的场景越多,它对接口参数结构的表达能力,要求越高。如果只有一个boolean 类型的参数,只能满足 true | false 两种场景罢了。

以产品详情接口为例,一种很自然的请求参数结构如下:

30326810f1a0bc0dde46ecb2e6fe7219.png

里面包含ChannelCode 渠道信息,IsOp 身份信息,MarketingInfo 营销相关的信息,PlatformId 平台信息,QueryNode 查询的节点信息,以及 Version 版本信息。最核心的参数 ProductId,被大量场景相关的参数所围绕。

审视一下QueryNode 参数,很容易可以发现,它正是 GraphQL 的雏形。只不过它用的是更复杂的 JSON 来描述查询字段,而 GraphQL 用更简洁的查询语句,完成同样的目的。

并且,QueryNode 参数,只支持一个层级的字段筛选;而 GraphQL 则支持多层级的筛选。

GraphQL 可以看作是 QueryNode 这种形式的参数设计的专业化。相比用 JSON 来描述查询结果,GraphQL 设计了一个更完整的 DSL,把字段、结构、参数等,都整合到一起。

仿照格林斯潘第十定律:

任何C或Fortran程序复杂到一定程度之后,都会包含一个临时开发的、不合规范的、充满程序错误的、运行速度很慢的、只有一半功能的Common Lisp实现。

或许可以说:

任何接口设计复杂到一定程度后,都会包含一个临时开发的、不合规范的、只有一半功能的GraphQL实现。

从SearchParams, FormData 到 JSON,再到 GraphQL 查询语句,我们看到不断有新的数据通讯方式出现,满足不同的场景和复杂度的要求。

站在这个层面上看,GraphQL 模式的出现,有一定的必然性。


二、GraphQL 语言设计中的必然性

作为一个查询相关的DSL,GraphQL 的语言设计,也不是随意的。

我们可以做一个思想实验。

假设你是一名架构师,你接到一项任务,设计一门前端友好的查询语言。要求:

1) 查询语法跟查询结果相近

2) 能精确查询想要的字段

3) 能合并多个请求到一个查询语句

4) 无接口版本管理问题

5) 代码即文档

我们知道查询结果是JSON 数据格式。而 JSON 是一个 key-value pair 风格的数据表示,因此可以从结果倒推出查询语句。

ad91202f643666d8405e73d964bb0fdd.png

上图是一个查询结果。很显然,它的查询语句不可能包含value 部分。我们删去 value 后,它变成下面这样。

3f58f1af717a0c3666ff95e68db3e4e8.png

查询语句跟查询结果拥有相同的key 及其层次结构关系。这是我们想要的。

我们可以再进一步,将冗余的双引号,逗号等部分删掉。

63a2b4b16056e56f7f632d88160388dc.png

我们得到了一个精简的写法,它已经是一段合法的GraphQL 查询语句了。

其中的设计思路和过程是如此简单直接,很难想象还有别的方案比目前这个更满足要求。

当然,只有字段和层级,并不足够。符合这种结构的数据太多了,不可能把整个数据库都查询出来。我们还需要设计参数传递的方式,以便能缩小数据范围。

e97b304edae68e83e906ba3fdf94cb6b.png

上图是一个自然而然的做法。用括号表示函数调用,里面可以添加参数,可谓经典的设计。

它跟ES2015 里的 (Method Definitions Shorthand) 也高度相似。如下所示:

0d0d117f66d11f2f839b0fc044022a09.png

前面演示的GraphQL 参数写法,参数值用的是字面量 userId: 123。这不是一个特别安全的做法,开发者会在代码里,用拼接字符串的方式将字面量值注入到查询语句,也就给了恶意攻击者注入代码的机会。

我们需要设计一个参数变量语法,明确参数位置和数量。

5050405980c8cf9ce2d52b2a908650cf.png

我们可以选用$xxx 这种常见的标记方法,它被很多语言采用来表示变量。沿用这种风格,可以大大减少开发者的学习成本。

前后端通讯的另一个痛点是,命名。前端经常吐槽后端的字段名过于冗长,或者不知所云,或者拼写错误,或者不符合前端表述习惯。最常见的情况是,后端字段名以大写字母开头,而前端习惯Class 或者 Component 是大写字母开头,实例和数据,则以小写字母开头。

我们期望有机会进行字段名调整。

别名映射(Alias)语法,正是为了这个目的而出现的。

f82ea4e946a72963862c910cf58e25d3.png

上面这种别名映射的语法,在其它语言里也很常见。如果不这样写,顶多就是变成:

uid as Uid 或者 uid = Uid 这类做法,差别不大。我认为选用冒号更佳,它跟 ES2015 的解构语法很接近。

6279910171987030bdffaff6bac6239c.png

至此,我们拥有了key 层级结构,参数传递,变量写法,别名映射等语法,可以编写足够复杂的查询语句了。不过,还有几个小欠缺。

比如对字段的条件表达。假设有两次查询,它们唯一的差别就是,一个有A 字段,另一个没有 A 字段,其它字段及其结构都是相同的。为了这么小的差别 ,前端难道要编写两个查询语句?

这显然不现实,我们需要设计一个语法描述和解决这个问题。

它就是——指令(Directive)。

05db923fd7b32e873b86aee74c60b1c4.png

指令,可以对字段做一些额外描述,比如

@include,是否包含该字段;

@skip,是否不包含该字段;

@deprecate,是否废弃该字段;

除了上述默认指令外,我们还可以支持自定义指令等功能。

指令的语法设计,在其它语言里也可以找到借鉴目标。Java,Phthon 以及 ESNext 都用了 @ 符号表示注解、装饰器等特性。

有了指令,我们可以把两个高度相似的查询语句,合并到一起,然后通过条件参数来切换。这是一个不错的做法。不过,指令是跟着单个字段走的,它不能解决多字段的问题。

比如,字段A 和字段 B,拥有相同的总体结构,仅仅只有 1 个字段名的差异。前端并不想编写一样的 key 值重复多次。

这意味着,我们需要设计一个片段语法(Fragment)。

fac8a1c5893e4857faf192fd04c6a503.png

如上所示,用fragment 声明一个片段,然后用三个点表示将片段在某个对象字段里展开。我们可以只编写一次公共结构,然后轻易地在多个对象字段里复用。

这种设计也是一个经典做法,跟JavaScript 里的 Spread Properties 很相近。

a2f732c582d768e481581479ed201853.png

至此,我们得到了一个相对完整的,对前端友好的查询语言设计。它几乎就是GraphQL 当前的形态。

如你所见,GraphQL 的查询语言设计,借鉴了主流开发语言里的众多成熟设计。使得任何拥有丰富的编程经验的开发者,很容易上手 GraphQL。

按照同样的要求,重新来一遍,大概率得到跟当前形态高度接近的设计。这是我理解的GraphQL 语言设计里包含的必然性。


三、GraphQL 的组成与链路

查询语法,是GraphQL 面向前端,或者说面向数据消费端的部分。

除此之外,GraphQL 还提供了面向后端,或者说面向数据提供方的部分。它就是基于 GraphQL 的 Type System 构建的 Schema。

一个GraphQL 服务和查询的链路,大致如下:

06fd4dba59ff42a62374110a591fbd8b.png

首先,服务端编写数据类型,构建一个数据结构之间的关联网络。其中Query 对象是数据消费的入口。所有查询,都是对 Query 对象下的字段的查询。可以把 Query 下的字段,理解为一个个 RESTful API。比如上图中的,Query.post 和 Query.author,相当于 /post 和 /author 接口。

GraphQL Schema 描述了数据的类型与结构,但它只是形状(Shape),它不包含真正的数据。我们需要编写 Resolver 函数,在里面去获取真正的数据。

Resolver 的简单形式如下:

47ffab0f1568bffd2afc52c1325b152f.png

每个Query 对象下的字段,都有一个取值函数,它能获取到前端传递过来的 query 查询语句里包含的参数,然后以任意方式获取数据。Resolver 函数可以是异步的。

有了Resolver 和 Schema,我们既定义了数据的形状,也定义了数据的获取方式。可以构建一个完整的 GraphQL 服务。

但它们只是类型定义和函数定义,如果没有调用函数,就不会产生真正的数据交互。

前端传递的query 查询语句,正是触发 Resolver 调用的源头。

ac1923e0177bf096a204f67e5ae0b144.png

如上所示,我们发起了查询,传递了参数。GraphQL 会解析我们的查询语句,然后跟 Schema 进行数据形状的验证,确保我们查询的结构是存在的,参数是足够的,类型是一致的。任何环节出现问题,都将返回错误信息。

数据形状验证通过后,GraphQL 将会根据 query 语句包含的字段结构,一一触发对应的 Resolver 函数,获取查询结果。也就是说,如果前端没有查询某个字段,就不会触发该字段对应的 Resolver 函数,也就不会产生对数据的获取行为。

此外,如果Resolver 返回的数据,大于 Schema 里描绘的结构;那么多出来的部分将被忽略,不会传递给前端。这是一个合理的设计。我们可以通过控制 Schema,来控制前端的数据访问权限,防止意外的将用户账号和密码泄露出去。

正是如此,GraphQL 服务能实现按需获取数据,精确传递数据。

da6f318aaf2223d4720b6aea1229d941.png

上图是默认情况下,基于faker 这个 npm 包,根据数据类型生成的 mock data。

31cdbbc003265c8a2e20371e658df324.png

在我们的设计里,默认的mocking,其内部实现方式很简单。我们先是编写了上图,根据 GraphQL Type 调用 faker 模块对应的方法,生成假数据。

33c518a5d3ec2f44db3e902ff4a03594.png

然后在createResolver 这个将中间件整合成 resolver 的函数里,先判断中间件里是否存在自定义的 mock handler 函数,如果没有,就追加前面编写的 mocker 处理函数。

我们还提供了mock 中间件,让开发者能指定 mock 数据来源,比如指定 mock json 文件。

f85b9c5ffce2897919bd1c6f9b0b65ef.png

mock 中间件,接收字符串参数时,它会搜寻本地的 mock 目录下是否有同名文件,作为当前字段的返回值。它也接收函数作为参数,在该函数里,我们可以手动编写更复杂的 mock 数据逻辑

fb482459fce7887b876240fd5a10d483.png

有趣的地方是,mock/user.json 文件里,只包含上图红框的数据,其关联出来的 collections 字段,是真实的。这是合理的做法,mock 应该跟着 resolver 走。关联字段拥有自己的 resolver,可能调用自己的接口;不应该因为父节点是 mock 的,子节点也进入 mock 模式。

如此,我们可以在父节点resolver 对应的后端接口挂掉后,mock 它,让没挂掉的子节点 resolver 正常运行。如果我们希望子节点 resolver 也进入 mock。很简单,添加一个 @mock 指令即可。

1d808717082313209d7da7fc2da0df04.png

如上所示,user 字段和 collections 字段的 resolver 都进入了 mock 模式。

b41e5328d5fe0d992f33849cca7c4cfa.png

自定义mock resolver 函数的方式如上图所示,mock 中间件保证了,只有在该字段进入 mock 模式时,才执行 mock resolver function。并且,mock resolver function 内部依然有机会通过调用 next 函数,触发后面的真实数据获取逻辑。

25d4b731ba5a2f3c4f10fc7800102ee1.png

以上所有这些灵活性,都来自于我们选用了表达能力和可组合性更好的中间件模式,代替普通该函数,承担resolver 的职能。


总结

至此,我们得到了一个简单而灵活的实践模式。在开发GraphQL-BFF 时,我们的 GraphQL-Service 跟后端基于领域模型的 Service,具有总体上的一一对应关系。不会产生后端数据层解耦后,在 GraphQL 层重新耦合的尴尬现象。

关于GraphQL 还有很多话题可以讨论,比如 batching , caching 等。这部分内容在网络上很多 GraphQL 的文档和教程里都可以找到,这里我们不再赘述。

总的而言,根据我们对raphQL 的考察和实践,我们认为它可以比 RESTful API 更好的解决我们面对的问题。

我们对GraphQL 的期望,不仅仅停留在 BFF 层。我们希望通过积累在 BFF 层使用 GraphQL 的成功经验,帮助我们摸索出在 Micro Frontend 架构上使用 GraphQL 模式的合理设计。

GraphQL 让我们看到,基于领域模型的微前端架构,可能是更好的方向。一个简单的支付按钮,也综合了多个领域模型,由多个开发者有组织的协同开发。并不因为它表面上看起来是一个 Button 组件,就由某个团队单独维护。

当然,探索GraphQL 的其它方向的前提是,GraphQL-BFF 架构得到成功的验证。就现阶段的实践成果来看,我们对此充满了信心。

尽管我们的代码暂无开源计划,不过相信这篇文章,足够完整和清楚地介绍了我们的GraphQL-BFF 方案。希望它能给大家带来一点帮助。​​​​


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值