关于前端:基于-GraphQL-的云音乐-BFF-建设实践

45次阅读

共计 8318 个字符,预计需要花费 21 分钟才能阅读完成。

图片来自:https://bz.zzzmh.cn/
本文作者:cgt

背景: 如何解耦大前端与服务端的适配层依赖

谈到 BFF,置信大家都不会太生疏,过来在云音乐,前后端的合作架构始终维持比拟传统的前后端合作模式。各个端所须要的接口齐全依赖服务端提供,服务端同学除了须要实现微服务的业务逻辑外,还须要针对前端页面调度各个领域的微服务,依据前端的数据诉求进行肯定水平的组装和适配。

在年初,咱们打算针对云音乐的 P0 页面,个人主页进行改版,云音乐的个人主页聚合了来自各个页面畛域的数据,比方用户个人信息,Mlog,云圈,K 歌等等,这些数据来自于各个不同的服务端团队。对大前端同学来说,咱们冀望能通过尽量少的接口调用获取到这些数据,以保障页面性能,同时,咱们冀望获取的数据接口能和页面 UI 高度适配,不须要在端上进行太多的数据转换。因而,服务端同学为咱们抽离了一层独立的两头服务,负责聚合各个业务的接口数据,同时,咱们须要各个业务服务端将业务畛域的 DTO 转换为 VO,保障能和 UI 进行适配。

在改版过程中,咱们发现了这个模式的一些问题:

  • 大前端所需接口的契约定义,对服务端有 深度依赖,很多时候一个页面字段的变更就须要平台服务端以及业务服务端进行评估和排期,因为职能差别,两头会产生大量的沟通老本。
  • 因为 前端 UI 的多变性,各个业务服务端针对该场景提供的接口,很难具备复用性,一旦更换了其余场景,服务端同学又不得不封装新的接口。

而针对这些问题,咱们发现业界其实曾经给出了比拟成熟的解决方案,就是通过在架构上引入 BFF 层。

BFF 的全称是「Backend For Frontend」,顾名思义就是面向前端的后端。它的主要职责就是针对页面的数据诉求,进行微服务的调度以及数据的组装和适配,这一部分原先咱们通过微服务去实现,但当初它从微服务拆解进去失去了独立。

在 BFF 的架构里,咱们不再须要平台服务端为咱们提供数据聚合,这解决了咱们之前提到的问题:

  • 大前端同学能够开始自行实现这一层的数据组装工作,从而与服务端在适配层实现 解耦,大部分字段的变更都能够由前端同学闭环实现,再没有大量的沟通老本。
  • 服务端同学无须再进行从 DTO 到 VO 的数据转换,从而能够提供 复用性更强 的接口,微服务的职责也会更加明确。

在云音乐,存在大量相似的场景,咱们冀望在这些场景下都能落地 BFF 架构,最终,随着可复用接口的积淀,以及沟通老本的升高,能够帮忙咱们晋升整体的业务吞吐量。

那么,问题来了,如何在大量相似的场景中,让大前端同学来承接 BFF 层呢?

为什么咱们抉择 GraphQL?

Faas VS GraphQL

目前业界比拟支流的两种 BFF 实现计划。

首先是基于 NodeJS + Faas 的模式,这种模式是基于大部分 Web 前端同学对 NodeJS 有肯定根底,能够疾速上手,同时它的编排非常灵活,根本能满足所有 BFF 诉求,甚至能超出 BFF 的边界,最后,咱们也冀望依附这种模式落地 BFF,但很快,咱们就发现这种模式面临的一些挑战:

  • 根底建设要求高:这种模式对团队的 Node 基础设施和云原生基础设施有肯定要求,毕竟把握 NodeJS 开发是一方面,针对服务的监控,运维,部署,线上问题调试都须要有对应的解决方案,并且咱们须要保障这些保障能笼罩到所有 NodeJS 服务
  • 存在肯定学习老本:除去 Web 前端的同学,对原生客户端的同学来说,只管 NodeJS 比拟轻量,也是一门全新的语言。

