随着软件工程的发展,系统架构越来越复杂,分层越来越多,分工也越来越细化。我们知道,互联网是离用户最近的行业,前端页面可以说无时无刻不在变化。前端本质上还是用户交互和数据展示,页面的高频变化意味着对数据需求的高频变化。在绝大多数场景中,页面数据都来自于服务端,因此对页面变化的感知势必会传递到服务端,而服务端是要做业务能力沉淀的,需要逐步完善领域模型,沉淀商业逻辑,所以就产生了一个矛盾,一个领域能力沉淀和高频数据变化的矛盾。

为了解决这个矛盾,在业界不断的探索实践中,逐渐在架构层面演化出一个 BFF 层,Backend For Frontend,顾名思义,专门响应前端需求的服务端。

我们知道,不同的团队,因为其面临的业务场景不同,在面对相似问题的时候,通常解法也会不同。本文会结合笔者之前的 BFF 实践经验,介绍一种基于 GraphQL 的 BFF 实现。

BFF

Backend For Frontend 在 15/16 年的时候开始逐步受到关注,经过多年的实践和发展,业界已经有了非常多的解决方案,各种技术栈的实践都有。个人认为,要想准确的理解 BFF,需要抛开具体的技术细节和业务场景,从团队协作的角度来看。

BFF 的诞生是为了解决服务端领域能力沉淀和前端多样化的数据需求之间的矛盾的。因此,凡是有助于减少前端和服务端之间的冲突,降低开发成本的,都属于 BFF 讨论的范围。

我们从下面几个方面讨论一下 BFF 的适用场景。

  • 数据聚合裁剪

    通常,为了降低页面对服务端开发的影响,服务端会单独分离出一层 API 层,专门响应页面需求。但是由于缺乏一些明确的约束和指导,由于各种各样的原因,慢慢的 API 层中会出现一些业务逻辑,导致 API 层与业务层边界模糊,影响到对页面需求的响应。

  • SSR

    现在提到 SSR,通常会理解成基于 NodeJS + React/Vue 的服务端渲染。其实,在早些时候,还有基于模板的实现,知名的模板引擎有 Apache Velocity(Java)、EJS(JS)等。

  • 上传下载

    上传下载是一个比较常见的业务场景,但是常常因为功能比较通用化,很难作为一个业务能力被沉淀到业务系统中。

  • 接口转发

    在一些场景中,不需要聚合其他接口就能满足页面需求,可以直接将页面请求转发给领域服务。但是往往又因为需要登录、鉴权等一系列的校验逻辑,导致无法直接通过 Nginx 做转发,需要一层服务来处理,比如上面说的 API 层。

在这些问题中,我们可以看到,有些问题服务端开发比较擅长,有些问题前端开发比较擅长。这也正好印证了 BFF 本身的定位。

在构建 BFF 服务的时候,我们需要回答好一个问题,那就是由谁来构建 BFF?

我们先来分析下前端和服务端构建 BFF 的优缺点。

谁来构建 优势 劣势
前端 能对页面的多样化需求做出快速反应,几乎没有沟通成本 需要具备后端开发能力,对人的要求更高;同时需要前端深入理解业务模型
服务端 对业务模型理解更加透彻;服务管理的工具更丰富 对页面的需求不敏感

从表格中我们可以看到,不管是前端还是服务端来构建 BFF,都有其优势和劣势。因此我们还是需要结合具体的场景和需求来判断和选择。

如果在系统中数据聚合裁剪需求比较复杂,而页面变化相对可控,那么可以选择由服务端来建设 BFF。如果业务中页面变化频率很高,比如营销页面,那么由前端来负责构建 BFF 相对来说总体更优。

总之,不管是前端做还是服务端做,主要还是要看哪种成本更低,对需求的及时交付帮助更大。

GraphQL

前面讨论了 BFF 适用的几种场景,其中数据聚合裁剪可以说是最常见、冲突最大的一个。GraphQL 及其生态给出了一套解决方案。

在官方定义中,GraphQL 有两层含义:

  • 一种用于 API 的查询语言。
  • 一种运行时,使用现有数据完成这些查询的运行时。

在上面这张图中,我们通过 DSL 来描述系统中的类型。每一个类型就是一个节点,多个节点以及节点之间的关系最终会生成一张图。我们所有的查询都是基于这张图来做的。可以形象的理解一下,我们每一次查询,都是从图中的某一个节点开始,拎出一棵树出来。

GraphQL 有如下几个特点:

  • 按需返回数据,不多不少,且数据有极强的确定性

    • 端上描述需要的数据字段、嵌套结构等,服务器按描述返回。同时字段类型、字段是否为空等都有明确的约定。
  • 合并请求
    • 可以将页面中的多个数据请求合并成一个 GraphQL 查询。在弱网环境下尤其有用。
  • 完善的类型系统
    • 通过 Schema 描述系统中所有的模型,包括字段类型、模型结构、模型之间的关系等。通过内省模式可以查询到系统中的所有 Schema 信息,效果同 API 文档。
    • 围绕类型系统,可以开发出很多高效的工具,比如客户端代码生成工具 GraphQL Code Generator。
  • API 的平滑升级,无版本
    • 新增字段不影响已有查询
    • 标记 Deprecated 字段在内省模式中会自动标出
  • 字段细粒度控制
    • 通过定义合适的 Resolver,可以对系统中任意模型、任意字段进行细粒度控制。比如类型转换、监控埋点等。

基于 GraphQL 的 BFF 实践

图中从左到右分别有四个演变阶段。在虚线框以前,主要是服务端在负责页面的数据聚合。其中 Server API 是业务逻辑直接对外提供 API。Internal Rest 是分化出来的 API 层,专职于页面数据聚合。随着微服务的推广,服务端逐渐按领域划分微服务,对外提供原子化的接口。业务按照领域划分出微服务以后,人员的组织架构也会相应的调整(康威定律),原有的 Internal Rest 层没有专职团队负责,代码逐渐腐化、难以维护,进而影响迭代交付速度。

这时候,我们就在思考通过一些技术手段来解决这个问题。下面是我们的具体实践经历,以及后来的方案演进。

NestJS + Apollo GraphQL

社区中有很多优秀的 NodeJS 框架,比如 koa、express、eggjs 等,我们之所以选择了 NestJS 主要是两点考虑。一是 NestJS 原生采用 TypeScript 开发,提供了完善的类型支持,对于服务端开发而言,其重要性是不言而喻的。二是 NestJS 提供了丰富的框架能力,比如说拦截器、过滤器、守卫等,相比中间件来说,功能更丰富,定位也更明确。

Apollo GraphQL 是社区里比较成功的 GraphQL 解决方案,有丰富的商业化案例。而且,NestJS 对 Apollo 有官方的集成支持。

下面我们通过一个样例来展示一下上面介绍的 GraphQL 相关的能力。

相关样例代码传送门:服务端代码客户端代码

在样例中,我们定义了商品和订单两个模型,并定义了一些查询。下面是内省模式下看到的系统中的模型信息。

我们可以在 playground 中进行数据查询,如下是查询商品信息的样例。

订单信息中通常还需要关联查询商品信息,下面是查询样例。

可以看到,GraphQL 很好的解决了数据聚合裁剪的问题。但是在业务量级提升、系统覆盖业务越来越多的时候,逐渐暴露出两个严重的问题。第一个是可靠性问题,单体服务容易出现单点故障。第二个是发布效率问题,不同业务由于使用了同一个 BFF 服务,常常出现发布冲突,需要排队等待。因此我们又进行了第二阶段的探索。

Apollo Federation

我们先用一张图简单描述一下 Federation 架构的组成。

在 Federation 架构中,各个团队只负责自身业务范围内的子图构建,在网关层,Federation Gateway 会将这些子图拼装成一张完整的图。这样就可以在保证 GraphQL 能力的同时,各个团队能够互不干扰、高效开发。

我们再来看一看 Federation 的样例,最终对外提供的能力还是商品和订单的查询,效果与前面的 demo 一致,但是系统中原来的单体服务被拆分成了三个,一个网关,一个商品服务,一个订单服务,这三个服务的开发迭代互不影响。