第二种形式就是咱们明天要聊的基于 GraphQL 的模式,GraphQL 定义来一套用于 API 的查询语言,开发者甚于能够通过一些低代码的编排疾速实现查询语言的定义,这给咱们带来了以下劣势:

  • 与技术栈解耦:开发者只须要认知 GraphQL 的 DSL,而不必再多学习一门语言,而 GraphQL 的 DSL 相对来说要好上手得多。
  • 复杂度更加可控:咱们能够对立实现 GraphQL 的执行引擎,开发者全副基于咱们的引擎服务执行查问,可能自定义的仅仅是数据图以及查问语句,从而咱们能够将服务开发的一些最佳实际附着到引擎下面。

什么是 GraphQL?

好,那到底什么是 GraphQL 呢?

GraphQL 总体分成两局部:

  • 一套用于 API 的查问 DSL:也能够称为 GraphQL 语句,你能够在这套 DSL 中形容你的查问所须要的字段,以及须要调用的接口,所需传递的参数等等。
  • 一个基于图状数据的服务端运行时:来执行这套查问 DSL,它的执行逻辑就是从一张残缺的数据图上,依据 GraphQL 语句的形容找到须要的节点,调度波及的接口,最初返回合乎查问语句的数据。

比方:在上图展现的案例中,咱们在 (图左) 编写了查问语句,(图右)则是引擎执行该查问语句后,在数据图上命中的节点。

能够看出,落地 GraphQL 的要害就在于实现它的服务端运行时,而 GraphQL 的运行时整体也能够拆解为三个局部:

  • GraphQL 引擎:解析 GraphQL 语句,目前社区曾经提供了各个开源版本的 GraphQL 引擎,包含 NodeJS,Java,Python 等等,咱们选定适宜本人的版本即可。
  • 类型定义:GraphQL 的类型零碎其实和其余类型零碎大同小异,GraphQL 提供了一些根底标量,你能够在这些根底标量的根底上一直扩大本人的业务模型,最终生成图状数据结构。
  • 解析器:咱们须要形容这些类型节点所须要执行的查问,当然,并不是所有的节点都须要执行查问,咱们只须要保障查问的后果和节点的类型定义统一即可。比方在上图的节点中,咱们别离给 song 节点和 album 节点执行了一次查问,他们会调获取歌曲详情以及获取专辑详情的 RPC 接口返回相应的数据。

如何在云音乐落地?

在理解 GraphQL 的运行机制后,咱们开始思考如何在云音乐进行落地,在进行方案设计的阶段,咱们提出了一些问题:

  • 咱们如何让大前端同学可能搭建稳固牢靠的 GraphQL 运行时?大部分大前端同学并不具备服务端开发教训,对服务的开发,部署,运维根本无所不知,从零开始搭建 GraphQL 运行时会带来微小的操作老本
  • 如何疾速上手 GraphQL 语句?只管 GraphQL 语句上手并不简单,但它自身不在前端同学的常识体系内,上手仍然存在肯定的学习老本。
  • 如何与云音乐现有研发体系的对接?
  • 如何尽可能扩充 GraphQL 的边界?

针对前两个问题,咱们想到能够通过 低代码 的形式进行 GraphQL 的利用研发,低代码能够说是以后业界十分风行的,一种能够 突破职能边界 的伎俩,很多团队通过低代码让服务端同学具备了搭建前端页面的能力。那么反过来思考,前端同学同样能够通过低代码从而具备编排服务端逻辑的能力。思考到这个方向后,咱们发现 GraphQL 人造就非常适合采纳低代码的模式进行搭建,其 DSL 的设计能够不便地转换成结构化的数据,从而映射成界面的操作。

针对第三个问题,GraphQL 利用应该具备和云音乐惯例利用统一的公布流程管控,防止无序公布导致的线上事变,咱们通过 Git 仓库对 GraphQL 利用的语句,类型定义,解析器进行治理,并融入云音乐前端研发平台 Febase 进行公布流程的管控。

最初一个问题,咱们心愿 GraphQL 能解决至多 70% 的 BFF 编排场景,如果仅仅依赖其本身的能力,会导致落地场景受限而意义不大,因而咱们基于 GraphQL 的指令机制,对 GraphQL 的能力进行了扩大,从而能应答更多的场景。

分布式的架构设计

咱们整体采纳了分布式的架构设计:

从流量走向上看,前端仍然通过 Restful 申请获取页面所须要的数据,这样做的目标是咱们的所有申请仍然能够依赖云音乐的通用 API 网关,具备 流量管制,异样降级,动态化 的能力,从而极大地晋升了接口稳定性。

而当申请通过 API 网关后,会转发到 GraphQL 利用所在的集群,GraphQL 利用的内置引擎会将接口 URL 转换成 GraphQL 语句,从而执行 GraphQL 语句,调度服务端 RPC 接口进行数据组装,最终返回页面须要的数据。

咱们会为每一个 GraphQL 利用调配独立的云原生容器,依靠于云音乐云原生的根底建设,咱们能够灵便安顿每一个 GraphQL 利用所须要的 Pod 数量,甚至能依据 CPU 进行容量的扩缩,从而加重前端同学的运维累赘。

在 Febase 平台,咱们提供了低代码的 GraphQL 编辑器,Groovy 脚本的编写能力,公布流程的管控,可视化的数据图编排能力,最终基于这些能力,平台可能输入一份 GraphQL 利用配置,这份配置的内容包含:

  • 从 URL 到 GraphQL 语句的映射关系
  • 执行查问语句所须要的数据图
  • 查问节点的解析器配置

引擎通过监听 zookeeper 拿到这份配置,并进行更新,这个过程就是 GraphQL 利用的 部署 过程,因为整个部署过程不会波及到服务的重启,仅仅是一次配置文件的热更新,所以它的日常公布也会十分快捷,几秒就能实现,进一步晋升咱们的研发效率。

在这些根底能力之外,咱们也和云音乐的一些基础设施平台进行了买通。比方:

  • 通过 Mock 平台,咱们容许开发者自在配置接口的 Mock 数据,只须要在申请头中退出一个标记位,就能够让申请走 Mock 链路。
  • 所有 GraphQL 语句,数据图,脚本都会保留在 Gitlab 进行治理,通过分支进行编辑。
  • 云音乐的契约治理平台为咱们提供了 Java 服务端 RPC 的数据模型,使得咱们能够以近乎零老本的形式来构建数据图。
  • 基于 Serverless 进行利用容器的部署,保障咱们的服务能够灵便地扩容缩容。
  • 买通了性能,日志等各类服务监控平台,具备齐备的服务运维能力。

基于契约疾速构建 GraphQL Schema

在理解咱们的整体架构后,咱们持续来看看 Febase 是如何以近乎零老本的形式构建 GraphQL 的数据图的。上面是一张非常简单数据图的构建过程,GraphQL 构建数据图的形式就是从根节点登程,录入字段以及字段对应的模型,并且咱们能够在任意模型下插入新的字段,并定义该字段的模型。在插入字段的时候,咱们须要定义字段对应的解析器,也就是该如何获取到字段对应的数据。

咱们发现,在传统的 GraphQL 数据图编排中,开发者须要自行定义模型和解析器,而事实上大部分时候,这个过程只是在搬运服务端的模型定义。因而在这里咱们约定了解析器做的事件仅仅只是调用服务端的 RPC 接口,那么只有开发者选定 RPC 接口,咱们就能够依据其响应的元信息拉取到服务端的数据模型,从而建设数据图。

比方在下面的例子里,当咱们要导入 song 这个字段时,零碎实际上是在仓库的约定门路下建设了两份文件:

  • resolver.json:形容引擎该如何调用接口,比方 RPC 接口的类名,办法名等等
  • type.schema:保留依据接口响应生成的 GraphQL 模型信息

上面是一份最繁难的 resolver.json 示例:

{
  "type": "rpc", // 调用的协定类型
  "clzName": "com.netease.music.api.SongService", // RPC 类名
  "methodName": "getSongById", // RPC 办法
  "params": [] // RPC 参数类型列表}

type.schema 其实就是 GraphQL 的模型定义:

type Query {song: Song}
type Song {
  id: ID
  name: String
}

那么,咱们是如何生成这份模型信息的呢?

通过钻研 GraphQL 的引擎源码,咱们发现,GraphQL 的模型定义,其实能够通过官网引擎提供的内置办法,等价转换称一份规范的 JSON 构造。那么,绝对于生成模型定义的源码,生成这份 JSON 构造要简略得多,比方,上文提到的 Song 模型,就能够进行如下转换:

import {introspectionFromSchema, buildSchema} from 'graphql';

const schema = introspectionFromSchema(buildSchema(schema));

转换后能够生成如下构造:

而在云音乐,所有服务端的接口模型定义都会保护在云音乐的契约治理平台,同样具备一份 JSON 构造来形容。

<img src=”https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/23211534140/88f9/7d3c/9211/bb2d1ec275ba645f82ad16211e802b9b.png” width=”40%”/>

这两份数据在逻辑上简直齐全等价,咱们编写了一个转换器,定义一些从 Java 类型到 GraphQL 类型的映射关系,即可实现转换,最终生成 GraphQL 须要的类型定义,并保留在咱们的 Git 仓库中。

在数据图的展现上,咱们间接采纳了 graphql-voyager 这个开源库,通过一些扩大让其具备了字段编辑能力,字段搜寻等开发过程中常常要用到的一些能力。

基于 AST 打造 LowCode GraphQL 编辑模式

有了类型定义之后,接下来,咱们就开始思考如何编写 GraphQL 语句了,下图是咱们的编辑界面。

编辑器采纳的理论是 LowCode 和 ProCode 双重模式,大部分时候,开发者只须要进行字段的筛选,以及一些指令表单的配置,即可实现 GraphQL 语句的编辑。

那么,咱们是如何实现这一成果的呢?

GraphQL 官网提供了语句编辑器 graphiql 曾经十分弱小,提供了语法提醒,谬误校验,语句调试等根本能力。但为了在团队外部大规模推广,这样的应用形式还是相对来说比拟原始,咱们须要进一步升高开发者的应用老本,提供 LowCode 的编辑模式。

这里咱们就会提到为什么说 GraphQL 人造适宜 LowCode 的编辑模式,咱们晓得,所有低代码编辑模式都须要定义一套标准协议,而界面的大部分操作都能够映射成对该协定的操作变更。

而针对一段 GraphQL 语句,通过官网引擎提供的内置办法,咱们能够轻松获取到它的 AST 构造,并且这段 AST 构造非常容易浏览和了解:

{song @param(from: "$query.id") {name}
}

通过调用转换方法,能够失去如下构造:

import {parse} from 'graphql';

const ast = parse(query); 

针对这部分构造,咱们能够和界面建设映射关系,比方当咱们通过对数据图文档进行字段勾选时,理论是生成相应的 selection 构造,并将其插入到指定门路的 selections 中。而当咱们通过表单配置指令时,批改的就是相应门路的 directives 构造。

并且因为咱们操作的是 AST 自身,所以开发者同样能够自行进行 GraphQL 语句的编写,语句的变更同样可能在操作面板体现进去。

除去低代码编辑能力外,编辑器还提供了一些辅助性能,这些性能能够让 GraphQL 接口的开发更加晦涩便当,比方:

  • 主动生成接口文档:GraphQL 的查问后果属于数据图的子集,这样咱们齐全能够依据开发者的 GraphQL 语句生成响应构造,并剖析出依赖的参数,从而主动生成接口文档,让 GraphQL 接口也能领有清晰的定义。
  • 追溯申请链路:在线开发最大的难点就在于问题的定位和调试,为了帮忙开发者更轻松的定位问题,咱们针对线下环境 GraphQL 语句的每一步操作都进行了打点,包含每一次 RPC 调用,脚本执行,并且记录了每一次操作的输出和输入,这样,开发者在进行了一次查问后,就能够查看残缺的申请链路,在申请出错时进行问题的定位。

基于指令和脚本强化原生 GraphQL 能力

刚刚咱们提到,要用 GraphQL 满足至多 70% 的 BFF 编排场景,如果只是用开源引擎,咱们很快就发现了上面的问题。

第一个问题是,咱们如何传递简单的 RPC 参数?在理论的业务场景里,因为 RPC 和 HTTP 接口曾经解耦,咱们往往须要通过一些逻辑结构能力结构出 RPC 须要的参数,比方在上面的 RPC 接口:

Class SearchDto {
  Integer pageSize;
  Integer cursor;
  Integer userId; // 须要获取登陆用户的 uesrId
  String search;
}
...
SongService.searchSongByUser(SearchDto params) {...}

这个接口的入参是一个结构化对象,其中其余 3 个参数来源于 HTTP 接口的查问参数透传,而 userId 则须要咱们从申请的 cookie 中解析进去。

GraphQL 提供了 变量 的机制,用来进行一些参数的透传,但如果要实现上述的参数结构,它的灵便度是不够的。

第二个问题是,咱们如何对响应后果做更灵便的数据转换?GraphQL 的响应后果和必须和 Schema 构造放弃严格统一,尽管咱们能够进行肯定的字段裁剪和重命名,但针对多样的页面,咱们须要更加灵便的数据转换,以便能够复用同一套 Schema 去面对更多场景。

下面两个问题的共性是,GraphQL 默认的 DSL 表白难以满足简单场景的诉求。侥幸的是,GraphQL 提供了一种名为指令的扩大机制。指令能够附着在字段或者片段蕴含的字段上,而后以任何服务端期待的形式来扭转查问的执行,上面是 GraphQL 引擎内置的 @skip 指令的应用示例。

{
  song {name @skip(if: true)
  }
}

上述指令的含意是,在判断条件为 true 时,跳过此字段的查问。

GraphQL 容许咱们自定义指令,咱们能够在 GraphQL 的解析器中拿到查问语句附着的指令形容,从而批改执行逻辑来实现指令的实现。

咱们针对下面提到的问题提供了两种自定义指令。

@param 指令:传递简单的 RPC 参数

directive @param(
  from: String
  dest: String
  scriptName: String
  scriptMethod: String
)

@param 指令次要在执行查问操作 之前 运行,负责收集参数起源,并将多个参数起源传入脚本进行解决,最终将处理结果传递到 RPC 参数中,它的执行流程如下图所示:

@convert 指令:对响应后果做更灵便的数据转换

directive @convert(
  from: String
  scriptName: String
  scriptMethod: String
)

@convert 指令次要在执行查问操作 之后 运行,负责收集响应后果,同样其输出到脚本进行解决,最终返回通过脚本解决好的后果,它的执行流程如下图所示:

在扩大了这两种指令后,开发者能够在查问操作的前后插入自定义脚本进行参数的结构和响应后果的解决。

咱们目前是基于 Java 实现的 GraphQL 引擎,因而脚本语言上采纳了 Groovy 语法,只管不是前端同学相熟的语言,但解决一些惯例的数据转换逻辑曾经入不敷出。而在实现这一部分后,咱们真正做到了简直能笼罩大部分 BFF 场景。

规范的研发流程管控

咱们冀望 GraphQL 利用的研发流程应该和一般利用的研发流程一样,当开发者接到需要时,他须要在平台创立迭代,咱们会为它调配分支和环境,当他测试回归实现后,须要进行一些卡点,咱们会在卡点环节提供一些语法校验以及变更的 Review,通过卡点流程后,开发者就会进入独占的上线通道,实现线上公布和验证后,开发者能够一键将开发分支合并到 master。

目前在云音乐,所有前端侧的利用研发都遵循这样一套流程,这套流程极大保障了咱们研发过程中的安全性和规范性。针对 GraphQL,咱们在利用卡点环节提供了语法校验,基于 graphql-language-service-interface 提供的 getDiagnostics,能够帮忙咱们疾速定位到谬误的地位。

const errList = getDiagnostics(query, schema);

小结

最初,总结一下,在本文中,咱们简略介绍了 GraphQL 以及在云音乐落地的背景,并且介绍了云音乐 Febase 平台 GraphQL 研发能力的整体架构设计,一些要害模块(数据图结构,低代码 GraphQL 编辑器)的实现思路,以及针对 GraphQL 引擎的扩大设计,GraphQL 利用的研发流程管控。前面咱们也会思考分享更多 GraphQL 引擎的实现细节以及 GraphQL 的利用案例。

目前,基于 GraphQL 的 BFF 研发模式曾经在云音乐实现了半年左右,期间也由大前端同学自主产出了 160+ 的数据接口,其中不乏一些高流量的外围场景。当然,针对 BFF 研发模式,咱们的确也还处在起步的摸索阶段。将来,随着 GraphQL 接口在云音乐业务中的覆盖度越来越高,咱们冀望可能从中总结出一些数据图模型的设计教训,帮忙前后端同学建设更高效的协作关系。

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0