相关代码传送门:网关代码商品服务代码订单服务代码

这里有个问题我们需要关注一下,在 gateway 的定义代码中,我们可以看到子服务列表是写死的。

import { Module } from '@nestjs/common';
import { GraphQLGatewayModule } from '@nestjs/graphql'; const graphqlGatewayModule = GraphQLGatewayModule.forRootAsync({
useFactory: async () => ({
server: {
path: '/graphql',
},
gateway: {
serviceList: [
{
name: 'goods',
url: 'http://localhost:3001/goods/graphql',
},
{
name: 'order',
url: 'http://localhost:3002/order/graphql',
},
],
},
}),
});

如果新增一个服务,就需要修改网关代码并重新发布,十分不便。我们需要一个自动化的服务注册机制。

Schema 集成系统

Apollo Federation 本身提供了一个 Schema 集成系统,其架构如下:

子服务将 schema 推送到 Apollo 官方的 registry 中,然后 registry 会将最新的信息推送给一个远端服务。网关再从这个远端服务通过轮询的方式拉取配置。

因此,我们仿照这个架构,设计开发了我们自己的 schema 集成系统。

效果类似,子服务将各自的 schema 推送给 SIS(Schema Integration System),SIS 在完成必要的校验以后,生成最新的有效配置信息,网关再通过轮询的方式将配置信息拉取到本地,动态更新。

实现网关的动态更新,依赖了一个内部 API,还处于实验阶段,需要持续关注后面官方的迭代计划。

基础设施建设

经过不断的迭代,整个 BFF 层已经形成了一套解决方案,周边有着完善的基础设施建设。

左边一栏属于开发阶段的基建,有项目脚手架、代码自动生成工具等提效工具。右边一栏属于运维阶段的基建,有阿里云 SLS 日志系统、Sentry 错误告警系统、Skywalking 链路监控系统等。

反思

前端来构建 BFF 层,通过引入 GraphQL 来解决数据聚合裁剪的问题,整个项目经过了多轮的迭代和演进,功能越来越完善,协作效率越来越高。

随着 BFF 层的展开,对前端的要求也越来越高,虽然是使用 JavaScript,但因为是完全的服务端开发,前端需要学习很多服务端开发的基础技能,同时思维方式也需要转变,从数据的消费方转变成数据的生产方,需要考虑向后兼容等一系列问题。

当然我们可以通过一系列的工程手段降低这些隐形成本,但是长远看,前端同学的服务端开发能力、运维能力还是必不可少的。

总结和展望

本文介绍了利用 GraphQL 来做数据聚合裁剪的 BFF 实践经验。从最初的单体服务到后面的服务拆分,经过了多轮的迭代演进,以及不断完善的周边基础设施,整个 BFF 最终形成了一个比较完善的解决方案。同时,BFF 也带来了如何快速构建服务端开发能力等新的问题和挑战。

正如文章开头所说,不同的业务场景,不同的开发团队都会导致 BFF 的技术选择和实践形式的不同。技术手段取决于业务场景和目标。适合自己的才是最好的。

关注微信公众号,获取最新推送~

加微信,深入交流~

基于 GraphQL 的 BFF 实践的更多相关文章

  1. 好未来数据中台 Node.js BFF实践(一):基础篇

    好未来数据中台 Node.js BFF实践系列文章列表: 基础篇 实战篇(TODO) 进阶篇(TODO) 好未来数据中台的Node.js中间层从7月份开始讨论可行性,截止到9月已经支持了4个平台,其中 ...

  2. prime 又一个开源的基于graphql 的cms

    prime 是一个开源的基于graphql 的cms,类似的已经又好多了,strapi 就是一个(graphql 是通过插件扩展的) graphcms 是一款不错的,但是是收费的,prime 是基于t ...

  3. grandstack 基于graphql&&react&& apollo&& neo4j 的全栈开发工具

    grandstack是一个基于graphql&&react&& apollo&& neo4j 的全栈开发工具. 有篇关于graphql 的5个常见问题的 ...

  4. graphcool-framework 一个基于graphql的后端开发框架

    特性 GraphQL database 启动便捷的数据迁移数据演变 Flexible auth 基于jwt 的认证 Realtime API 基于graphql 的Subscriptions High ...

  5. 《分享》Graphql入门与实践

    最近项目用到了graphql,学习了一些并在公司做了一个小分享,希望对你有帮助 一.介绍 Graphql是一种面向数据的API查询语言 Graphql给前端提供一种强力的查询工具,我们可以根据自己定义 ...

  6. 基于 MySQL 的数据库实践(基本查询)

    首先根据准备工作中的操作导入大学模式,打开数据库连接后进入到 MySQL 的交互界面,再使用命令 use db-book; 切换到 db-book 数据库. 单关系查询 SQL 查询的基本结构由三个子 ...

  7. 3D GIS 应用开发 —— 基于 Mapbox GL 的实践总结

    最近在折腾的 web 端的可视化项目,由于相关业务的需要,用到了 Mapbox 这一地图开发的神器.在此先奉上一个基于mapbox-gl实现的demo(来源:uber的deck.gl项目): 下面我们 ...

  8. jenkins X实践系列(2) —— 基于jx的DevOps实践

    jx是云原生CICD,devops的一个最佳实践之一,目前在快速的发展成熟中.最近调研了JX,这里为第2篇,使用已经安装好的jx来实践CICD,旨在让大家了解基于jx的DevOps是如何运转的,感兴趣 ...

  9. 中小团队基于Docker的devops实践

    笔者所在的技术团队负责了数十个项目的开发和维护工作,每个项目都至少有dev.qa.hidden.product四个环境,数百台机器,在各个系统之间疲于奔命,解决各种琐碎的问题,如何从这些琐碎的事情中解 ...

  10. 2018-2019-2 20165330《网络对抗技术》Exp10 Final 基于PowerShell的渗透实践

    目录 实验内容 实验步骤 实验中遇到的问题 实验总结与体会 实验内容 PoweShell简介 PowerShell入门学习 PowerShell渗透工具介绍 相关渗透实践分析 ms15-034之Pow ...

随机推荐

  1. H5 表格标签基本使用

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  2. 耗电—Android

    Android应用耗电分析与优化 http://sanwen8.cn/p/297Ut7b.html http://www.cnblogs.com/kobe8/p/3819305.html 有效控制An ...

  3. Thinkphp3.2.3 执行query命令 包括在模板中使用&lt;php&gt; &lt;/php&gt;时 query的使用方法

    $sql="select * from `rjshop_productbase` where `id`=1"; $Model =M();$query=$Model->quer ...

  4. sax/dom/jdom/dom4j的区别

    sax/dom/jdom/dom4j的区别[转] 博客分类: xml   网上能够查到很多的4种解析方式的区别,我再做一下摘录和总结,顺带给自己做个备份. SAX sax分析器在对xml文档进行分析时 ...

  5. L#脚本语言,直接把DLL当脚本执行(图解说明)

    L#是什么:Run DLL as a Script. A Pure C# IL Runner,直接解析执行IL的脚本引擎. 从原理上讲是模拟执行了CLR的工作,从表现上讲就是把DLL作为资源直接加载执 ...

  6. android system.img

    哥们要我做些模拟包,给过来的是mtk的底包,需要从system.img中提取部分文件. 网上一找资料,说是yaffs2文件系统,同时以前做linux的时候也是用yaffs2,感觉碰到老朋友了,不管三七 ...

  7. poj 3130 How I Mathematician Wonder What You Are!

    http://poj.org/problem?id=3130 #include <cstdio> #include <cstring> #include <algorit ...

  8. iOS下的实际网络连接状态检测(转)

    序言 网络连接状态检测对于我们的iOS app开发来说是一个非常通用的需求.为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示.对绝大部分iOS开发者来说,从苹果示例代 ...

  9. JAVA Useful Program(1)

    public static void main(String[] args){       //字符串有整型的相互转换          String str=String.valueOf(123); ...

  10. MySQL操作的一些优化

    1.用于不要使用select * from table xxx. 需要查询哪些列就在语句中指明,一个表结构复杂时,可能会有上百列,使用*来查询时会造成很大的浪费. 2.选择合适的属性及大小    例如 ...